Add Billing Time Increment Option in Client Ticket Time Detail Report, this option will later be available globally

This commit is contained in:
johnnyq
2026-03-04 18:33:08 -05:00
parent 47b8ec6f96
commit 203b161e82

View File

@@ -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();
<input type="date" class="form-control" name="to" value="<?php echo nullable_htmlentities($to); ?>">
</div>
<div class="col-md-3 mb-2">
<label class="mb-1">Billing time increment</label>
<select class="form-control" name="billing_increment">
<option value="0.1" <?php echo ($billing_increment_key === '0.1') ? 'selected' : ''; ?>>0.1 hour (6 minutes)</option>
<option value="0.25" <?php echo ($billing_increment_key === '0.25') ? 'selected' : ''; ?>>0.25 hour (15 minutes)</option>
<option value="0.5" <?php echo ($billing_increment_key === '0.5') ? 'selected' : ''; ?>>0.5 hour (30 minutes)</option>
</select>
</div>
<div class="col-md-2 mb-2 d-flex align-items-end ml-auto">
<button type="submit" class="btn btn-success btn-block">
<i class="fas fa-fw fa-filter mr-1"></i>Apply
</button>
</div>
<div class="col-md-4 mb-2 d-flex align-items-end">
<div class="custom-control custom-checkbox">
<input
@@ -134,11 +178,7 @@ $result = $stmt->get_result();
</div>
</div>
<div class="col-md-2 mb-2 d-flex align-items-end">
<button type="submit" class="btn btn-success btn-block">
<i class="fas fa-fw fa-filter mr-1"></i>Apply
</button>
</div>
</div>
</form>
</div>
@@ -157,9 +197,9 @@ $result = $stmt->get_result();
<tbody>
<?php
// Helper: print ticket subtotal row
$printTicketSubtotalRow = function($ticket_label_html, $ticket_seconds) {
$ticket_billed = secondsToDecimalHours($ticket_seconds);
// Helper: print ticket subtotal row (billable hours = sum of rounded replies for that ticket)
$printTicketSubtotalRow = function($ticket_label_html, $ticket_seconds, $ticket_billable_seconds) {
$ticket_billed = secondsToDecimalHours($ticket_billable_seconds);
?>
<tr class="font-weight-bold">
<td class="text-right pr-3">Ticket Total for <?php echo $ticket_label_html; ?></td>
@@ -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 '<tr><td colspan="3"></td></tr>';
}
// Client subtotal
// Client subtotal (billable based on sum of rounded replies across all tickets)
?>
<tr class="font-weight-bold">
<td class="text-right">
Total for <?php echo $current_client_name; ?> (<?php echo $client_ticket_count; ?> tickets)
</td>
<td class="text-right"><?php echo formatDuration(secondsToHmsString($client_time_seconds)); ?></td>
<td class="text-right"><?php echo number_format($client_billed_hours, 2); ?></td>
<td class="text-right"><?php echo number_format(secondsToDecimalHours($client_billable_seconds), 2); ?></td>
</tr>
<tr><td colspan="3"></td></tr>
<?php
@@ -248,7 +286,7 @@ $result = $stmt->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 '<tr><td colspan="3"></td></tr>';
// 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();
<?php
}
// Reply row (indented) - date/time + reply content + time
// Reply row (indented)
?>
<tr>
<td class="pl-4 text-muted">
@@ -311,15 +346,19 @@ $result = $stmt->get_result();
</div>
</td>
<td class="text-right"><?php echo formatDuration($reply_hms); ?></td>
<td class="text-right"><?php echo number_format(secondsToDecimalHours($reply_seconds), 2); ?></td>
<td class="text-right"><?php echo number_format(secondsToDecimalHours($reply_billable_seconds), 2); ?></td>
</tr>
<?php
// Totals
// Totals (raw time)
$ticket_time_seconds += $reply_seconds;
$client_time_seconds += $reply_seconds;
$grand_time_seconds += $reply_seconds;
$grand_time_seconds += $reply_seconds;
// Totals (billable time = sum of rounded replies)
$ticket_billable_seconds += $reply_billable_seconds;
$client_billable_seconds += $reply_billable_seconds;
$grand_billable_seconds += $reply_billable_seconds;
}
if (!$had_rows) {
@@ -333,10 +372,7 @@ $result = $stmt->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 '<tr><td colspan="3"></td></tr>';
}
@@ -347,7 +383,7 @@ $result = $stmt->get_result();
Total for <?php echo $current_client_name; ?> (<?php echo $client_ticket_count; ?> tickets)
</td>
<td class="text-right"><?php echo formatDuration(secondsToHmsString($client_time_seconds)); ?></td>
<td class="text-right"><?php echo number_format($client_billed_hours, 2); ?></td>
<td class="text-right"><?php echo number_format(secondsToDecimalHours($client_billable_seconds), 2); ?></td>
</tr>
<tr><td colspan="3"></td></tr>
@@ -358,7 +394,7 @@ $result = $stmt->get_result();
Grand Total (<?php echo $grand_ticket_count; ?> tickets)
</td>
<td class="text-right"><?php echo formatDuration(secondsToHmsString($grand_time_seconds)); ?></td>
<td class="text-right"><?php echo number_format($grand_billed_hours, 2); ?></td>
<td class="text-right"><?php echo number_format(secondsToDecimalHours($grand_billable_seconds), 2); ?></td>
</tr>
<?php
}
@@ -368,8 +404,8 @@ $result = $stmt->get_result();
<small class="text-muted p-2">
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 <?php echo (int)$billing_increment_minutes; ?> minutes (<?php echo nullable_htmlentities($billing_increment_key); ?> hours),
then summing those rounded values for ticket/client/grand totals.
<br>
Reply content is displayed under each reply timestamp.
</small>