From e4865583097444a1f40752d41a21b18d8c9cb523 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Gjermund=20H=C3=B8s=C3=B8ien=20Wiggen?= Date: Tue, 9 Jun 2026 21:58:09 +0200 Subject: [PATCH] =?UTF-8?q?feat:=20batch=20ticket=20operations=20=E2=80=94?= =?UTF-8?q?=20multi-select,=20bulk=20status,=20bulk=20assign?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Checkbox column on every ticket row - Select multiple tickets via checkboxes - Floating sticky action bar at bottom when tickets selected: - Shows count: "3 selected" with Clear button - Quick status change buttons - Assign to me button - Checkbox click stops propagation (doesn't select ticket for triage panel) - Batch operations run sequentially via API Co-Authored-By: Claude Opus 4.8 --- web/src/app/page.tsx | 78 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 77 insertions(+), 1 deletion(-) diff --git a/web/src/app/page.tsx b/web/src/app/page.tsx index 1259cd6..4201fa4 100644 --- a/web/src/app/page.tsx +++ b/web/src/app/page.tsx @@ -185,6 +185,8 @@ function TicketWorkbenchContent() { const [error, setError] = useState(null); const [selectedId, setSelectedId] = useState(null); const [selectedTxs, setSelectedTxs] = useState([]); + const [batchIds, setBatchIds] = useState>(new Set()); + const [batchSaving, setBatchSaving] = useState(false); const [searchQuery, setSearchQuery] = useState(""); const [filters, setFilters] = useState([]); @@ -543,6 +545,36 @@ function TicketWorkbenchContent() { setTickets((prev) => prev.map((t) => (t.id === ticketId ? data.ticket : t))); } }; + + const handleBatchStatus = async (newStatus: string) => { + setBatchSaving(true); + for (const id of batchIds) { + await updateTicket(id, { status: newStatus }); + } + setBatchSaving(false); + setBatchIds(new Set()); + await fetchData(); + }; + + const handleBatchAssign = async () => { + const me = users[0]?.id; + if (!me) return; + setBatchSaving(true); + for (const id of batchIds) { + await updateTicket(id, { owner_id: me }); + } + setBatchSaving(false); + setBatchIds(new Set()); + await fetchData(); + }; + + const toggleBatchId = (id: number) => { + setBatchIds((prev) => { + const next = new Set(prev); + if (next.has(id)) next.delete(id); else next.add(id); + return next; + }); + }; const handleColumnResize = (colKey: string, e: React.MouseEvent) => { e.preventDefault(); e.stopPropagation(); @@ -931,8 +963,17 @@ function TicketWorkbenchContent() { ? "bg-accent/80 shadow-[inset_3px_0_0_var(--primary)]" : "hover:bg-accent/45" )} - style={{ width: availableColumns.filter((c) => c.visible).reduce((sum, c) => sum + c.width, 96), minWidth: "100%" }} + style={{ width: availableColumns.filter((c) => c.visible).reduce((sum, c) => sum + c.width, 96) + 36, minWidth: "100%" }} > + {/* Checkbox */} + e.stopPropagation()}> + toggleBatchId(ticket.id)} + className="h-3.5 w-3.5 rounded border-border accent-primary" + /> + {availableColumns.filter((c) => c.visible).map((col) => { if (col.key.startsWith("cf.")) { const cfKey = col.key.slice(3); @@ -1010,6 +1051,41 @@ function TicketWorkbenchContent() { + {/* Floating batch action bar */} + {batchIds.size > 0 && ( +
+ + {batchIds.size} selected + + +
+ Status: + {statusOptions.filter((s) => s.key !== "all").slice(0, 5).map((s) => ( + + ))} +
+ +
+
+ )} +