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>
73 lines
2.3 KiB
PHP
73 lines
2.3 KiB
PHP
#!/usr/bin/env php
|
|
<?php
|
|
/**
|
|
* Migration: Add closed_at column to tickets table
|
|
*
|
|
* Adds a dedicated timestamp for when tickets are closed,
|
|
* so avg resolution time isn't inflated by post-close edits.
|
|
*
|
|
* Usage: php scripts/add_closed_at_column.php
|
|
*/
|
|
|
|
require_once dirname(__DIR__) . '/config/config.php';
|
|
|
|
$conn = new mysqli(
|
|
$GLOBALS['config']['DB_HOST'],
|
|
$GLOBALS['config']['DB_USER'],
|
|
$GLOBALS['config']['DB_PASS'],
|
|
$GLOBALS['config']['DB_NAME']
|
|
);
|
|
|
|
if ($conn->connect_error) {
|
|
die("Database connection failed: " . $conn->connect_error . "\n");
|
|
}
|
|
|
|
echo "Adding closed_at column to tickets table...\n";
|
|
|
|
// Add the column if it doesn't exist
|
|
$result = $conn->query("SHOW COLUMNS FROM tickets LIKE 'closed_at'");
|
|
if ($result->num_rows > 0) {
|
|
echo "Column 'closed_at' already exists, skipping ALTER TABLE.\n";
|
|
} else {
|
|
$sql = "ALTER TABLE tickets ADD COLUMN closed_at TIMESTAMP NULL DEFAULT NULL AFTER updated_at";
|
|
if ($conn->query($sql)) {
|
|
echo "Column added successfully.\n";
|
|
} else {
|
|
die("Failed to add column: " . $conn->error . "\n");
|
|
}
|
|
|
|
// Add index for stats queries
|
|
$conn->query("CREATE INDEX idx_tickets_closed_at ON tickets (closed_at)");
|
|
echo "Index created.\n";
|
|
}
|
|
|
|
// Backfill: For existing closed tickets, use the audit log to find when they were closed
|
|
echo "\nBackfilling closed_at from audit log...\n";
|
|
|
|
$sql = "UPDATE tickets t
|
|
JOIN (
|
|
SELECT entity_id as ticket_id, MIN(created_at) as first_closed
|
|
FROM audit_log
|
|
WHERE entity_type = 'ticket'
|
|
AND action_type = 'update'
|
|
AND details LIKE '%\"status\":\"Closed\"%'
|
|
GROUP BY entity_id
|
|
) al ON t.ticket_id = al.ticket_id
|
|
SET t.closed_at = al.first_closed
|
|
WHERE t.status = 'Closed' AND t.closed_at IS NULL";
|
|
|
|
$result = $conn->query($sql);
|
|
$backfilled = $conn->affected_rows;
|
|
echo "Backfilled $backfilled tickets from audit log.\n";
|
|
|
|
// For any remaining closed tickets without audit log entries, use updated_at as fallback
|
|
$sql = "UPDATE tickets SET closed_at = updated_at WHERE status = 'Closed' AND closed_at IS NULL";
|
|
$conn->query($sql);
|
|
$fallback = $conn->affected_rows;
|
|
if ($fallback > 0) {
|
|
echo "Used updated_at as fallback for $fallback tickets without audit log entries.\n";
|
|
}
|
|
|
|
echo "\nMigration complete!\n";
|
|
$conn->close();
|