cleanup: remove side panel and broken inline expansion

Stripped back to clean table before redesign. Removed selectedId tracking,
transactions fetching, and all side-panel related code. Row click now
navigates directly to ticket detail.

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
Gjermund Høsøien Wiggen
2026-06-09 22:38:54 +02:00
parent 1f308b4342
commit 667979c4b2

View File

@@ -1,6 +1,6 @@
"use client";
import { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import React, { Suspense, useCallback, useEffect, useMemo, useRef, useState } from "react";
import { createPortal } from "react-dom";
import { useRouter, useSearchParams } from "next/navigation";
import {
@@ -18,8 +18,8 @@ import {
XIcon,
} from "lucide-react";
import { formatDistanceToNow } from "date-fns";
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 { createTicket, getCustomFields, getLifecycles, getQueueCustomFields, getQueues, getTickets, getUsers, getViews, createView, deleteView, getDashboards, updateTicket } from "@/lib/api";
import type { CustomField, Lifecycle, Queue, QueueCustomField, SavedView, Ticket, User } from "@/lib/types";
import { Button } from "@/components/ui/button";
import {
Dialog,
@@ -183,8 +183,6 @@ function TicketWorkbenchContent() {
const [loading, setLoading] = useState(true);
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 [batchIds, setBatchIds] = useState<Set<number>>(new Set());
const [batchSaving, setBatchSaving] = useState(false);
@@ -524,20 +522,12 @@ function TicketWorkbenchContent() {
});
}, [clock, filters, queues, routeQueue, searchQuery, sortKey, tickets, view]);
const selectedTicket =
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 ?? []));
}
};
@@ -872,8 +862,8 @@ function TicketWorkbenchContent() {
</div>
)}
<div className="grid min-h-0 flex-1 grid-cols-1 xl:grid-cols-[minmax(0,1fr)_372px]">
<section className="min-w-0 overflow-auto bg-card/48">
<div className="min-h-0 flex-1">
<section className="min-w-0 overflow-auto bg-card/48 h-full">
<div className="min-w-[760px]">
{filteredTickets.length === 0 ? (
<div className="flex min-h-80 flex-col items-center justify-center px-5 text-center">
@@ -921,7 +911,7 @@ function TicketWorkbenchContent() {
</div>
{filteredTickets.map((ticket) => {
const selected = ticket.id === selectedId;
const selected = false;
const ownerName = ticket.owner_id
? users.find((u) => u.id === ticket.owner_id)?.username ?? "assigned"
: null;
@@ -931,7 +921,7 @@ function TicketWorkbenchContent() {
key={ticket.id}
role="button"
tabIndex={0}
onClick={() => setSelectedId(ticket.id)}
onClick={() => router.push(`/tickets/${ticket.id}`)}
onDoubleClick={() => router.push(`/tickets/${ticket.id}`)}
onKeyDown={(e) => { if (e.key === "Enter") router.push(`/tickets/${ticket.id}`); }}
className={cn(
@@ -1035,160 +1025,6 @@ 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/82 xl:flex xl:flex-col">
{selectedTicket ? (
<>
{/* Compact header */}
<div className="border-b border-border px-4 py-3">
<div className="flex items-center gap-2.5">
<span
className="h-3 w-3 shrink-0 rounded-full ring-2 ring-offset-1 ring-offset-card"
style={{
backgroundColor: STATUS_META[selectedTicket.status]?.color ?? "#71717a",
boxShadow: `0 0 0 2px color-mix(in srgb, ${STATUS_META[selectedTicket.status]?.color ?? "#71717a"} 20%, transparent)`,
}}
/>
<div className="min-w-0 flex-1">
<p className="text-xs font-semibold leading-tight text-foreground line-clamp-2">
{selectedTicket.subject}
</p>
<p className="mt-0.5 flex items-center gap-1 text-[10px] text-muted-foreground">
<span className="font-mono">{formatTicketId(selectedTicket.id)}</span>
<span>·</span>
<span>{queueName(queues, selectedTicket.queue_id)}</span>
{selectedTicket.owner_id && (
<>
<span>·</span>
<span>{users.find((u) => u.id === selectedTicket.owner_id)?.username}</span>
</>
)}
</p>
</div>
<button
onClick={() => router.push(`/tickets/${selectedTicket.id}`)}
className="shrink-0 rounded p-1 text-muted-foreground/50 hover:bg-accent hover:text-foreground"
title="Open full view"
>
<ChevronRightIcon className="h-4 w-4" />
</button>
</div>
{/* Status dots — visual, clickable */}
<div className="mt-3 flex items-center gap-1.5">
{["new", "open", "in_progress", "resolved", "closed"].map((s) => {
const isCurrent = selectedTicket.status === s;
const color = STATUS_META[s]?.color ?? "#71717a";
return (
<button
key={s}
type="button"
disabled={isCurrent}
onClick={() => handleQuickStatus(selectedTicket.id, s)}
title={statusLabel(s)}
className={cn(
"h-3.5 w-3.5 rounded-full transition-all",
isCurrent
? "ring-2 ring-offset-1 ring-offset-card scale-125"
: "opacity-40 hover:opacity-80 hover:scale-110",
)}
style={{ backgroundColor: color }}
/>
);
})}
{!selectedTicket.owner_id && (
<button
onClick={() => handleQuickAssign(selectedTicket.id)}
className="ml-auto rounded border border-border px-2 py-0.5 text-[10px] font-medium text-muted-foreground hover:bg-accent hover:text-foreground transition-colors"
>
Take it
</button>
)}
</div>
</div>
{/* Activity feed — compact timeline */}
<div className="flex-1 overflow-auto">
{selectedTxs.length === 0 ? (
<div className="p-4 text-center text-xs text-muted-foreground">No activity</div>
) : (
<div className="py-1">
{selectedTxs.slice(0, 6).map((tx) => (
<div key={tx.id} className="flex gap-3 px-4 py-2">
<div className="relative flex flex-col items-center pt-0.5">
<div className="h-1.5 w-1.5 rounded-full bg-border" />
<div className="mt-1 flex-1 w-px bg-border/30" />
</div>
<div className="min-w-0 flex-1 pb-1">
<p className="text-xs text-foreground/90">
{tx.transaction_type === "Create" ? "Created" :
tx.transaction_type === "StatusChange" ? `Status → ${tx.new_value ? statusLabel(tx.new_value) : "?"}` :
tx.transaction_type === "SetOwner" ? `Owner → ${tx.new_value ? users.find((u) => u.id === tx.new_value)?.username ?? "assigned" : "unassigned"}` :
tx.transaction_type === "Correspond" ? "Reply" :
tx.transaction_type === "Comment" ? "Internal note" :
tx.transaction_type === "SetTeam" ? `Team changed` :
tx.transaction_type === "CustomFieldChange" ? `${tx.field}${tx.new_value}` :
tx.transaction_type}
</p>
{tx.data && (tx.data as any).body && (
<p className="mt-0.5 line-clamp-2 text-[11px] leading-relaxed text-muted-foreground">
{(tx.data as any).body}
</p>
)}
<p className="mt-0.5 text-[10px] text-muted-foreground/50">
{formatDistanceToNow(new Date(tx.created_at), { addSuffix: true })}
</p>
</div>
</div>
))}
</div>
)}
</div>
</>
) : (
<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/30">
<ChevronRightIcon className="h-5 w-5 text-muted-foreground/30" />
</div>
<p className="text-xs text-muted-foreground">Select a ticket to triage</p>
</div>
)}
</aside>
</div>
<Dialog open={dialogOpen} onOpenChange={setDialogOpen}>