diff --git a/agent/reports/client_ticket_time_detail.php b/agent/reports/client_ticket_time_detail.php
index bfafacf4..802a4df4 100644
--- a/agent/reports/client_ticket_time_detail.php
+++ b/agent/reports/client_ticket_time_detail.php
@@ -2,7 +2,7 @@
require_once "includes/inc_all_reports.php";
-enforceUserPermission('module_support');
+enforceUserPermission('module_sales');
/**
* Convert seconds to "HH:MM:SS" (supports totals > 24h by using hours > 24)
@@ -21,10 +21,21 @@ function secondsToHmsString($seconds) {
function secondsToDecimalHours($seconds) {
$seconds = (int) max(0, $seconds);
if ($seconds === 0) return 0.00;
-
return round($seconds / 3600, 2);
}
+/**
+ * Round UP seconds to the nearest increment (in seconds).
+ */
+function secondsRoundUpToIncrement($seconds, $increment_seconds) {
+ $seconds = (int) max(0, $seconds);
+ $increment_seconds = (int) max(1, $increment_seconds);
+
+ if ($seconds === 0) return 0;
+
+ return (int) (ceil($seconds / $increment_seconds) * $increment_seconds);
+}
+
/**
* Validate YYYY-MM-DD
*/
@@ -32,6 +43,16 @@ function isValidDateYmd($s) {
return is_string($s) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $s);
}
+/**
+ * Billing increment options
+ * Key = hours (string), Value = increment seconds
+ */
+$billing_increment_options = [
+ '0.1' => 6 * 60, // 6 minutes
+ '0.25' => 15 * 60, // 15 minutes [DEFAULT]
+ '0.5' => 30 * 60, // 30 minutes
+];
+
// Default range: current month
$from = isset($_GET['from']) ? $_GET['from'] : date('Y-m-01');
$to = isset($_GET['to']) ? $_GET['to'] : date('Y-m-t');
@@ -45,6 +66,14 @@ $to_dt = $to . " 23:59:59";
$billable_only = (isset($_GET['billable_only']) && (int)$_GET['billable_only'] === 1) ? 1 : 0;
+// Billing increment selection (default 0.25)
+$billing_increment_key = isset($_GET['billing_increment']) ? (string)$_GET['billing_increment'] : '0.25';
+if (!array_key_exists($billing_increment_key, $billing_increment_options)) {
+ $billing_increment_key = '0.25';
+}
+$billing_increment_seconds = $billing_increment_options[$billing_increment_key];
+$billing_increment_minutes = (int) round($billing_increment_seconds / 60);
+
// Ticket-level billable flag (same as your original report)
$billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : "";
@@ -120,6 +149,21 @@ $result = $stmt->get_result();
+
+ Billing time increment
+
+ >0.1 hour (6 minutes)
+ >0.25 hour (15 minutes)
+ >0.5 hour (30 minutes)
+
+
+
+
+
+ Apply
+
+
+
-
-
- Apply
-
-
+
@@ -157,9 +197,9 @@ $result = $stmt->get_result();
Ticket Total for
@@ -178,13 +218,16 @@ $result = $stmt->get_result();
$client_ticket_count = 0;
$client_time_seconds = 0;
- $client_billed_hours = 0.0;
+
+ // Billable seconds are based on rounding each reply UP to the chosen increment
+ $client_billable_seconds = 0;
$ticket_time_seconds = 0;
+ $ticket_billable_seconds = 0;
$grand_ticket_count = 0;
$grand_time_seconds = 0;
- $grand_billed_hours = 0.0;
+ $grand_billable_seconds = 0;
$had_rows = false;
@@ -203,18 +246,14 @@ $result = $stmt->get_result();
$reply_seconds = (int)$r['reply_time_seconds'];
$reply_hms = secondsToHmsString($reply_seconds);
+ // Rounded-up billable seconds for THIS reply
+ $reply_billable_seconds = secondsRoundUpToIncrement($reply_seconds, $billing_increment_seconds);
+
// Reply content: escape for safety, keep line breaks readable
$reply_content_raw = $r['reply_content'] ?? '';
- // Remove all HTML tags completely
$reply_content_clean = strip_tags($reply_content_raw);
-
- // Normalize line breaks (convert CRLF/CR to LF)
$reply_content_clean = str_replace(["\r\n", "\r"], "\n", $reply_content_clean);
-
- // Collapse excessive blank lines (more than 2 into 2)
$reply_content_clean = preg_replace("/\n{3,}/", "\n\n", $reply_content_clean);
-
- // Escape safely for output
$reply_content_html = nl2br(nullable_htmlentities(trim($reply_content_clean)));
// Close out previous client if client changed
@@ -222,25 +261,24 @@ $result = $stmt->get_result();
// Close out previous ticket (if any)
if ($current_ticket_id !== null) {
- $ticket_billed = $printTicketSubtotalRow($current_ticket_label_html, $ticket_time_seconds);
- $client_billed_hours += $ticket_billed;
- $grand_billed_hours += $ticket_billed;
+ $printTicketSubtotalRow($current_ticket_label_html, $ticket_time_seconds, $ticket_billable_seconds);
$ticket_time_seconds = 0;
+ $ticket_billable_seconds = 0;
$current_ticket_id = null;
$current_ticket_label_html = null;
echo ' ';
}
- // Client subtotal
+ // Client subtotal (billable based on sum of rounded replies across all tickets)
?>
Total for ( tickets)
-
+
get_result();
// Reset client totals
$client_ticket_count = 0;
$client_time_seconds = 0;
- $client_billed_hours = 0.0;
+ $client_billable_seconds = 0;
}
// Client header
@@ -269,16 +307,13 @@ $result = $stmt->get_result();
// Ticket changed: close previous ticket subtotal
if ($current_ticket_id !== null && $ticket_id !== $current_ticket_id) {
- $ticket_billed = $printTicketSubtotalRow($current_ticket_label_html, $ticket_time_seconds);
-
- // Add billed totals once per ticket
- $client_billed_hours += $ticket_billed;
- $grand_billed_hours += $ticket_billed;
+ $printTicketSubtotalRow($current_ticket_label_html, $ticket_time_seconds, $ticket_billable_seconds);
echo ' ';
- // Reset ticket accumulator
+ // Reset ticket accumulators
$ticket_time_seconds = 0;
+ $ticket_billable_seconds = 0;
$current_ticket_id = null;
$current_ticket_label_html = null;
}
@@ -300,7 +335,7 @@ $result = $stmt->get_result();
@@ -311,15 +346,19 @@ $result = $stmt->get_result();
-
+
get_result();
} else {
// Close last ticket subtotal
if ($current_ticket_id !== null) {
- $ticket_billed = $printTicketSubtotalRow($current_ticket_label_html, $ticket_time_seconds);
- $client_billed_hours += $ticket_billed;
- $grand_billed_hours += $ticket_billed;
-
+ $printTicketSubtotalRow($current_ticket_label_html, $ticket_time_seconds, $ticket_billable_seconds);
echo ' ';
}
@@ -347,7 +383,7 @@ $result = $stmt->get_result();
Total for ( tickets)
-
+
@@ -358,7 +394,7 @@ $result = $stmt->get_result();
Grand Total ( tickets)
-
+
get_result();
This report shows only ticket replies with time worked within the selected date range.
- Ticket “Billable (hrs)” totals are calculated by summing reply time per ticket within the range,
- then rounding that ticket total up to the nearest 15 minutes (0.25 hours).
+ “Billable (hrs)” is calculated by rounding each reply up to the nearest minutes ( hours),
+ then summing those rounded values for ticket/client/grand totals.
Reply content is displayed under each reply timestamp.