fix: All tickets link, redesign side panel into triage panel
- Fix sidebar All tickets link: /?view=all instead of / (avoids dashboard redirect)
- Replace useless side panel with triage command center:
- Quick status change buttons (click to transition inline)
- Assign to me button (appears when unassigned)
- Mini activity feed showing last 8 transactions with type labels,
status badges, old→new values, and comment previews
- Relative timestamps
- Open full view button
- Fetches ticket transactions on selection
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -17,8 +17,8 @@ import {
|
||||
XIcon,
|
||||
} from "lucide-react";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { createTicket, getCustomFields, getLifecycles, getQueueCustomFields, getQueues, getTickets, getUsers, getViews, createView, deleteView, getDashboards } from "@/lib/api";
|
||||
import type { CustomField, Lifecycle, Queue, QueueCustomField, SavedView, Ticket, User } from "@/lib/types";
|
||||
import { createTicket, getCustomFields, getLifecycles, getQueueCustomFields, getQueues, getTickets, getUsers, getViews, createView, deleteView, getDashboards, getTicketTransactions, updateTicket } from "@/lib/api";
|
||||
import type { CustomField, Lifecycle, Queue, QueueCustomField, SavedView, Ticket, Transaction, User } from "@/lib/types";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import {
|
||||
Dialog,
|
||||
@@ -148,6 +148,7 @@ function TicketWorkbenchContent() {
|
||||
const [refreshing, setRefreshing] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [selectedId, setSelectedId] = useState<number | null>(null);
|
||||
const [selectedTxs, setSelectedTxs] = useState<Transaction[]>([]);
|
||||
|
||||
const [searchQuery, setSearchQuery] = useState("");
|
||||
const [filters, setFilters] = useState<Filter[]>([]);
|
||||
@@ -442,7 +443,28 @@ function TicketWorkbenchContent() {
|
||||
}, [clock, filters, queues, routeQueue, searchQuery, sortKey, tickets, view]);
|
||||
|
||||
const selectedTicket =
|
||||
filteredTickets.find((ticket) => ticket.id === selectedId) ?? filteredTickets[0] ?? null;
|
||||
filteredTickets.find((ticket) => ticket.id === selectedId) ?? null;
|
||||
|
||||
// Fetch transactions when selection changes
|
||||
useEffect(() => {
|
||||
if (!selectedTicket) { setSelectedTxs([]); return; }
|
||||
getTicketTransactions(selectedTicket.id).then(({ data }) => setSelectedTxs(data ?? []));
|
||||
}, [selectedTicket?.id]);
|
||||
|
||||
const handleQuickStatus = async (ticketId: number, newStatus: string) => {
|
||||
const { data } = await updateTicket(ticketId, { status: newStatus });
|
||||
if (data) {
|
||||
setTickets((prev) => prev.map((t) => (t.id === ticketId ? data.ticket : t)));
|
||||
getTicketTransactions(ticketId).then(({ data: txs }) => setSelectedTxs(txs ?? []));
|
||||
}
|
||||
};
|
||||
|
||||
const handleQuickAssign = async (ticketId: number) => {
|
||||
const { data } = await updateTicket(ticketId, { owner_id: users[0]?.id ?? null });
|
||||
if (data) {
|
||||
setTickets((prev) => prev.map((t) => (t.id === ticketId ? data.ticket : t)));
|
||||
}
|
||||
};
|
||||
const visibleTitle = routeQueue
|
||||
? queueName(queues, routeQueue)
|
||||
: VIEW_LABELS[view] ?? "All tickets";
|
||||
@@ -776,66 +798,123 @@ function TicketWorkbenchContent() {
|
||||
<aside className="hidden min-h-0 border-l border-border bg-card/76 backdrop-blur xl:flex xl:flex-col">
|
||||
{selectedTicket ? (
|
||||
<>
|
||||
<div className="border-b border-border bg-card/82 p-5">
|
||||
<div className="flex items-start justify-between gap-3">
|
||||
<div className="border-b border-border bg-card/82 p-4">
|
||||
<div className="flex items-start justify-between gap-2">
|
||||
<div className="min-w-0">
|
||||
<p className="font-mono text-xs font-semibold text-muted-foreground">
|
||||
<p className="font-mono text-[11px] font-semibold text-muted-foreground">
|
||||
{formatTicketId(selectedTicket.id)}
|
||||
</p>
|
||||
<h2 className="mt-2 text-lg font-semibold leading-snug text-foreground">
|
||||
<h2 className="mt-1 text-sm font-semibold leading-snug text-foreground line-clamp-2">
|
||||
{selectedTicket.subject}
|
||||
</h2>
|
||||
<p className="mt-1 text-xs text-muted-foreground">
|
||||
{queueName(queues, selectedTicket.queue_id)} · {selectedTicket.owner_id ? users.find((u) => u.id === selectedTicket.owner_id)?.username ?? "assigned" : "unassigned"}
|
||||
</p>
|
||||
</div>
|
||||
<TicketStatusBadge status={selectedTicket.status} />
|
||||
</div>
|
||||
<Button
|
||||
onClick={() => router.push(`/tickets/${selectedTicket.id}`)}
|
||||
className="mt-4 h-8 w-full bg-primary"
|
||||
size="sm"
|
||||
>
|
||||
Open ticket
|
||||
<ChevronRightIcon className="h-4 w-4" />
|
||||
</Button>
|
||||
|
||||
<div className="mt-3 flex flex-wrap gap-1">
|
||||
{statusOptions.filter((s) => s.key !== "all").slice(0, 4).map((s) => {
|
||||
const allowed = selectedTicket.status !== s.key;
|
||||
return (
|
||||
<button
|
||||
key={s.key}
|
||||
type="button"
|
||||
disabled={!allowed}
|
||||
onClick={() => handleQuickStatus(selectedTicket.id, s.key)}
|
||||
className={cn(
|
||||
"rounded px-2 py-1 text-[11px] font-semibold transition-colors",
|
||||
selectedTicket.status === s.key
|
||||
? "bg-primary/10 text-primary"
|
||||
: allowed
|
||||
? "bg-muted/60 text-muted-foreground hover:bg-accent hover:text-foreground"
|
||||
: "cursor-default text-muted-foreground/40"
|
||||
)}
|
||||
>
|
||||
{s.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
<div className="mt-2 flex gap-2">
|
||||
<Button
|
||||
onClick={() => router.push(`/tickets/${selectedTicket.id}`)}
|
||||
className="h-7 flex-1 bg-primary text-xs"
|
||||
size="sm"
|
||||
>
|
||||
Open full view
|
||||
<ChevronRightIcon className="ml-1 h-3.5 w-3.5" />
|
||||
</Button>
|
||||
{!selectedTicket.owner_id && (
|
||||
<Button
|
||||
variant="outline"
|
||||
size="sm"
|
||||
onClick={() => handleQuickAssign(selectedTicket.id)}
|
||||
className="h-7 text-xs"
|
||||
>
|
||||
Assign to me
|
||||
</Button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="flex-1 overflow-auto p-5">
|
||||
<dl className="grid overflow-hidden rounded-md border border-border bg-background/55 text-sm">
|
||||
<div className="grid grid-cols-[104px_minmax(0,1fr)] border-b border-border px-3 py-2.5">
|
||||
<dt className="text-xs font-semibold uppercase text-muted-foreground">Queue</dt>
|
||||
<dd className="truncate text-foreground">{queueName(queues, selectedTicket.queue_id)}</dd>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{selectedTxs.length === 0 ? (
|
||||
<div className="p-4 text-center text-xs text-muted-foreground">
|
||||
No activity yet.
|
||||
</div>
|
||||
<div className="grid grid-cols-[104px_minmax(0,1fr)] border-b border-border px-3 py-2.5">
|
||||
<dt className="text-xs font-semibold uppercase text-muted-foreground">Owner</dt>
|
||||
<dd className="truncate text-foreground">{selectedTicket.owner_id ?? "Unassigned"}</dd>
|
||||
) : (
|
||||
<div className="divide-y divide-border/50">
|
||||
{selectedTxs.slice(0, 8).map((tx) => (
|
||||
<div key={tx.id} className="px-4 py-2.5">
|
||||
<div className="flex items-center gap-2">
|
||||
<span className="text-[11px] font-semibold text-foreground">
|
||||
{tx.transaction_type === "Create" ? "Created" :
|
||||
tx.transaction_type === "StatusChange" ? "Status changed" :
|
||||
tx.transaction_type === "SetOwner" ? "Owner changed" :
|
||||
tx.transaction_type === "Comment" ? "Comment" :
|
||||
tx.transaction_type === "Correspond" ? "Reply" :
|
||||
tx.transaction_type === "SetTeam" ? "Team changed" :
|
||||
tx.transaction_type === "CustomFieldChange" ? `${tx.field} set` :
|
||||
tx.transaction_type}
|
||||
</span>
|
||||
{tx.transaction_type === "StatusChange" && tx.new_value && (
|
||||
<span className={cn(
|
||||
"inline-flex items-center rounded px-1.5 py-0.5 text-[10px] font-semibold",
|
||||
STATUS_META[tx.new_value]?.tone ?? "bg-muted text-muted-foreground"
|
||||
)}>
|
||||
{statusLabel(tx.new_value)}
|
||||
</span>
|
||||
)}
|
||||
{tx.old_value && tx.new_value && tx.transaction_type !== "StatusChange" && (
|
||||
<span className="text-[10px] text-muted-foreground">
|
||||
{tx.old_value} → {tx.new_value}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
{tx.data && (tx.data as any).body && (
|
||||
<p className="mt-1 line-clamp-2 text-xs leading-relaxed text-muted-foreground">
|
||||
{(tx.data as any).body}
|
||||
</p>
|
||||
)}
|
||||
<p className="mt-0.5 text-[10px] text-muted-foreground/60">
|
||||
{formatDistanceToNow(new Date(tx.created_at), { addSuffix: true })}
|
||||
</p>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
<div className="grid grid-cols-[104px_minmax(0,1fr)] border-b border-border px-3 py-2.5">
|
||||
<dt className="text-xs font-semibold uppercase text-muted-foreground">Creator</dt>
|
||||
<dd className="truncate text-foreground">{selectedTicket.creator_id}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[104px_minmax(0,1fr)] border-b border-border px-3 py-2.5">
|
||||
<dt className="text-xs font-semibold uppercase text-muted-foreground">Created</dt>
|
||||
<dd className="truncate text-foreground">{new Date(selectedTicket.created_at).toLocaleString()}</dd>
|
||||
</div>
|
||||
<div className="grid grid-cols-[104px_minmax(0,1fr)] px-3 py-2.5">
|
||||
<dt className="text-xs font-semibold uppercase text-muted-foreground">Updated</dt>
|
||||
<dd className="truncate text-foreground">{new Date(selectedTicket.updated_at).toLocaleString()}</dd>
|
||||
</div>
|
||||
</dl>
|
||||
|
||||
<div className="mt-5 rounded-md border border-border bg-accent/42 p-4">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-foreground">
|
||||
<CheckCircle2Icon className="h-4 w-4 text-emerald-600" />
|
||||
Next action
|
||||
</div>
|
||||
<p className="mt-2 text-sm text-muted-foreground">
|
||||
Open the ticket to reply, change status, and review its transaction history.
|
||||
</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</>
|
||||
) : (
|
||||
<div className="flex flex-1 items-center justify-center p-6 text-center text-sm text-muted-foreground">
|
||||
Select a ticket to inspect it.
|
||||
<div className="flex flex-1 flex-col items-center justify-center gap-2 p-6 text-center">
|
||||
<div className="flex h-10 w-10 items-center justify-center rounded-full bg-muted/50">
|
||||
<ChevronRightIcon className="h-5 w-5 text-muted-foreground/40" />
|
||||
</div>
|
||||
<p className="text-sm text-muted-foreground">Select a ticket</p>
|
||||
<p className="text-xs text-muted-foreground/60">Quick actions and activity will appear here</p>
|
||||
</div>
|
||||
)}
|
||||
</aside>
|
||||
|
||||
Reference in New Issue
Block a user