feat: add dashboards — tables, CRUD API, widget data endpoint
- New dashboards table (name, description, layout, is_default) - New dashboard_widgets table (view_id, title, widget_type, position, config) - GET/POST/PATCH/DELETE /dashboards - GET/POST/PATCH/DELETE /dashboards/:id/widgets - GET /dashboards/:id/widgets/:id/data — runs saved view filters, returns pre-aggregated data for count/ticket_list/status_chart/grouped_counts - is_default uniqueness enforced on PATCH Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
59
web/src/components/widgets/ticket-list-widget.tsx
Normal file
59
web/src/components/widgets/ticket-list-widget.tsx
Normal file
@@ -0,0 +1,59 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import { formatDistanceToNow } from "date-fns";
|
||||
import { CircleIcon } from "lucide-react";
|
||||
import type { WidgetData } from "@/lib/types";
|
||||
import { cn, formatTicketId } from "@/lib/utils";
|
||||
|
||||
const STATUS_COLORS: Record<string, string> = {
|
||||
new: "#64748b",
|
||||
open: "#2563eb",
|
||||
in_progress: "#d97706",
|
||||
resolved: "#16a34a",
|
||||
closed: "#71717a",
|
||||
};
|
||||
|
||||
function statusLabel(status: string) {
|
||||
return status.replaceAll("_", " ");
|
||||
}
|
||||
|
||||
export function TicketListWidget({ data }: { data: WidgetData }) {
|
||||
const tickets = data.tickets ?? [];
|
||||
|
||||
return (
|
||||
<div className="flex h-full flex-col overflow-hidden rounded-lg border border-border bg-card">
|
||||
<div className="flex items-center justify-between border-b border-border px-3 py-2">
|
||||
<span className="text-xs font-semibold text-foreground">{data.title}</span>
|
||||
<span className="text-[11px] tabular-nums text-muted-foreground">{data.total}</span>
|
||||
</div>
|
||||
<div className="flex-1 overflow-auto">
|
||||
{tickets.length === 0 ? (
|
||||
<p className="px-3 py-4 text-center text-xs text-muted-foreground">No tickets</p>
|
||||
) : (
|
||||
tickets.map((ticket) => (
|
||||
<Link
|
||||
key={ticket.id}
|
||||
href={`/tickets/${ticket.id}`}
|
||||
className="flex items-center gap-2 border-b border-border/50 px-3 py-2 text-xs transition-colors hover:bg-accent/40 last:border-b-0"
|
||||
>
|
||||
<CircleIcon
|
||||
className="h-2 w-2 shrink-0"
|
||||
style={{ color: STATUS_COLORS[ticket.status] ?? "#71717a", fill: STATUS_COLORS[ticket.status] ?? "#71717a" }}
|
||||
/>
|
||||
<span className="min-w-0 flex-1 truncate font-medium text-foreground">
|
||||
{ticket.subject}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground">
|
||||
{ticket.owner_name ?? "unassigned"}
|
||||
</span>
|
||||
<span className="shrink-0 text-[11px] text-muted-foreground/60">
|
||||
{formatDistanceToNow(new Date(ticket.updated_at), { addSuffix: true })}
|
||||
</span>
|
||||
</Link>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
Reference in New Issue
Block a user