From 10962f795f214e9a37c4d9b55d63d36d640a5df4 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gjermund=20H=C3=B8s=C3=B8ien=20Wiggen?= Date: Sun, 7 Jun 2026 22:58:50 +0200 Subject: [PATCH] feat: three-column ticket list layout (list + detail as peers, no Sheet) - Replace Sheet slide-over with persistent right-column detail panel - Ticket list shrinks to w-80 when ticket selected, detail takes flex-1 - Animated transition (300ms ease-out) when selecting/deselecting - Kept existing conversation thread, properties sidebar, reply box inline --- web/src/app/page.tsx | 624 ++++++++++++++++++++++--------------------- 1 file changed, 323 insertions(+), 301 deletions(-) diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 6af13db..7df5885 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -16,12 +16,6 @@ import { DialogDescription, DialogFooter, } from "@/components/ui/dialog"; -import { - Sheet, - SheetContent, - SheetHeader, - SheetTitle, -} from "@/components/ui/sheet"; import { Textarea } from "@/components/ui/textarea"; import { cn } from "@/lib/utils"; @@ -66,7 +60,7 @@ function getInitialColor(name: string): string { return colors[Math.abs(hash) % colors.length]; } -function TicketRow({ ticket, onClick }: { ticket: Ticket; onClick: () => void }) { +function TicketRow({ ticket, selected, onClick }: { ticket: Ticket; selected: boolean; onClick: () => void }) { const statusColor = STATUS_COLORS[ticket.status] || STATUS_COLORS.new; const shortId = ticket.id.slice(0, 8); const timeAgo = formatDistanceToNow(new Date(ticket.updated_at), { addSuffix: true }); @@ -74,7 +68,12 @@ function TicketRow({ ticket, onClick }: { ticket: Ticket; onClick: () => void }) return ( + + + {loading && ( +
+ {Array.from({ length: 6 }).map((_, i) => ( +
+
+
+
+
- ))} +
+ ))} +
+ )} + + {error && ( +
+

{error}

+
+ )} + + {!loading && !error && ticket && ( + <> + {/* Title */} +
+

+ {ticket.subject} +

+

+ {ticket.id.slice(0, 8)} +

- )} - {error && ( -
-

{error}

-
- )} + {/* Conversation */} +
+ {transactions.length === 0 && ( +
+

+ No activity yet +

+
+ )} + {transactions.map((tx) => { + const isSystem = + tx.transaction_type === "status_change" || + tx.transaction_type === "assignment" || + tx.transaction_type === "create"; + const isAgent = + tx.transaction_type === "agent_reply" || + tx.transaction_type === "internal_note"; - {!loading && !error && ticket && ( -
- {/* Header */} - - - {ticket.subject} - -

- {ticket.id.slice(0, 8)} -

-
- - {/* Conversation */} -
- {transactions.length === 0 && ( -
-

- No activity yet -

-
- )} - {transactions.map((tx) => { - const isSystem = - tx.transaction_type === "status_change" || - tx.transaction_type === "assignment" || - tx.transaction_type === "create"; - const isAgent = - tx.transaction_type === "agent_reply" || - tx.transaction_type === "internal_note"; - - if (isSystem) { - const txTimeAgo = formatDistanceToNow( - new Date(tx.created_at), - { addSuffix: true } - ); - let message = ""; - if (tx.transaction_type === "create") { - message = "Ticket created"; - } else if (tx.transaction_type === "status_change") { - const oldLabel = tx.old_value - ? STATUS_LABELS[tx.old_value] || tx.old_value - : "?"; - const newLabel = tx.new_value - ? STATUS_LABELS[tx.new_value] || tx.new_value - : "?"; - message = `${getInitial(tx.creator_id)} changed status from ${oldLabel} to ${newLabel}`; - } else if (tx.transaction_type === "assignment") { - message = tx.new_value - ? `${getInitial(tx.creator_id)} assigned to ${tx.new_value}` - : `${getInitial(tx.creator_id)} unassigned`; - } else { - message = tx.transaction_type; - } - return ( -
- - {message} · {txTimeAgo} - -
- ); - } - - const isInternal = - tx.transaction_type === "internal_note"; + if (isSystem) { const txTimeAgo = formatDistanceToNow( new Date(tx.created_at), { addSuffix: true } ); - + let message = ""; + if (tx.transaction_type === "create") { + message = "Ticket created"; + } else if (tx.transaction_type === "status_change") { + const oldLabel = tx.old_value + ? STATUS_LABELS[tx.old_value] || tx.old_value + : "?"; + const newLabel = tx.new_value + ? STATUS_LABELS[tx.new_value] || tx.new_value + : "?"; + message = `${getInitial(tx.creator_id)} changed status from ${oldLabel} to ${newLabel}`; + } else if (tx.transaction_type === "assignment") { + message = tx.new_value + ? `${getInitial(tx.creator_id)} assigned to ${tx.new_value}` + : `${getInitial(tx.creator_id)} unassigned`; + } else { + message = tx.transaction_type; + } return (
-
- - {getInitial(tx.creator_id)} - -
-
- {isInternal && ( -
- Internal note -
- )} -

- {typeof tx.data === "object" && - tx.data !== null && - "body" in (tx.data as Record) - ? String( - (tx.data as Record).body - ) - : tx.transaction_type} -

-

- {txTimeAgo} -

-
+ + {message} · {txTimeAgo} +
); - })} -
+ } - {/* Reply box */} -
-
-