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(); + +?> +
+
+

+ + Client Time Detail Audit Report ( to ) + + Billable Only + +

+
+ +
+
+ +
+ +
+
+
+ + +
+ +
+ + +
+ +
+
+ + > + +
+
+ +
+ +
+
+
+
+
+ +
+
+ + + + + + + + + + + + + + + + + '; + } + + // Client subtotal + ?> + + + + + + + + + + + '; + + // 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++; + + ?> + + + + + + + + + + + + + + + + '; + } + + // Close last client subtotal + ?> + + + + + + + + + + + + + + + + +
Ticket / Replies with TimeTime WorkedBillable (hrs)
Ticket Total for
+ Total for ( tickets) +
+ + +
+ +
+
+ No ticket replies with time worked found for this date range. +
+ Total for ( tickets) +
+ Grand Total ( tickets) +
+ + + 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). +
+ Reply content is displayed under each reply timestamp. +
+
+
+ +Unbilled Tickets

+ diff --git a/agent/reports/ticket_by_client_v2.php b/agent/reports/ticket_by_client_v2.php deleted file mode 100644 index 25e5c864..00000000 --- a/agent/reports/ticket_by_client_v2.php +++ /dev/null @@ -1,279 +0,0 @@ - 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 - * Examples: - * 1 min => 0.25 - * 16 min => 0.50 - * 61 min => 1.25 - */ -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; -} - -$year = isset($_GET['year']) ? (int) $_GET['year'] : (int) date('Y'); -$month = isset($_GET['month']) ? (int) $_GET['month'] : (int) date('m'); -if ($month < 1 || $month > 12) $month = (int) date('m'); - -$billable_only = (isset($_GET['billable_only']) && (int) $_GET['billable_only'] === 1) ? 1 : 0; - -// Used for Year dropdown -$sql_ticket_years = mysqli_query($mysqli, "SELECT DISTINCT YEAR(ticket_created_at) AS ticket_year FROM tickets ORDER BY ticket_year DESC"); - -// Billable filter (adjust field name if yours differs) -$billable_sql = $billable_only ? " AND t.ticket_billable = 1 " : ""; - -/** - * IMPORTANT: - * This sums time worked ONLY for replies within the selected month/year - * by filtering on tr.ticket_reply_created_at. - * If your column name differs, replace ticket_reply_created_at accordingly. - */ -$stmt = $mysqli->prepare(" - SELECT - c.client_id, - c.client_name, - t.ticket_id, - t.ticket_prefix, - t.ticket_number, - t.ticket_subject, - SEC_TO_TIME(COALESCE(SUM(TIME_TO_SEC(tr.ticket_reply_time_worked)), 0)) AS ticket_time_hms, - COALESCE(SUM(TIME_TO_SEC(tr.ticket_reply_time_worked)), 0) AS ticket_time_seconds - FROM tickets t - INNER JOIN clients c - ON c.client_id = t.ticket_client_id - LEFT JOIN ticket_replies tr - ON tr.ticket_reply_ticket_id = t.ticket_id - AND tr.ticket_reply_time_worked IS NOT NULL - AND YEAR(tr.ticket_reply_created_at) = ? - AND MONTH(tr.ticket_reply_created_at) = ? - WHERE c.client_archived_at IS NULL - $billable_sql - GROUP BY t.ticket_id - HAVING ticket_time_seconds > 0 - ORDER BY c.client_name ASC, t.ticket_number ASC, t.ticket_id ASC -"); -$stmt->bind_param("ii", $year, $month); -$stmt->execute(); -$result = $stmt->get_result(); - -?> - -
-
-

- - Ticket By Client () - - Billable Only - -

-
- -
-
- -
- - -
-
-
- - -
- -
- - -
- -
-
- - > - -
-
- -
- -
-
-
-
-
- -
-
- - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
TicketTime WorkedBillable (hrs)
- Total for ( tickets) -
No tickets with time worked found for this month.
- Total for ( tickets) -
- Grand Total ( tickets) -
- - - Billed hours are calculated per ticket by rounding that ticket’s worked time up to the nearest 15 minutes (0.25 hours). - -
-
- -