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 Time Time Worked Billable (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.