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:
Gjermund Høsøien Wiggen
2026-06-09 11:26:22 +02:00
parent aa90b88991
commit b70a133ea2
15 changed files with 2349 additions and 90 deletions

View File

@@ -13,8 +13,8 @@ import {
PanelLeftIcon,
CommandIcon,
} from "lucide-react";
import { getTickets, getQueues, getViews } from "@/lib/api";
import type { Queue, SavedView } from "@/lib/types";
import { getTickets, getQueues, getViews, getDashboards } from "@/lib/api";
import type { Dashboard, Queue, SavedView } from "@/lib/types";
import { CommandPalette } from "@/components/command-palette";
import { ThemeToggle } from "@/components/theme-toggle";
import { cn } from "@/lib/utils";
@@ -87,6 +87,7 @@ function SidebarNav() {
});
const [queues, setQueues] = useState<(Queue & { count: number })[]>([]);
const [savedViews, setSavedViews] = useState<SavedView[]>([]);
const [dashboards, setDashboards] = useState<Dashboard[]>([]);
useEffect(() => {
getTickets().then(({ data }) => {
@@ -120,6 +121,10 @@ function SidebarNav() {
getViews().then(({ data }) => {
if (data) setSavedViews(data);
});
getDashboards().then(({ data }) => {
if (data) setDashboards(data);
});
}, []);
const collapsed = useSidebarCollapsed();
@@ -204,6 +209,29 @@ function SidebarNav() {
</div>
)}
{dashboards.length > 0 && (
<div className="mt-4">
{!collapsed && (
<div className="px-2 py-1.5 text-[11px] font-semibold text-sidebar-foreground/45 uppercase">
Dashboards
</div>
)}
{dashboards.map((dash) => {
const active =
pathname.startsWith("/dashboards/") && pathname.endsWith(dash.id);
return (
<SidebarNavItem
key={dash.id}
href={`/dashboards/${dash.id}`}
icon={LayoutGridIcon}
label={dash.name}
active={active}
/>
);
})}
</div>
)}
{savedViews.length > 0 && (
<div className="mt-4">
{!collapsed && (