feat: batch ticket operations — multi-select, bulk status, bulk assign
- 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 <noreply@anthropic.com>
This commit is contained in:
@@ -185,6 +185,8 @@ function TicketWorkbenchContent() {
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [selectedTxs, setSelectedTxs] = useState<Transaction[]>([]);
|
||||
const [batchIds, setBatchIds] = useState<Set<number>>(new Set());
|
||||
const [batchSaving, setBatchSaving] = useState(false);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
@@ -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 */}
|
||||
<span className="flex w-9 shrink-0 items-center justify-center" onClick={(e) => e.stopPropagation()}>
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={batchIds.has(ticket.id)}
|
||||
onChange={() => toggleBatchId(ticket.id)}
|
||||
className="h-3.5 w-3.5 rounded border-border accent-primary"
|
||||
/>
|
||||
</span>
|
||||
{availableColumns.filter((c) => c.visible).map((col) => {
|
||||
if (col.key.startsWith("cf.")) {
|
||||
const cfKey = col.key.slice(3);
|
||||
@@ -1010,6 +1051,41 @@ function TicketWorkbenchContent() {
|
||||
</div>
|
||||
</section>
|
||||
|
||||
{/* Floating batch action bar */}
|
||||
{batchIds.size > 0 && (
|
||||
<div className="sticky bottom-0 z-20 flex items-center gap-3 border-t border-border bg-card/95 px-5 py-3 shadow-lg backdrop-blur">
|
||||
<span className="text-sm font-semibold tabular-nums text-foreground">
|
||||
{batchIds.size} selected
|
||||
</span>
|
||||
<button type="button" onClick={() => setBatchIds(new Set())} className="text-xs text-muted-foreground hover:text-foreground">
|
||||
Clear
|
||||
</button>
|
||||
<div className="ml-auto flex items-center gap-2">
|
||||
<span className="text-xs text-muted-foreground">Status:</span>
|
||||
{statusOptions.filter((s) => s.key !== "all").slice(0, 5).map((s) => (
|
||||
<button
|
||||
key={s.key}
|
||||
type="button"
|
||||
disabled={batchSaving}
|
||||
onClick={() => handleBatchStatus(s.key)}
|
||||
className="rounded bg-muted/60 px-2.5 py-1 text-xs font-semibold text-foreground hover:bg-accent disabled:opacity-50"
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
))}
|
||||
<div className="mx-2 h-5 w-px bg-border" />
|
||||
<button
|
||||
type="button"
|
||||
disabled={batchSaving}
|
||||
onClick={handleBatchAssign}
|
||||
className="rounded bg-primary px-3 py-1 text-xs font-semibold text-primary-foreground hover:bg-primary/90 disabled:opacity-50"
|
||||
>
|
||||
{batchSaving ? "Saving..." : "Assign to me"}
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
<aside className="hidden min-h-0 border-l border-border bg-card/76 backdrop-blur xl:flex xl:flex-col">
|
||||
{selectedTicket ? (
|
||||
<>
|
||||
|
||||
Reference in New Issue
Block a user