fix: Enable proper sorting for Created By and Assigned To columns

Fixed server-side sorting for user-related columns on dashboard:

Problem:
- Clicking "Created By" or "Assigned To" headers didn't sort
- Columns were missing from $allowedColumns validation
- Fell back to ticket_id sort, appearing random to users

Solution:
1. Added 'created_by' and 'assigned_to' to $allowedColumns array

2. Smart sort expression mapping:
   - created_by → sorts by display_name/username (not user ID)
   - assigned_to → uses CASE to put unassigned at end, then sorts by name
   - Other columns → use table prefix (t.column_name)

3. Database-level NULL handling for assigned_to:
   - Uses CASE WHEN to sort unassigned tickets last
   - Regardless of ASC/DESC direction
   - Then alphabetically sorts assigned users

Result:
- A→Z: Alice, Bob, Charlie... Unassigned
- Z→A: Zack, Yolanda, Xavier... Unassigned
- Consistent grouping and predictable order

Co-Authored-By: Claude Sonnet 4.5 <noreply@anthropic.com>
This commit is contained in:
2026-01-09 16:42:13 -05:00
parent 08a73eb84c
commit a3298e7dbe

View File

@@ -151,26 +151,38 @@ class TicketModel {
} }
// Validate sort column to prevent SQL injection // Validate sort column to prevent SQL injection
$allowedColumns = ['ticket_id', 'title', 'status', 'priority', 'category', 'type', 'created_at', 'updated_at']; $allowedColumns = ['ticket_id', 'title', 'status', 'priority', 'category', 'type', 'created_at', 'updated_at', 'created_by', 'assigned_to'];
if (!in_array($sortColumn, $allowedColumns)) { if (!in_array($sortColumn, $allowedColumns)) {
$sortColumn = 'ticket_id'; $sortColumn = 'ticket_id';
} }
// Map column names to actual sort expressions
// For user columns, sort by display name with NULL handling for unassigned
$sortExpression = $sortColumn;
if ($sortColumn === 'created_by') {
$sortExpression = "COALESCE(u_created.display_name, u_created.username, 'System')";
} elseif ($sortColumn === 'assigned_to') {
// Put unassigned (NULL) at the end regardless of sort direction
$sortExpression = "CASE WHEN t.assigned_to IS NULL THEN 1 ELSE 0 END, COALESCE(u_assigned.display_name, u_assigned.username)";
} else {
$sortExpression = "t.$sortColumn";
}
// Validate sort direction // Validate sort direction
$sortDirection = strtolower($sortDirection) === 'asc' ? 'ASC' : 'DESC'; $sortDirection = strtolower($sortDirection) === 'asc' ? 'ASC' : 'DESC';
// Get total count for pagination // Get total count for pagination
$countSql = "SELECT COUNT(*) as total FROM tickets $whereClause"; $countSql = "SELECT COUNT(*) as total FROM tickets $whereClause";
$countStmt = $this->conn->prepare($countSql); $countStmt = $this->conn->prepare($countSql);
if (!empty($params)) { if (!empty($params)) {
$countStmt->bind_param($paramTypes, ...$params); $countStmt->bind_param($paramTypes, ...$params);
} }
$countStmt->execute(); $countStmt->execute();
$totalResult = $countStmt->get_result(); $totalResult = $countStmt->get_result();
$totalTickets = $totalResult->fetch_assoc()['total']; $totalTickets = $totalResult->fetch_assoc()['total'];
// Get tickets with pagination and creator info // Get tickets with pagination and creator info
$sql = "SELECT t.*, $sql = "SELECT t.*,
u_created.username as creator_username, u_created.username as creator_username,
@@ -181,7 +193,7 @@ class TicketModel {
LEFT JOIN users u_created ON t.created_by = u_created.user_id LEFT JOIN users u_created ON t.created_by = u_created.user_id
LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id LEFT JOIN users u_assigned ON t.assigned_to = u_assigned.user_id
$whereClause $whereClause
ORDER BY $sortColumn $sortDirection ORDER BY $sortExpression $sortDirection
LIMIT ? OFFSET ?"; LIMIT ? OFFSET ?";
$stmt = $this->conn->prepare($sql); $stmt = $this->conn->prepare($sql);