diff --git a/convex/dashboards.ts b/convex/dashboards.ts index 5259c3b..43d4481 100644 --- a/convex/dashboards.ts +++ b/convex/dashboards.ts @@ -57,6 +57,28 @@ function generateWidgetKey(dashboardId: Id<"dashboards">) { return `${dashboardId.toString().slice(-6)}-${rand}` } +function normalizeQueueSummaryConfig(config: unknown) { + return { + type: "queue-summary", + title: "Resumo das filas", + dataSource: { metricKey: "queues.summary_cards" }, + ...(typeof config === "object" && config ? (config as Record) : {}), + } +} + +function queueSummaryLayout(widgetKey: string) { + return { + i: widgetKey, + x: 0, + y: 0, + w: 12, + h: 6, + minW: 8, + minH: 4, + static: false, + } +} + function normalizeWidgetConfig(type: WidgetType, config: unknown) { if (config && typeof config === "object") { return config @@ -580,6 +602,146 @@ export const updateWidget = mutation({ }, }) +export const ensureQueueSummaryWidget = mutation({ + args: { + tenantId: v.string(), + actorId: v.id("users"), + dashboardId: v.id("dashboards"), + }, + handler: async (ctx, { tenantId, actorId, dashboardId }) => { + await requireStaff(ctx, actorId, tenantId) + const dashboard = await ctx.db.get(dashboardId) + if (!dashboard || dashboard.tenantId !== tenantId) { + throw new ConvexError("Dashboard não encontrado") + } + + const widgets = await ctx.db + .query("dashboardWidgets") + .withIndex("by_dashboard_order", (q) => q.eq("dashboardId", dashboardId)) + .collect() + + widgets.sort((a, b) => a.order - b.order || a.createdAt - b.createdAt) + + const now = Date.now() + let queueWidget = widgets.find((widget) => widget.type === "queue-summary") + let changed = false + + if (!queueWidget) { + // Shift existing widgets to make room at the top. + await Promise.all( + widgets.map((widget) => + ctx.db.patch(widget._id, { + order: widget.order + 1, + updatedAt: now, + updatedBy: actorId, + }), + ), + ) + const widgetKey = generateWidgetKey(dashboardId) + const config = normalizeQueueSummaryConfig(undefined) + const layout = queueSummaryLayout(widgetKey) + const widgetId = await ctx.db.insert("dashboardWidgets", { + tenantId, + dashboardId, + widgetKey, + title: config.title, + type: "queue-summary", + config, + layout, + order: 0, + createdBy: actorId, + updatedBy: actorId, + createdAt: now, + updatedAt: now, + isHidden: false, + }) + const createdWidget = await ctx.db.get(widgetId) + if (!createdWidget) { + throw new ConvexError("Falha ao criar widget de resumo por fila.") + } + queueWidget = createdWidget + widgets.unshift(queueWidget) + changed = true + } else { + // Ensure the existing widget is first and has the expected config. + const desiredConfig = normalizeQueueSummaryConfig(queueWidget.config) + if (JSON.stringify(queueWidget.config) !== JSON.stringify(desiredConfig)) { + await ctx.db.patch(queueWidget._id, { config: desiredConfig, updatedAt: now, updatedBy: actorId }) + queueWidget = { ...queueWidget, config: desiredConfig } + changed = true + } + if (queueWidget.order !== 0) { + let nextOrder = 1 + for (const widget of widgets) { + if (widget._id === queueWidget._id) continue + if (widget.order !== nextOrder) { + await ctx.db.patch(widget._id, { order: nextOrder, updatedAt: now, updatedBy: actorId }) + } + nextOrder += 1 + } + await ctx.db.patch(queueWidget._id, { order: 0, updatedAt: now, updatedBy: actorId }) + changed = true + } + } + + if (!queueWidget) { + throw new ConvexError("Não foi possível garantir o widget de resumo por fila.") + } + + const widgetKey = queueWidget.widgetKey + + // Normalize dashboard layout (queue summary first). + const currentLayout = Array.isArray(dashboard.layout) ? dashboard.layout : [] + const filteredLayout = currentLayout.filter((item) => item.i !== widgetKey) + const normalizedLayout = [queueSummaryLayout(widgetKey), ...filteredLayout] + + let dashboardPatch: Partial> = {} + if (JSON.stringify(currentLayout) !== JSON.stringify(normalizedLayout)) { + dashboardPatch.layout = normalizedLayout + changed = true + } + + // Ensure sections used in TV playlists include the widget on the first slide. + if (Array.isArray(dashboard.sections) && dashboard.sections.length > 0) { + const updatedSections = dashboard.sections.map((section, index) => { + const keys = Array.isArray(section.widgetKeys) ? section.widgetKeys : [] + const without = keys.filter((key) => key !== widgetKey) + if (index === 0) { + const nextKeys = [widgetKey, ...without] + if (JSON.stringify(nextKeys) !== JSON.stringify(keys)) { + changed = true + return { ...section, widgetKeys: nextKeys } + } + return section + } + if (without.length !== keys.length) { + changed = true + return { ...section, widgetKeys: without } + } + return section + }) + if (changed && JSON.stringify(updatedSections) !== JSON.stringify(dashboard.sections)) { + dashboardPatch.sections = updatedSections + } + } + + if (Object.keys(dashboardPatch).length > 0) { + dashboardPatch.updatedAt = now + dashboardPatch.updatedBy = actorId + await ctx.db.patch(dashboardId, dashboardPatch) + } else if (changed) { + await ctx.db.patch(dashboardId, { updatedAt: now, updatedBy: actorId }) + } + + if (!changed) { + // Nothing changed; still return success. + return { ensured: false, widgetKey } + } + + return { ensured: true, widgetKey } + }, +}) + export const duplicateWidget = mutation({ args: { tenantId: v.string(), diff --git a/src/components/dashboards/dashboard-builder.tsx b/src/components/dashboards/dashboard-builder.tsx index cb91972..180163b 100644 --- a/src/components/dashboards/dashboard-builder.tsx +++ b/src/components/dashboards/dashboard-builder.tsx @@ -570,6 +570,7 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" } const [isDeletingDashboard, setIsDeletingDashboard] = useState(false) const fullscreenContainerRef = useRef(null) const previousSidebarStateRef = useRef<{ open: boolean; openMobile: boolean } | null>(null) + const ensureQueueSummaryRequestedRef = useRef(false) const updateLayoutMutation = useMutation(api.dashboards.updateLayout) const updateFiltersMutation = useMutation(api.dashboards.updateFilters) @@ -578,6 +579,7 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" } const duplicateWidgetMutation = useMutation(api.dashboards.duplicateWidget) const removeWidgetMutation = useMutation(api.dashboards.removeWidget) const updateMetadataMutation = useMutation(api.dashboards.updateMetadata) + const ensureQueueSummaryWidgetMutation = useMutation(api.dashboards.ensureQueueSummaryWidget) const archiveDashboardMutation = useMutation(api.dashboards.archive) useEffect(() => { @@ -625,6 +627,43 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" } layoutRef.current = layoutState }, [layoutState]) + useEffect(() => { + if (!dashboard || !convexUserId || !isStaff) return + if (widgets.length === 0) return + const queueIndex = widgets.findIndex((widget) => { + const type = (widget.type ?? "").toLowerCase() + if (type === "queue-summary") return true + const configType = + widget.config && typeof widget.config === "object" + ? ((widget.config as WidgetConfig).type ?? "").toLowerCase() + : "" + return configType === "queue-summary" + }) + if (queueIndex === 0) { + ensureQueueSummaryRequestedRef.current = false + return + } + if (queueIndex === -1 || queueIndex > 0) { + if (ensureQueueSummaryRequestedRef.current) return + ensureQueueSummaryRequestedRef.current = true + ensureQueueSummaryWidgetMutation({ + tenantId, + actorId: convexUserId as Id<"users">, + dashboardId: dashboard.id as Id<"dashboards">, + }).catch((error) => { + console.error("[dashboards] Failed to ensure queue summary widget", error) + ensureQueueSummaryRequestedRef.current = false + }) + } + }, [ + dashboard, + widgets, + convexUserId, + isStaff, + tenantId, + ensureQueueSummaryWidgetMutation, + ]) + useEffect(() => { if (sections.length === 0) { setActiveSectionIndex(0)