Fix avg resolution time using dedicated closed_at column

The dashboard's "Avg Resolution" stat was using updated_at, which gets
overwritten on any post-close edit (title change, comment, etc.),
inflating the metric. Also fixes "Closed Today" count for the same reason.

- Add closed_at TIMESTAMP column to tickets table
- Set closed_at on close, preserve on re-edit, clear on reopen
- Update StatsModel queries to use closed_at instead of updated_at
- Add migration script with audit log backfill for existing tickets

Run: php scripts/add_closed_at_column.php

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-02-11 14:59:35 -05:00
parent bcc163bc77
commit 13f0fab138
3 changed files with 90 additions and 11 deletions

View File

@@ -216,6 +216,9 @@ class TicketModel {
* @return array ['success' => bool, 'error' => string|null, 'conflict' => bool]
*/
public function updateTicket(array $ticketData, ?int $updatedBy = null, ?string $expectedUpdatedAt = null): array {
// closed_at: set on close (preserve if already set), clear on reopen
$closedAtClause = "closed_at = CASE WHEN ? = 'Closed' THEN COALESCE(closed_at, NOW()) ELSE NULL END";
// Build query with optional optimistic locking
if ($expectedUpdatedAt !== null) {
// Optimistic locking enabled - check that updated_at hasn't changed
@@ -227,7 +230,8 @@ class TicketModel {
category = ?,
type = ?,
updated_by = ?,
updated_at = NOW()
updated_at = NOW(),
$closedAtClause
WHERE ticket_id = ? AND updated_at = ?";
} else {
// No optimistic locking
@@ -239,7 +243,8 @@ class TicketModel {
category = ?,
type = ?,
updated_by = ?,
updated_at = NOW()
updated_at = NOW(),
$closedAtClause
WHERE ticket_id = ?";
}
@@ -250,7 +255,7 @@ class TicketModel {
if ($expectedUpdatedAt !== null) {
$stmt->bind_param(
"sissssiis",
"sissssisis",
$ticketData['title'],
$ticketData['priority'],
$ticketData['status'],
@@ -258,12 +263,13 @@ class TicketModel {
$ticketData['category'],
$ticketData['type'],
$updatedBy,
$ticketData['status'],
$ticketData['ticket_id'],
$expectedUpdatedAt
);
} else {
$stmt->bind_param(
"sissssii",
"sissssisi",
$ticketData['title'],
$ticketData['priority'],
$ticketData['status'],
@@ -271,6 +277,7 @@ class TicketModel {
$ticketData['category'],
$ticketData['type'],
$updatedBy,
$ticketData['status'],
$ticketData['ticket_id']
);
}