From a5428463131104b6ab833a6d3b2ed369cdc5fdb0 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Thu, 6 Nov 2025 17:05:31 -0300 Subject: [PATCH] feat: add queue summary widget and layout fixes --- convex/dashboards.ts | 7 ++ convex/metrics.ts | 109 +++++++++++++++++ .../admin/devices/admin-devices-overview.tsx | 84 +++++++++++-- .../devices/device-custom-field-manager.tsx | 11 +- .../dashboards/dashboard-builder.tsx | 2 + src/components/dashboards/metric-catalog.ts | 10 ++ src/components/dashboards/widget-renderer.tsx | 112 +++++++++++++++--- .../tickets/ticket-custom-fields.tsx | 15 ++- .../tickets/ticket-details-panel.tsx | 2 +- .../tickets/ticket-queue-summary.tsx | 5 +- .../tickets/ticket-summary-header.tsx | 20 ++++ src/components/ui/chart.tsx | 18 +-- 12 files changed, 350 insertions(+), 45 deletions(-) diff --git a/convex/dashboards.ts b/convex/dashboards.ts index f1d88c9..5259c3b 100644 --- a/convex/dashboards.ts +++ b/convex/dashboards.ts @@ -13,6 +13,7 @@ const WIDGET_TYPES = [ "radar", "gauge", "table", + "queue-summary", "text", ] as const @@ -145,6 +146,12 @@ function normalizeWidgetConfig(type: WidgetType, config: unknown) { ], options: { downloadCSV: true }, } + case "queue-summary": + return { + type: "queue-summary", + title: "Resumo por fila", + dataSource: { metricKey: "queues.summary_cards" }, + } case "text": default: return { diff --git a/convex/metrics.ts b/convex/metrics.ts index 9da0090..f4db8dd 100644 --- a/convex/metrics.ts +++ b/convex/metrics.ts @@ -90,6 +90,24 @@ function filterTicketsByQueue | null }>( }) } +const QUEUE_RENAME_LOOKUP: Record = { + "Suporte N1": "Chamados", + "suporte-n1": "Chamados", + chamados: "Chamados", + "Suporte N2": "Laboratório", + "suporte-n2": "Laboratório", + laboratorio: "Laboratório", + Laboratorio: "Laboratório", + visitas: "Visitas", +} + +function renameQueueName(value: string) { + const direct = QUEUE_RENAME_LOOKUP[value] + if (direct) return direct + const normalizedKey = value.replace(/\s+/g, "-").toLowerCase() + return QUEUE_RENAME_LOOKUP[normalizedKey] ?? value +} + type AgentStatsRaw = { agentId: Id<"users"> name: string | null @@ -441,6 +459,97 @@ const metricResolvers: Record = { data, } }, + "queues.summary_cards": async (ctx, { tenantId, viewer, params }) => { + const queueFilter = parseQueueIds(params) + const filterHas = queueFilter && queueFilter.length > 0 + const normalizeKey = (id: Id<"queues"> | null) => (id ? String(id) : "sem-fila") + + const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect() + const queueNameMap = new Map() + queues.forEach((queue) => { + const key = String(queue._id) + queueNameMap.set(key, renameQueueName(queue.name)) + }) + + const now = Date.now() + const stats = new Map< + string, + { id: string; name: string; pending: number; inProgress: number; paused: number; breached: number } + >() + + const ensureEntry = (key: string, fallbackName?: string) => { + if (!stats.has(key)) { + const resolvedName = + queueNameMap.get(key) ?? + (key === "sem-fila" ? "Sem fila" : fallbackName ?? "Fila desconhecida") + stats.set(key, { + id: key, + name: resolvedName, + pending: 0, + inProgress: 0, + paused: 0, + breached: 0, + }) + } + return stats.get(key)! + } + + for (const queue of queues) { + const key = String(queue._id) + if (filterHas && queueFilter && !queueFilter.includes(key)) continue + ensureEntry(key) + } + + const scopedTickets = await fetchScopedTickets(ctx, tenantId, viewer) + for (const ticket of scopedTickets) { + const key = normalizeKey(ticket.queueId ?? null) + if (filterHas && queueFilter && !queueFilter.includes(key)) continue + const entry = ensureEntry(key) + const status = normalizeStatus(ticket.status) + if (status === "PENDING") { + entry.pending += 1 + } else if (status === "AWAITING_ATTENDANCE") { + entry.inProgress += 1 + } else if (status === "PAUSED") { + entry.paused += 1 + } + if (status !== "RESOLVED") { + const dueAt = typeof ticket.dueAt === "number" ? ticket.dueAt : null + if (dueAt && dueAt < now) { + entry.breached += 1 + } + } + } + + if (!(filterHas && queueFilter && !queueFilter.includes("sem-fila"))) { + ensureEntry("sem-fila", "Sem fila") + } else if (filterHas) { + stats.delete("sem-fila") + } + + const data = Array.from(stats.values()).map((item) => ({ + id: item.id, + name: item.name, + pending: item.pending, + inProgress: item.inProgress, + paused: item.paused, + breached: item.breached, + })) + + data.sort((a, b) => { + const totalA = a.pending + a.inProgress + a.paused + const totalB = b.pending + b.inProgress + b.paused + if (totalA === totalB) { + return a.name.localeCompare(b.name, "pt-BR") + } + return totalB - totalA + }) + + return { + meta: { kind: "collection", key: "queues.summary_cards" }, + data, + } + }, "tickets.sla_compliance_by_queue": async (ctx, { tenantId, viewer, params }) => { const rangeDays = parseRange(params) const companyId = parseCompanyId(params) diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index 88f2415..4b8f17a 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -132,6 +132,29 @@ function areColumnConfigsEqual(a: DeviceInventoryColumnConfig[], b: DeviceInvent return a.every((col, idx) => col.key === b[idx]?.key && (col.label ?? "") === (b[idx]?.label ?? "")) } +function formatDeviceCustomFieldDisplay(entry?: { value?: unknown; displayValue?: string }): string { + if (!entry) return "—" + if (typeof entry.displayValue === "string" && entry.displayValue.trim().length > 0) { + return entry.displayValue + } + const raw = entry.value + if (raw === null || raw === undefined) return "—" + if (Array.isArray(raw)) { + const values = raw + .map((item) => (item === null || item === undefined ? "" : String(item).trim())) + .filter((item) => item.length > 0) + return values.length > 0 ? values.join(", ") : "—" + } + if (typeof raw === "boolean") { + return raw ? "Sim" : "Não" + } + if (typeof raw === "number") { + return Number.isFinite(raw) ? String(raw) : "—" + } + const asString = String(raw).trim() + return asString.length > 0 ? asString : "—" +} + type DeviceAlertEntry = { id: string kind: string @@ -886,7 +909,7 @@ export type DevicesQueryItem = { lastPostureAt?: number | null linkedUsers?: Array<{ id: string; email: string; name: string }> remoteAccessEntries: DeviceRemoteAccessEntry[] - customFields?: Array<{ fieldKey: string; label: string; type?: string; value: unknown; displayValue?: string }> + customFields?: Array<{ fieldId?: string; fieldKey: string; label: string; type?: string; value: unknown; displayValue?: string }> } export function normalizeDeviceItem(raw: Record): DevicesQueryItem { @@ -3421,6 +3444,49 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { [deviceFieldDefs] ) + const displayCustomFields = useMemo(() => { + const definitions = deviceFieldDefs ?? [] + const values = device?.customFields ?? [] + const result: Array<{ key: string; label: string; value: string }> = [] + const valueMap = new Map() + + values.forEach((field) => { + if (field.fieldId) { + valueMap.set(String(field.fieldId), field) + } + if (field.fieldKey) { + valueMap.set(field.fieldKey, field) + } + }) + + const used = new Set() + definitions.forEach((definition) => { + const idKey = String(definition.id) + const valueEntry = valueMap.get(idKey) ?? valueMap.get(definition.key) + used.add(idKey) + result.push({ + key: idKey, + label: definition.label, + value: formatDeviceCustomFieldDisplay(valueEntry), + }) + }) + + values.forEach((field) => { + const idKey = field.fieldId ? String(field.fieldId) : undefined + const keyKey = field.fieldKey ?? field.label + const compositeKey = idKey ?? keyKey + if (!compositeKey || used.has(compositeKey)) return + used.add(compositeKey) + result.push({ + key: compositeKey, + label: field.label, + value: formatDeviceCustomFieldDisplay(field), + }) + }) + + return result + }, [deviceFieldDefs, device?.customFields]) + const handleSaveCustomFields = useCallback(async () => { if (!device || !convexUserId) return try { @@ -3550,14 +3616,14 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { {/* Campos personalizados (posicionado logo após métricas) */}
-
+

Campos personalizados

- {(device.customFields ?? []).length} + {displayCustomFields.length}
- {(!device.customFields || device.customFields.length === 0) ? ( + {displayCustomFields.length === 0 ? (

Nenhum campo personalizado definido para este dispositivo.

) : null}
@@ -3575,12 +3641,12 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { ) : null}
- {device.customFields && device.customFields.length > 0 ? ( + {displayCustomFields.length > 0 ? (
- {(device.customFields ?? []).map((f, idx) => ( -
-

{f.label}

-

{(f.displayValue ?? f.value ?? "—") as string}

+ {displayCustomFields.map((field) => ( +
+

{field.label}

+

{field.value}

))}
diff --git a/src/components/admin/devices/device-custom-field-manager.tsx b/src/components/admin/devices/device-custom-field-manager.tsx index 2ac3b86..ef01d05 100644 --- a/src/components/admin/devices/device-custom-field-manager.tsx +++ b/src/components/admin/devices/device-custom-field-manager.tsx @@ -194,13 +194,16 @@ export function DeviceCustomFieldManager({ return ( <> {triggerButton} - { + { setOpen(value) if (!value) { resetForm() } - }}> - + }} + > + Campos personalizados de dispositivos @@ -209,7 +212,7 @@ export function DeviceCustomFieldManager({ -
+

Campos cadastrados

diff --git a/src/components/dashboards/dashboard-builder.tsx b/src/components/dashboards/dashboard-builder.tsx index 8b51bb2..cb91972 100644 --- a/src/components/dashboards/dashboard-builder.tsx +++ b/src/components/dashboards/dashboard-builder.tsx @@ -206,6 +206,7 @@ const WIDGET_LIBRARY: Array<{ { type: "radar", title: "Gráfico radar", description: "Comparação radial entre dimensões de performance." }, { type: "gauge", title: "Indicador radial", description: "Mede um percentual (0-100%) em formato de gauge." }, { type: "table", title: "Tabela dinâmica", description: "Lista tabular com cabeçalhos personalizáveis e ordenação." }, + { type: "queue-summary", title: "Resumo por fila", description: "Cards com pendências, andamento e SLA por fila." }, { type: "text", title: "Bloco de texto", description: "Destaques, insights ou instruções em rich-text." }, ] @@ -230,6 +231,7 @@ const widgetSizePresets: Record< radar: { default: { w: 5, h: 6 }, min: { w: 4, h: 4 }, max: { w: 8, h: 9 } }, gauge: { default: { w: 4, h: 5 }, min: { w: 3, h: 4 }, max: { w: 6, h: 7 } }, table: { default: { w: 8, h: 8 }, min: { w: 6, h: 5 }, max: { w: 12, h: 12 } }, + "queue-summary": { default: { w: 12, h: 6 }, min: { w: 8, h: 4 }, max: { w: 12, h: 9 } }, text: { default: { w: 6, h: 4 }, min: { w: 4, h: 3 }, max: { w: 10, h: 8 } }, } diff --git a/src/components/dashboards/metric-catalog.ts b/src/components/dashboards/metric-catalog.ts index 7e98ffb..75a72c5 100644 --- a/src/components/dashboards/metric-catalog.ts +++ b/src/components/dashboards/metric-catalog.ts @@ -32,6 +32,7 @@ export type DashboardMetricDefinition = { | "radar" | "gauge" | "table" + | "queue-summary" | "text" keywords?: string[] encoding?: MetricEncoding @@ -116,6 +117,15 @@ export const DASHBOARD_METRIC_DEFINITIONS: DashboardMetricDefinition[] = [ options: { legend: false, tooltip: true, indicator: "dot", valueFormatter: "percent" }, keywords: ["sla", "fila", "percentual", "qualidade"], }, + { + key: "queues.summary_cards", + name: "Resumo por fila (cards)", + description: "Resumo visual com pendências, andamento e violações de SLA por fila.", + defaultTitle: "Resumo das filas", + recommendedWidget: "queue-summary", + keywords: ["fila", "cards", "pendências", "sla"], + audience: "admin", + }, { key: "tickets.sla_rate", name: "Taxa geral de cumprimento de SLA", diff --git a/src/components/dashboards/widget-renderer.tsx b/src/components/dashboards/widget-renderer.tsx index f3cf63e..b23f7d4 100644 --- a/src/components/dashboards/widget-renderer.tsx +++ b/src/components/dashboards/widget-renderer.tsx @@ -34,6 +34,7 @@ import { api } from "@/convex/_generated/api" import { DEFAULT_TENANT_ID } from "@/lib/constants" import { useAuth } from "@/lib/auth-client" import { cn } from "@/lib/utils" +import type { TicketQueueSummary } from "@/lib/schemas/ticket" import { Card, CardContent, @@ -59,12 +60,14 @@ import { TableHeader, TableRow, } from "@/components/ui/table" +import { TicketQueueSummaryCards } from "@/components/tickets/ticket-queue-summary" const numberFormatter = new Intl.NumberFormat("pt-BR", { maximumFractionDigits: 1 }) const percentFormatter = new Intl.NumberFormat("pt-BR", { style: "percent", maximumFractionDigits: 1 }) const CHART_COLORS = ["var(--chart-1)", "var(--chart-2)", "var(--chart-3)", "var(--chart-4)", "var(--chart-5)"] const DEFAULT_CHART_HEIGHT = 320 +const PRESENTATION_CHART_HEIGHT = 420 export type DashboardFilters = { range?: "7d" | "30d" | "90d" | "custom" @@ -330,6 +333,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } description, metric, config, + mode, }) case "line": return renderLineChart({ @@ -337,6 +341,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } description, metric, config, + mode, }) case "area": return renderAreaChart({ @@ -344,6 +349,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } description, metric, config, + mode, }) case "pie": return renderPieChart({ @@ -351,6 +357,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } description, metric, config, + mode, }) case "radar": return renderRadarChart({ @@ -358,6 +365,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } description, metric, config, + mode, }) case "gauge": return renderGauge({ @@ -365,6 +373,7 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } description, metric, config, + mode, }) case "table": return renderTable({ @@ -373,6 +382,14 @@ export function WidgetRenderer({ widget, filters, mode = "edit", onReadyChange } metric, config, isLoading, + mode, + }) + case "queue-summary": + return renderQueueSummary({ + title: resolvedTitle, + description, + metric, + isLoading, }) case "text": default: @@ -472,11 +489,13 @@ function renderBarChart({ description, metric, config, + mode, }: { title: string description?: string metric: MetricResult config: WidgetConfig + mode: WidgetRendererProps["mode"] }) { const xKey = config.encoding?.x ?? "date" const series = Array.isArray(config.encoding?.y) ? config.encoding?.y ?? [] : [] @@ -493,6 +512,8 @@ function renderBarChart({ const yAxisTickFormatter = valueFormatter === "percent" ? (value: number) => percentFormatter.format(value) : undefined const allowDecimals = valueFormatter === "percent" + const isPresentation = mode === "tv" || mode === "print" + const minHeight = isPresentation ? PRESENTATION_CHART_HEIGHT : DEFAULT_CHART_HEIGHT return ( @@ -502,7 +523,7 @@ function renderBarChart({ @@ -548,11 +569,13 @@ function renderLineChart({ description, metric, config, + mode, }: { title: string description?: string metric: MetricResult config: WidgetConfig + mode: WidgetRendererProps["mode"] }) { const xKey = config.encoding?.x ?? "date" const series = Array.isArray(config.encoding?.y) ? config.encoding?.y ?? [] : [] @@ -569,6 +592,8 @@ function renderLineChart({ const allowDecimals = valueFormatter === "percent" const yAxisTickFormatter = valueFormatter === "percent" ? (value: number) => percentFormatter.format(value) : undefined + const isPresentation = mode === "tv" || mode === "print" + const minHeight = isPresentation ? PRESENTATION_CHART_HEIGHT : DEFAULT_CHART_HEIGHT return ( @@ -578,7 +603,7 @@ function renderLineChart({ @@ -618,11 +643,13 @@ function renderAreaChart({ description, metric, config, + mode, }: { title: string description?: string metric: MetricResult config: WidgetConfig + mode: WidgetRendererProps["mode"] }) { const xKey = config.encoding?.x ?? "date" const series = Array.isArray(config.encoding?.y) ? config.encoding?.y ?? [] : [] @@ -640,6 +667,8 @@ function renderAreaChart({ const allowDecimals = valueFormatter === "percent" const yAxisTickFormatter = valueFormatter === "percent" ? (value: number) => percentFormatter.format(value) : undefined + const isPresentation = mode === "tv" || mode === "print" + const minHeight = isPresentation ? PRESENTATION_CHART_HEIGHT : DEFAULT_CHART_HEIGHT return ( @@ -649,7 +678,7 @@ function renderAreaChart({ @@ -698,17 +727,21 @@ function renderPieChart({ description, metric, config, + mode, }: { title: string description?: string metric: MetricResult config: WidgetConfig + mode: WidgetRendererProps["mode"] }) { const categoryKey = config.encoding?.category ?? "name" const valueKey = config.encoding?.value ?? "value" const chartData = Array.isArray(metric.data) ? (metric.data as Array>) : [] const { showLegend, showTooltip, indicator, valueFormatter } = resolveChartOptions(config, { indicator: "dot" }) const tooltipValueFormatter = (value: unknown) => formatMetricValue(value, valueFormatter) + const isPresentation = mode === "tv" || mode === "print" + const minHeight = isPresentation ? PRESENTATION_CHART_HEIGHT : DEFAULT_CHART_HEIGHT return ( {chartData.length === 0 ? ( @@ -721,7 +754,7 @@ function renderPieChart({ return acc }, {}) as ChartConfig} className="group/chart flex h-full w-full items-center justify-center px-2 pb-4 [&_.recharts-legend-wrapper]:px-2 [&_.recharts-legend-wrapper]:pb-1 [&_.recharts-default-legend]:flex [&_.recharts-default-legend]:flex-wrap [&_.recharts-default-legend]:justify-center" - style={{ minHeight: DEFAULT_CHART_HEIGHT }} + style={{ minHeight, height: "100%" }} > {showTooltip ? ( @@ -759,17 +792,21 @@ function renderRadarChart({ description, metric, config, + mode, }: { title: string description?: string metric: MetricResult config: WidgetConfig + mode: WidgetRendererProps["mode"] }) { const angleKey = config.encoding?.angle ?? "label" const radiusKey = config.encoding?.radius ?? "value" const chartData = Array.isArray(metric.data) ? (metric.data as Array>) : [] const { showLegend, showTooltip, indicator, valueFormatter } = resolveChartOptions(config, { indicator: "line" }) const tooltipValueFormatter = (value: unknown) => formatMetricValue(value, valueFormatter) + const isPresentation = mode === "tv" || mode === "print" + const minHeight = isPresentation ? PRESENTATION_CHART_HEIGHT : DEFAULT_CHART_HEIGHT return ( {chartData.length === 0 ? ( @@ -778,7 +815,7 @@ function renderRadarChart({ @@ -813,21 +850,25 @@ function renderGauge({ description, metric, config, + mode, }: { title: string description?: string metric: MetricResult config: WidgetConfig + mode: WidgetRendererProps["mode"] }) { const raw = metric.data as { value?: number; total?: number; resolved?: number } | null const value = parseNumeric(raw?.value) ?? 0 const display = Math.max(0, Math.min(1, value)) + const isPresentation = mode === "tv" || mode === "print" + const minHeight = isPresentation ? PRESENTATION_CHART_HEIGHT : DEFAULT_CHART_HEIGHT return ( 0 ? config.columns @@ -883,29 +926,43 @@ function renderTable({ { field: "updatedAt", label: "Atualizado em" }, ] const rows = Array.isArray(metric.data) ? (metric.data as Array>) : [] + const isPresentation = mode === "tv" || mode === "print" + const containerClass = cn( + "flex h-full flex-col overflow-hidden rounded-xl border border-border/60 bg-white/80", + isPresentation ? "min-h-[320px]" : "min-h-[260px]", + ) + const scrollClass = cn( + "overflow-x-hidden overflow-y-auto", + isPresentation ? "max-h-none" : "max-h-[360px]", + ) return ( {rows.length === 0 ? ( ) : ( -
-
- +
+
+
{columns.map((column) => ( - {column.label} + + {column.label} + ))} {rows.map((row, index) => ( - {columns.map((column) => ( - - {renderTableCellValue(row[column.field as keyof typeof row])} - - ))} + {columns.map((column) => { + const cellValue = row[column.field as keyof typeof row] + return ( + + {renderTableCellValue(cellValue)} + + ) + })} ))} @@ -917,6 +974,31 @@ function renderTable({ ) } +function renderQueueSummary({ + title, + description, + metric, + isLoading, +}: { + title: string + description?: string + metric: MetricResult + isLoading: boolean +}) { + const queues = Array.isArray(metric.data) ? (metric.data as TicketQueueSummary[]) : [] + return ( + + {queues.length === 0 ? ( + + ) : ( +
+ +
+ )} +
+ ) +} + function renderTableCellValue(value: unknown) { if (typeof value === "number") { return numberFormatter.format(value) diff --git a/src/components/tickets/ticket-custom-fields.tsx b/src/components/tickets/ticket-custom-fields.tsx index 6cb39f7..e2bad16 100644 --- a/src/components/tickets/ticket-custom-fields.tsx +++ b/src/components/tickets/ticket-custom-fields.tsx @@ -174,9 +174,10 @@ export function TicketCustomFieldsList({ record, emptyMessage, className }: Tick type TicketCustomFieldsSectionProps = { ticket: TicketWithDetails + hidePreview?: boolean } -export function TicketCustomFieldsSection({ ticket }: TicketCustomFieldsSectionProps) { +export function TicketCustomFieldsSection({ ticket, hidePreview = false }: TicketCustomFieldsSectionProps) { const { convexUserId, role } = useAuth() const canEdit = Boolean(convexUserId && (role === "admin" || role === "agent")) @@ -318,10 +319,14 @@ export function TicketCustomFieldsSection({ ticket }: TicketCustomFieldsSectionP ) : null} - + {hidePreview ? ( +

Visualize os valores no resumo principal.

+ ) : ( + + )} diff --git a/src/components/tickets/ticket-details-panel.tsx b/src/components/tickets/ticket-details-panel.tsx index 2d673cb..b69ed2c 100644 --- a/src/components/tickets/ticket-details-panel.tsx +++ b/src/components/tickets/ticket-details-panel.tsx @@ -129,7 +129,7 @@ export function TicketDetailsPanel({ ticket }: TicketDetailsPanelProps) { - +
diff --git a/src/components/tickets/ticket-queue-summary.tsx b/src/components/tickets/ticket-queue-summary.tsx index 0ff86b9..5c9cab6 100644 --- a/src/components/tickets/ticket-queue-summary.tsx +++ b/src/components/tickets/ticket-queue-summary.tsx @@ -16,13 +16,14 @@ interface TicketQueueSummaryProps { export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) { const { convexUserId, isStaff } = useAuth() const enabled = Boolean(isStaff && convexUserId) + const shouldFetch = Boolean(!queues && enabled) const fromServer = useQuery( api.queues.summary, - enabled ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip" + shouldFetch ? { tenantId: DEFAULT_TENANT_ID, viewerId: convexUserId as Id<"users"> } : "skip" ) as TicketQueueSummary[] | undefined const data: TicketQueueSummary[] = queues ?? fromServer ?? [] - if (!queues && fromServer === undefined) { + if (!queues && shouldFetch && fromServer === undefined) { return (
{Array.from({ length: 3 }).map((_, index) => ( diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index 071957a..a613a0e 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -27,6 +27,7 @@ import { Textarea } from "@/components/ui/textarea" import { Spinner } from "@/components/ui/spinner" import { useTicketCategories } from "@/hooks/use-ticket-categories" import { useDefaultQueues } from "@/hooks/use-default-queues" +import { mapTicketCustomFields } from "@/lib/ticket-custom-fields" import { DropdownMenu, DropdownMenuContent, @@ -213,6 +214,7 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { queuesEnabled ? { tenantId: ticket.tenantId, viewerId: convexUserId as Id<"users"> } : "skip" ) const queues: TicketQueueSummary[] = Array.isArray(queuesResult) ? queuesResult : [] + const customFieldEntries = useMemo(() => mapTicketCustomFields(ticket.customFields), [ticket.customFields]) const { categories, isLoading: categoriesLoading } = useTicketCategories(ticket.tenantId) const workSummaryRemote = useQuery( api.tickets.workSummary, @@ -1583,6 +1585,24 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
) : null}
+
+ Informações adicionais + {customFieldEntries.length > 0 ? ( +
+ {customFieldEntries.map((entry) => ( +
+

{entry.label}

+

{entry.formattedValue}

+
+ ))} +
+ ) : ( +

Nenhum campo adicional preenchido para este chamado.

+ )} +
diff --git a/src/components/ui/chart.tsx b/src/components/ui/chart.tsx index a077ad6..ccd41f4 100644 --- a/src/components/ui/chart.tsx +++ b/src/components/ui/chart.tsx @@ -51,15 +51,15 @@ function ChartContainer({ return ( -
+
{children}