diff --git a/agent/reports/client_ticket_time_detail.php b/agent/reports/client_ticket_time_detail.php new file mode 100644 index 00000000..b84f3402 --- /dev/null +++ b/agent/reports/client_ticket_time_detail.php @@ -0,0 +1,383 @@ + 24h by using hours > 24) + */ +function secondsToHmsString($seconds) { + $seconds = (int) max(0, $seconds); + $hours = intdiv($seconds, 3600); + $minutes = intdiv($seconds % 3600, 60); + $secs = $seconds % 60; + return sprintf('%02d:%02d:%02d', $hours, $minutes, $secs); +} + +/** + * 15-minute round up, return decimal hours in 0.25 increments + * NOTE: In this report, billed hours are calculated per TICKET total + * (sum of reply time within range, then rounded up to nearest 15 minutes). + */ +function secondsToQuarterHourDecimal($seconds) { + $seconds = (int) max(0, $seconds); + if ($seconds === 0) return 0.00; + + $quarters = (int) ceil($seconds / 900); // 900 seconds = 15 minutes + return $quarters * 0.25; +} + +/** + * Validate YYYY-MM-DD + */ +function isValidDateYmd($s) { + return is_string($s) && preg_match('/^\d{4}-\d{2}-\d{2}$/', $s); +} + +// Default range: current month +$from = isset($_GET['from']) ? $_GET['from'] : date('Y-m-01'); +$to = isset($_GET['to']) ? $_GET['to'] : date('Y-m-t'); + +if (!isValidDateYmd($from)) $from = date('Y-m-01'); +if (!isValidDateYmd($to)) $to = date('Y-m-t'); + +// Inclusive datetime bounds +$from_dt = $from . " 00:00:00"; +$to_dt = $to . " 23:59:59"; + +$billable_only = (isset($_GET['billable_only']) && (int)$_GET['billable_only'] === 1) ? 1 : 0; + +// Ticket-level billable flag (same as your original report) +$billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : ""; + +/** + * Query returns ONLY replies that have time_worked and are within date range. + * Reply content column = tr.ticket_reply + */ +$stmt = $mysqli->prepare(" + SELECT + c.client_id, + c.client_name, + t.ticket_id, + t.ticket_prefix, + t.ticket_number, + t.ticket_subject, + + tr.ticket_reply_id, + tr.ticket_reply_created_at, + tr.ticket_reply_time_worked, + TIME_TO_SEC(tr.ticket_reply_time_worked) AS reply_time_seconds, + tr.ticket_reply AS reply_content + + FROM tickets t + INNER JOIN clients c + ON c.client_id = t.ticket_client_id + + INNER JOIN ticket_replies tr + ON tr.ticket_reply_ticket_id = t.ticket_id + AND tr.ticket_reply_time_worked IS NOT NULL + AND TIME_TO_SEC(tr.ticket_reply_time_worked) > 0 + AND tr.ticket_reply_created_at BETWEEN ? AND ? + + WHERE c.client_archived_at IS NULL + $billable_sql + + ORDER BY c.client_name ASC, + t.ticket_number ASC, + t.ticket_id ASC, + tr.ticket_reply_created_at ASC +"); +$stmt->bind_param("ss", $from_dt, $to_dt); +$stmt->execute(); +$result = $stmt->get_result(); + +?> +
| Ticket / Replies with Time | +Time Worked | +Billable (hrs) | +
|---|---|---|
| Ticket Total for | ++ | + | '; + } + + // Client subtotal + ?> + |
| + Total for ( tickets) + | ++ | + |
| + | '; + + // Reset ticket accumulator + $ticket_time_seconds = 0; + $current_ticket_id = null; + $current_ticket_label_html = null; + } + + // Ticket header (first row for this ticket) + if ($ticket_id !== $current_ticket_id) { + $current_ticket_id = $ticket_id; + $current_ticket_label_html = $ticket_label_html; + + $client_ticket_count++; + $grand_ticket_count++; + + ?> + | |
| + | + | + |
|
+
+
+
+
+
+ |
+ + | + |
| + No ticket replies with time worked found for this date range. + | +'; + } + + // Close last client subtotal + ?> + | |
| + Total for ( tickets) + | ++ | + |
| + Grand Total ( tickets) + | ++ | + |
Client Time Detail Audit
+ +| Ticket | -Time Worked | -Billable (hrs) | -
|---|---|---|
| - Total for ( tickets) - | -- | - |
| - | ||
| = "$display_ticket - $ticket_subject" ?> | -- | - |
| No tickets with time worked found for this month. | -||
| - Total for ( tickets) - | -- | - |
| - Grand Total ( tickets) - | -- | - |