feat: ensure queue summary widget in dashboards

This commit is contained in:
Esdras Renan 2025-11-06 17:23:29 -03:00
parent a542846313
commit 343f0c8c64
2 changed files with 201 additions and 0 deletions

View file

@ -57,6 +57,28 @@ function generateWidgetKey(dashboardId: Id<"dashboards">) {
return `${dashboardId.toString().slice(-6)}-${rand}` 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<string, unknown>) : {}),
}
}
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) { function normalizeWidgetConfig(type: WidgetType, config: unknown) {
if (config && typeof config === "object") { if (config && typeof config === "object") {
return config 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<Doc<"dashboards">> = {}
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({ export const duplicateWidget = mutation({
args: { args: {
tenantId: v.string(), tenantId: v.string(),

View file

@ -570,6 +570,7 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
const [isDeletingDashboard, setIsDeletingDashboard] = useState(false) const [isDeletingDashboard, setIsDeletingDashboard] = useState(false)
const fullscreenContainerRef = useRef<HTMLDivElement | null>(null) const fullscreenContainerRef = useRef<HTMLDivElement | null>(null)
const previousSidebarStateRef = useRef<{ open: boolean; openMobile: boolean } | null>(null) const previousSidebarStateRef = useRef<{ open: boolean; openMobile: boolean } | null>(null)
const ensureQueueSummaryRequestedRef = useRef(false)
const updateLayoutMutation = useMutation(api.dashboards.updateLayout) const updateLayoutMutation = useMutation(api.dashboards.updateLayout)
const updateFiltersMutation = useMutation(api.dashboards.updateFilters) 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 duplicateWidgetMutation = useMutation(api.dashboards.duplicateWidget)
const removeWidgetMutation = useMutation(api.dashboards.removeWidget) const removeWidgetMutation = useMutation(api.dashboards.removeWidget)
const updateMetadataMutation = useMutation(api.dashboards.updateMetadata) const updateMetadataMutation = useMutation(api.dashboards.updateMetadata)
const ensureQueueSummaryWidgetMutation = useMutation(api.dashboards.ensureQueueSummaryWidget)
const archiveDashboardMutation = useMutation(api.dashboards.archive) const archiveDashboardMutation = useMutation(api.dashboards.archive)
useEffect(() => { useEffect(() => {
@ -625,6 +627,43 @@ export function DashboardBuilder({ dashboardId, editable = true, mode = "edit" }
layoutRef.current = layoutState layoutRef.current = layoutState
}, [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(() => { useEffect(() => {
if (sections.length === 0) { if (sections.length === 0) {
setActiveSectionIndex(0) setActiveSectionIndex(0)