"use client"; import { useState, useEffect, use, useCallback, useRef } from "react"; import { useRouter } from "next/navigation"; import Link from "next/link"; import { GripIcon, PencilIcon, PlusIcon, Trash2Icon, RefreshCwIcon, LayoutGridIcon, } from "lucide-react"; import { getDashboard, createWidget, deleteWidget, updateWidget, getWidgetData, getViews, getTeams, updateDashboard, } from "@/lib/api"; import type { Dashboard, DashboardWidget, SavedView, Team, WidgetData, } from "@/lib/types"; import { Button } from "@/components/ui/button"; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from "@/components/ui/dialog"; import { CountWidget } from "@/components/widgets/count-widget"; import { TicketListWidget } from "@/components/widgets/ticket-list-widget"; import { StatusChartWidget } from "@/components/widgets/status-chart-widget"; import { GroupedCountsWidget } from "@/components/widgets/grouped-counts-widget"; import { cn } from "@/lib/utils"; function widgetGridStyle(position: { x: number; y: number; w: number; h: number }) { return { gridColumn: `${position.x + 1} / span ${position.w}`, gridRow: `${position.y + 1} / span ${position.h}`, }; } export default function DashboardPage({ params }: { params: Promise<{ id: string }> }) { const { id } = use(params); const router = useRouter(); const [dashboard, setDashboard] = useState(null); const [widgets, setWidgets] = useState<(DashboardWidget & { data?: WidgetData })[]>([]); const [views, setViews] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [autoRefresh, setAutoRefresh] = useState(false); const [editMode, setEditMode] = useState(false); const [teams, setTeams] = useState([]); // Add widget dialog const [addOpen, setAddOpen] = useState(false); const [addViewId, setAddViewId] = useState(""); const [addTitle, setAddTitle] = useState(""); const [addType, setAddType] = useState("count"); const [addGroupBy, setAddGroupBy] = useState("owner"); const [adding, setAdding] = useState(false); const fetchDashboard = useCallback(async () => { const { data, error } = await getDashboard(id); if (error || !data) { setError(error ?? "Dashboard not found"); setLoading(false); return; } setDashboard(data); const widgetList = data.widgets ?? []; setWidgets(widgetList); // Fetch data for each widget for (const widget of widgetList) { const { data: wData } = await getWidgetData(id, widget.id); if (wData) { setWidgets((prev) => prev.map((w) => (w.id === widget.id ? { ...w, data: wData } : w)) ); } } setLoading(false); }, [id]); useEffect(() => { fetchDashboard(); getViews().then(({ data }) => { if (data) setViews(data); }); getTeams().then(({ data }) => { if (data) setTeams(data); }); }, [fetchDashboard]); // Auto-refresh: only refresh widget data, not structure useEffect(() => { if (!autoRefresh || !dashboard) return; const interval = setInterval(() => { for (const widget of widgets) { getWidgetData(dashboard.id, widget.id).then(({ data: wData }) => { if (wData) { setWidgets((prev) => prev.map((w) => (w.id === widget.id ? { ...w, data: wData } : w)) ); } }); } }, 30_000); return () => clearInterval(interval); }, [autoRefresh, dashboard?.id]); const handleAddWidget = async () => { if (!addViewId || !addTitle.trim()) return; setAdding(true); // Smart positioning: fill a 3-column grid (4 units each in 12-col grid) const COLS = 3; const W = 4; const H = 2; const occupied = new Set(widgets.map((w) => `${w.position.x},${w.position.y}`)); let x = 0; let y = 0; while (occupied.has(`${x},${y}`)) { x += W; if (x >= COLS * W) { x = 0; y += H; } } const pos = { x, y, w: W, h: H }; const config = addType === "grouped_counts" ? { group_by: addGroupBy } : {}; const { data, error } = await createWidget(id, { view_id: addViewId, title: addTitle.trim(), widget_type: addType, position: pos, config, }); if (!error && data) { setWidgets((prev) => [...prev, data]); const { data: wData } = await getWidgetData(id, data.id); if (wData) { setWidgets((prev) => prev.map((w) => (w.id === data.id ? { ...w, data: wData } : w))); } setAddOpen(false); setAddViewId(""); setAddTitle(""); setAddType("count"); } setAdding(false); }; const handleDeleteWidget = async (widgetId: string) => { await deleteWidget(id, widgetId); setWidgets((prev) => prev.filter((w) => w.id !== widgetId)); }; const gridRef = useRef(null); const [resizingId, setResizingId] = useState(null); const [draggingId, setDraggingId] = useState(null); // Resize: track mousedown → mousemove → mouseup const handleResizeStart = (e: React.MouseEvent, widgetId: string) => { e.preventDefault(); e.stopPropagation(); setResizingId(widgetId); const startX = e.clientX; const startY = e.clientY; const widget = widgets.find((w) => w.id === widgetId); if (!widget || !gridRef.current) return; const startW = widget.position.w; const startH = widget.position.h; const gridWidth = gridRef.current.offsetWidth; const unitSize = gridWidth / 12; const onMove = (ev: MouseEvent) => { const dx = Math.round((ev.clientX - startX) / unitSize); const dy = Math.round((ev.clientY - startY) / unitSize); const newW = Math.max(1, Math.min(12, startW + dx)); const newH = Math.max(1, startH + dy); setWidgets((prev) => prev.map((w) => w.id === widgetId ? { ...w, position: { ...w.position, w: newW, h: newH } } : w ) ); }; const onUp = async () => { document.removeEventListener("mousemove", onMove); document.removeEventListener("mouseup", onUp); setResizingId(null); const updated = widgets.find((w) => w.id === widgetId); if (updated) { await updateWidget(id, widgetId, { position: updated.position }); } }; document.addEventListener("mousemove", onMove); document.addEventListener("mouseup", onUp); }; // Drag: HTML5 DnD const handleDragStart = (e: React.DragEvent, widgetId: string) => { setDraggingId(widgetId); e.dataTransfer.setData("text/plain", widgetId); e.dataTransfer.effectAllowed = "move"; }; const handleDragOver = (e: React.DragEvent) => { e.preventDefault(); e.dataTransfer.dropEffect = "move"; }; const handleDrop = async (e: React.DragEvent, targetId: string) => { e.preventDefault(); const sourceId = e.dataTransfer.getData("text/plain"); if (!sourceId || sourceId === targetId) { setDraggingId(null); return; } const source = widgets.find((w) => w.id === sourceId); const target = widgets.find((w) => w.id === targetId); if (!source || !target) { setDraggingId(null); return; } // Swap positions const sourcePos = { ...source.position }; const targetPos = { ...target.position }; setWidgets((prev) => prev.map((w) => { if (w.id === sourceId) return { ...w, position: targetPos }; if (w.id === targetId) return { ...w, position: sourcePos }; return w; }) ); await updateWidget(id, sourceId, { position: targetPos }); await updateWidget(id, targetId, { position: sourcePos }); setDraggingId(null); }; const handleDragEnd = () => { setDraggingId(null); }; const renderWidget = (widget: DashboardWidget & { data?: WidgetData }) => { if (!widget.data) { return (
); } switch (widget.data.type) { case "count": return ; case "ticket_list": return ; case "status_chart": return ; case "grouped_counts": return ; default: return (

Unknown type: {widget.data.type}

); } }; if (loading) { return (
); } if (error || !dashboard) { return (

{error ?? "Dashboard not found"}

Go to ticket list
); } return (
Dashboard

{dashboard.name}

{dashboard.description && (

{dashboard.description}

)}
{editMode && ( )}
{widgets.length === 0 ? (

No widgets yet

{editMode ? ( ) : ( )}
) : (
{widgets.map((widget) => (
editMode && handleDragStart(e, widget.id)} onDragOver={handleDragOver} onDrop={(e) => handleDrop(e, widget.id)} onDragEnd={handleDragEnd} > {/* Drag handle */} {editMode && (
)} {renderWidget(widget)} {editMode && ( <> {/* Resize handle */}
handleResizeStart(e, widget.id)} >
)}
))}
)}
Add widget Choose a saved view and widget type to add to this dashboard.
setAddTitle(e.target.value)} placeholder="e.g. Open tickets" className="mt-1 h-9 w-full rounded-md border border-input bg-card px-3 text-sm outline-none focus:border-ring" />
{addType === "grouped_counts" && (
)}
); }