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) | ||