From 06deb99bcd8996b15cb47edbf1d6af8505bee7e7 Mon Sep 17 00:00:00 2001 From: codex-bot Date: Tue, 4 Nov 2025 13:41:32 -0300 Subject: [PATCH] =?UTF-8?q?feat(export,tickets,forms,emails):\n-=20Corrige?= =?UTF-8?q?=20scroll=20de=20Dialogs=20e=20melhora=20UI=20de=20sele=C3=A7?= =?UTF-8?q?=C3=A3o=20de=20colunas=20(=C3=ADcones=20e=20separador)\n-=20Aju?= =?UTF-8?q?sta=20rota/params=20da=20exporta=C3=A7=C3=A3o=20em=20massa=20e?= =?UTF-8?q?=20adiciona=20modal=20de=20exporta=C3=A7=C3=A3o=20individual\n-?= =?UTF-8?q?=20Renomeia=20'Chamado=20padr=C3=A3o'=20para=20'Chamado'=20e=20?= =?UTF-8?q?garante=20visibilidade=20total=20para=20admin/agente\n-=20Adici?= =?UTF-8?q?ona=20toggles=20por=20empresa/usu=C3=A1rio=20para=20habilitar?= =?UTF-8?q?=20Admiss=C3=A3o/Desligamento\n-=20Exibe=20badge=20do=20tipo=20?= =?UTF-8?q?de=20solicita=C3=A7=C3=A3o=20na=20listagem=20e=20no=20cabe?= =?UTF-8?q?=C3=A7alho=20do=20ticket\n-=20Prepara=20notifica=C3=A7=C3=B5es?= =?UTF-8?q?=20por=20e-mail=20(coment=C3=A1rio=20p=C3=BAblico=20e=20encerra?= =?UTF-8?q?mento)=20via=20SMTP\n?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- convex/_generated/api.d.ts | 2 + convex/ticketNotifications.ts | 159 +++++++++++++++ convex/tickets.ts | 38 +++- .../companies/admin-companies-manager.tsx | 77 ++++++- .../admin/devices/admin-devices-overview.tsx | 191 +++++++++++++++++- .../admin/users/admin-users-workspace.tsx | 62 ++++++ src/components/portal/portal-ticket-form.tsx | 2 +- src/components/tickets/new-ticket-dialog.tsx | 2 +- .../tickets/ticket-summary-header.tsx | 13 +- src/components/tickets/tickets-table.tsx | 11 + src/components/ui/dialog.tsx | 2 +- src/lib/mappers/ticket.ts | 1 + 12 files changed, 543 insertions(+), 17 deletions(-) create mode 100644 convex/ticketNotifications.ts diff --git a/convex/_generated/api.d.ts b/convex/_generated/api.d.ts index e842232..dce9e18 100644 --- a/convex/_generated/api.d.ts +++ b/convex/_generated/api.d.ts @@ -31,6 +31,7 @@ import type * as seed from "../seed.js"; import type * as slas from "../slas.js"; import type * as teams from "../teams.js"; import type * as ticketFormSettings from "../ticketFormSettings.js"; +import type * as ticketNotifications from "../ticketNotifications.js"; import type * as tickets from "../tickets.js"; import type * as users from "../users.js"; @@ -72,6 +73,7 @@ declare const fullApi: ApiFromModules<{ slas: typeof slas; teams: typeof teams; ticketFormSettings: typeof ticketFormSettings; + ticketNotifications: typeof ticketNotifications; tickets: typeof tickets; users: typeof users; }>; diff --git a/convex/ticketNotifications.ts b/convex/ticketNotifications.ts new file mode 100644 index 0000000..e19a6b4 --- /dev/null +++ b/convex/ticketNotifications.ts @@ -0,0 +1,159 @@ +"use node" + +import tls from "tls" +import { action } from "./_generated/server" +import { v } from "convex/values" + +function b64(input: string) { + return Buffer.from(input, "utf8").toString("base64") +} + +async function sendSmtpMail(cfg: { host: string; port: number; username: string; password: string; from: string }, to: string, subject: string, html: string) { + return new Promise((resolve, reject) => { + const socket = tls.connect(cfg.port, cfg.host, { rejectUnauthorized: false }, () => { + let buffer = "" + const send = (line: string) => socket.write(line + "\r\n") + const wait = (expected: string | RegExp) => + new Promise((res) => { + const onData = (data: Buffer) => { + buffer += data.toString() + const lines = buffer.split(/\r?\n/) + const last = lines.filter(Boolean).slice(-1)[0] ?? "" + if (typeof expected === "string" ? last.startsWith(expected) : expected.test(last)) { + socket.removeListener("data", onData) + res() + } + } + socket.on("data", onData) + socket.on("error", reject) + }) + + ;(async () => { + await wait(/^220 /) + send(`EHLO ${cfg.host}`) + await wait(/^250-/) + await wait(/^250 /) + send("AUTH LOGIN") + await wait(/^334 /) + send(b64(cfg.username)) + await wait(/^334 /) + send(b64(cfg.password)) + await wait(/^235 /) + send(`MAIL FROM:<${cfg.from.match(/<(.+)>/)?.[1] ?? cfg.from}>`) + await wait(/^250 /) + send(`RCPT TO:<${to}>`) + await wait(/^250 /) + send("DATA") + await wait(/^354 /) + const headers = [ + `From: ${cfg.from}`, + `To: ${to}`, + `Subject: ${subject}`, + "MIME-Version: 1.0", + "Content-Type: text/html; charset=UTF-8", + ].join("\r\n") + send(headers + "\r\n\r\n" + html + "\r\n.") + await wait(/^250 /) + send("QUIT") + socket.end() + resolve() + })().catch(reject) + }) + socket.on("error", reject) + }) +} + +function buildBaseUrl() { + return process.env.NEXT_PUBLIC_APP_URL || process.env.APP_BASE_URL || "http://localhost:3000" +} + +function emailTemplate({ title, message, ctaLabel, ctaUrl }: { title: string; message: string; ctaLabel: string; ctaUrl: string }) { + return ` + + + + +
+ + + + +
+
+ Raven + Raven +
+

${title}

+

${message}

+ ${ctaLabel} +

Se o botão não funcionar, copie e cole esta URL no navegador:
${ctaUrl}

+
+

© ${new Date().getFullYear()} Raven — Rever Tecnologia

+
` +} + +export const sendPublicCommentEmail = action({ + args: { + to: v.string(), + ticketId: v.string(), + reference: v.number(), + subject: v.string(), + }, + handler: async (_ctx, { to, ticketId, reference, subject }) => { + const smtp = { + host: process.env.SMTP_ADDRESS!, + port: Number(process.env.SMTP_PORT ?? 465), + username: process.env.SMTP_USERNAME!, + password: process.env.SMTP_PASSWORD!, + from: process.env.MAILER_SENDER_EMAIL || "Raven ", + } + if (!smtp.host || !smtp.username || !smtp.password) { + console.warn("SMTP not configured; skipping ticket comment email") + return { skipped: true } + } + const baseUrl = buildBaseUrl() + const url = `${baseUrl}/portal/tickets/${ticketId}` + const mailSubject = `Atualização no chamado #${reference}: ${subject}` + const html = emailTemplate({ + title: `Nova atualização no seu chamado #${reference}`, + message: `Um novo comentário foi adicionado ao chamado “${subject}”. Clique abaixo para visualizar e responder pelo portal.`, + ctaLabel: "Abrir e responder", + ctaUrl: url, + }) + await sendSmtpMail(smtp, to, mailSubject, html) + return { ok: true } + }, +}) + +export const sendResolvedEmail = action({ + args: { + to: v.string(), + ticketId: v.string(), + reference: v.number(), + subject: v.string(), + }, + handler: async (_ctx, { to, ticketId, reference, subject }) => { + const smtp = { + host: process.env.SMTP_ADDRESS!, + port: Number(process.env.SMTP_PORT ?? 465), + username: process.env.SMTP_USERNAME!, + password: process.env.SMTP_PASSWORD!, + from: process.env.MAILER_SENDER_EMAIL || "Raven ", + } + if (!smtp.host || !smtp.username || !smtp.password) { + console.warn("SMTP not configured; skipping ticket resolution email") + return { skipped: true } + } + const baseUrl = buildBaseUrl() + const url = `${baseUrl}/portal/tickets/${ticketId}` + const mailSubject = `Seu chamado #${reference} foi encerrado` + const html = emailTemplate({ + title: `Chamado #${reference} encerrado`, + message: `O chamado “${subject}” foi marcado como concluído. Caso necessário, você pode responder pelo portal para reabrir dentro do prazo.`, + ctaLabel: "Ver detalhes", + ctaUrl: url, + }) + await sendSmtpMail(smtp, to, mailSubject, html) + return { ok: true } + }, +}) diff --git a/convex/tickets.ts b/convex/tickets.ts index e3ef772..53f835f 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -1,5 +1,6 @@ // CI touch: enable server-side assignee filtering and trigger redeploy import { mutation, query } from "./_generated/server"; +import { api } from "./_generated/api"; import type { MutationCtx, QueryCtx } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import { Id, type Doc, type DataModel } from "./_generated/dataModel"; @@ -1235,6 +1236,7 @@ export const list = query({ priority: t.priority, channel: t.channel, queue: queueName, + formTemplate: t.formTemplate ?? null, company: company ? { id: company._id, name: company.name, isAvulso: company.isAvulso ?? false } : t.companyId || t.companySnapshot @@ -1887,6 +1889,20 @@ export const addComment = mutation({ }); // bump ticket updatedAt await ctx.db.patch(args.ticketId, { updatedAt: now }); + // Notificação por e-mail: comentário público para o solicitante + try { + const snapshotEmail = (ticketDoc.requesterSnapshot as { email?: string } | undefined)?.email + if (requestedVisibility === "PUBLIC" && snapshotEmail && String(ticketDoc.requesterId) !== String(args.authorId)) { + await ctx.scheduler.runAfter(0, api.ticketNotifications.sendPublicCommentEmail, { + to: snapshotEmail, + ticketId: String(ticketDoc._id), + reference: ticketDoc.reference ?? 0, + subject: ticketDoc.subject ?? "", + }) + } + } catch (e) { + console.warn("[tickets] Falha ao agendar e-mail de comentário", e) + } return id; }, }); @@ -2122,6 +2138,22 @@ export async function resolveTicketHandler( createdAt: now, }) + // Notificação por e-mail: encerramento do chamado + try { + const requesterDoc = await ctx.db.get(ticketDoc.requesterId) + const email = (requesterDoc as Doc<"users"> | null)?.email || (ticketDoc.requesterSnapshot as { email?: string } | undefined)?.email || null + if (email) { + await ctx.scheduler.runAfter(0, api.ticketNotifications.sendResolvedEmail, { + to: email, + ticketId: String(ticketId), + reference: ticketDoc.reference ?? 0, + subject: ticketDoc.subject ?? "", + }) + } + } catch (e) { + console.warn("[tickets] Falha ao agendar e-mail de encerramento", e) + } + for (const rel of linkedTickets) { const existing = new Set((rel.relatedTicketIds ?? []).map((value) => String(value))) existing.add(String(ticketId)) @@ -2433,10 +2465,14 @@ export const listTicketForms = query({ }> for (const template of TICKET_FORM_CONFIG) { - const enabled = resolveFormEnabled(template.key, template.defaultEnabled, settings as Doc<"ticketFormSettings">[], { + let enabled = resolveFormEnabled(template.key, template.defaultEnabled, settings as Doc<"ticketFormSettings">[], { companyId: viewerCompanyId, userId: viewer.user._id, }) + const viewerRole = (viewer.role ?? "").toUpperCase() + if (viewerRole === "ADMIN" || viewerRole === "AGENT") { + enabled = true + } if (!enabled) { continue } diff --git a/src/components/admin/companies/admin-companies-manager.tsx b/src/components/admin/companies/admin-companies-manager.tsx index fd78e93..760a33b 100644 --- a/src/components/admin/companies/admin-companies-manager.tsx +++ b/src/components/admin/companies/admin-companies-manager.tsx @@ -80,7 +80,9 @@ import { Textarea } from "@/components/ui/textarea" import { TimePicker } from "@/components/ui/time-picker" import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group" import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion" -import { useQuery } from "convex/react" +import { useQuery, useMutation } from "convex/react" +import { useAuth } from "@/lib/auth-client" +import type { Id } from "@/convex/_generated/dataModel" import { api } from "@/convex/_generated/api" import { MultiValueInput } from "@/components/ui/multi-value-input" import { AdminDevicesOverview } from "@/components/admin/devices/admin-devices-overview" @@ -1685,6 +1687,13 @@ function CompanySheet({ tenantId, editor, onClose, onCreated, onUpdated }: Compa + + Tipos de solicitação + + + + + {editor?.mode === "edit" ? ( Dispositivos vinculadas @@ -2173,3 +2182,69 @@ function BusinessHoursEditor({ form }: BusinessHoursEditorProps) { ) } + +type CompanyRequestTypesControlsProps = { tenantId?: string | null; companyId: string | null } +function CompanyRequestTypesControls({ tenantId, companyId }: CompanyRequestTypesControlsProps) { + const { convexUserId } = useAuth() + const canLoad = Boolean(tenantId && convexUserId) + const settings = useQuery( + api.ticketFormSettings.list, + canLoad ? { tenantId: tenantId as string, viewerId: convexUserId as Id<"users"> } : "skip" + ) as Array<{ template: string; scope: string; companyId?: string | null; enabled: boolean; updatedAt: number }> | undefined + const upsert = useMutation(api.ticketFormSettings.upsert) + + const resolveEnabled = (template: "admissao" | "desligamento") => { + const scoped = (settings ?? []).filter((s) => s.template === template) + const base = true + if (!companyId) return base + const latest = scoped + .filter((s) => s.scope === "company" && String(s.companyId ?? "") === String(companyId)) + .sort((a, b) => b.updatedAt - a.updatedAt)[0] + return typeof latest?.enabled === "boolean" ? latest.enabled : base + } + + const admissaoEnabled = resolveEnabled("admissao") + const desligamentoEnabled = resolveEnabled("desligamento") + + const handleToggle = async (template: "admissao" | "desligamento", enabled: boolean) => { + if (!tenantId || !convexUserId || !companyId) return + try { + await upsert({ + tenantId, + actorId: convexUserId as Id<"users">, + template, + scope: "company", + companyId: companyId as unknown as Id<"companies">, + enabled, + }) + toast.success("Configuração salva.") + } catch (error) { + console.error("Falha ao salvar configuração de formulário", error) + toast.error("Não foi possível salvar a configuração.") + } + } + + return ( +
+

Defina quais tipos de solicitação estão disponíveis para colaboradores/gestores desta empresa. Administradores e agentes sempre veem todas as opções.

+
+ + +
+
+ ) +} diff --git a/src/components/admin/devices/admin-devices-overview.tsx b/src/components/admin/devices/admin-devices-overview.tsx index 43b9296..56c167b 100644 --- a/src/components/admin/devices/admin-devices-overview.tsx +++ b/src/components/admin/devices/admin-devices-overview.tsx @@ -22,6 +22,8 @@ import { Lock, Cloud, RefreshCcw, + CheckSquare, + RotateCcw, AlertTriangle, Key, Globe, @@ -1694,13 +1696,13 @@ export function AdminDevicesOverview({ tenantId, initialCompanyFilterSlug = "all if (companyFilterSlug !== "all") { params.set("companyId", companyFilterSlug) } - orderedSelection.forEach((id) => params.append("deviceId", id)) + orderedSelection.forEach((id) => params.append("machineId", id)) params.set("columns", JSON.stringify(normalizedColumns)) if (selectedTemplateId) { params.set("templateId", selectedTemplateId) } const qs = params.toString() - const url = `/api/reports/devices-inventory.xlsx${qs ? `?${qs}` : ""}` + const url = `/api/reports/machines-inventory.xlsx${qs ? `?${qs}` : ""}` const response = await fetch(url) if (!response.ok) { @@ -2013,10 +2015,13 @@ export function AdminDevicesOverview({ tenantId, initialCompanyFilterSlug = "all

Colunas

- -
@@ -3304,6 +3309,95 @@ export function DeviceDetails({ device }: DeviceDetailsProps) { } }, []) + // Exportação individual (colunas personalizadas) + const [isSingleExportOpen, setIsSingleExportOpen] = useState(false) + const [singleExporting, setSingleExporting] = useState(false) + const [singleExportError, setSingleExportError] = useState(null) + const [singleColumns, setSingleColumns] = useState([...DEFAULT_DEVICE_COLUMN_CONFIG]) + const { convexUserId } = useAuth() + const deviceFieldDefs = useQuery( + api.deviceFields.listForTenant, + convexUserId && device ? { tenantId: device.tenantId, viewerId: convexUserId as Id<"users"> } : "skip" + ) as Array<{ id: string; key: string; label: string }> | undefined + + const baseColumnOptionsSingle = useMemo( + () => DEVICE_INVENTORY_COLUMN_METADATA.map((meta) => ({ key: meta.key, label: meta.label })), + [] + ) + const customColumnOptionsSingle = useMemo(() => { + const map = new Map() + ;(deviceFieldDefs ?? []).forEach((field) => map.set(field.key, { key: field.key, label: field.label })) + ;(device?.customFields ?? []).forEach((field) => { + if (!map.has(field.fieldKey)) map.set(field.fieldKey, { key: field.fieldKey, label: field.label }) + }) + return Array.from(map.values()) + .map((f) => ({ key: `custom:${f.key}`, label: f.label })) + .sort((a, b) => a.label.localeCompare(b.label, "pt-BR")) + }, [deviceFieldDefs, device?.customFields]) + + const singleCustomOrder = useMemo(() => customColumnOptionsSingle.map((c) => c.key), [customColumnOptionsSingle]) + + useEffect(() => { + setSingleColumns((prev) => orderColumnConfig(prev, singleCustomOrder)) + }, [singleCustomOrder]) + + const toggleSingleColumn = useCallback((key: string, checked: boolean, label?: string) => { + setSingleColumns((prev) => { + const filtered = prev.filter((col) => col.key !== key) + if (checked) return orderColumnConfig([...filtered, { key, label }], singleCustomOrder) + return filtered + }) + }, [singleCustomOrder]) + + const resetSingleColumns = useCallback(() => { + setSingleColumns(orderColumnConfig([...DEFAULT_DEVICE_COLUMN_CONFIG], singleCustomOrder)) + }, [singleCustomOrder]) + + const selectAllSingleColumns = useCallback(() => { + const all: DeviceInventoryColumnConfig[] = [ + ...baseColumnOptionsSingle.map((c) => ({ key: c.key })), + ...customColumnOptionsSingle.map((c) => ({ key: c.key, label: c.label })), + ] + setSingleColumns(orderColumnConfig(all, singleCustomOrder)) + }, [baseColumnOptionsSingle, customColumnOptionsSingle, singleCustomOrder]) + + const handleExportSingle = useCallback(async () => { + if (!device) return + const normalized = orderColumnConfig(singleColumns, singleCustomOrder) + if (normalized.length === 0) { + toast.info("Selecione ao menos uma coluna para exportar.") + return + } + setSingleExporting(true) + setSingleExportError(null) + try { + const params = new URLSearchParams() + params.append("machineId", device.id) + params.set("columns", JSON.stringify(normalized)) + const url = `/api/reports/machines-inventory.xlsx?${params.toString()}` + const response = await fetch(url) + if (!response.ok) throw new Error(`HTTP ${response.status}`) + const disposition = response.headers.get("Content-Disposition") + const filenameMatch = disposition?.match(/filename="?([^";]+)"?/i) + const filename = filenameMatch?.[1] ?? `machine-inventory-${device.hostname}.xlsx` + const blob = await response.blob() + const downloadUrl = window.URL.createObjectURL(blob) + const a = document.createElement("a") + a.href = downloadUrl + a.download = filename + document.body.appendChild(a) + a.click() + a.remove() + window.URL.revokeObjectURL(downloadUrl) + setIsSingleExportOpen(false) + } catch (err) { + console.error("Falha na exportação individual", err) + setSingleExportError("Não foi possível gerar a planilha. Tente novamente.") + } finally { + setSingleExporting(false) + } + }, [device, singleColumns, singleCustomOrder]) + return ( @@ -4969,13 +5063,11 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
- {device ? ( - - ) : null} + {device ? ( + + ) : null} @@ -5021,6 +5113,83 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
+ {/* Exportação individual: seleção de colunas */} + (!singleExporting ? setIsSingleExportOpen(open) : null)}> + + + Exportar planilha — {device.displayName ?? device.hostname ?? "Dispositivo"} + Escolha as colunas a incluir na exportação. + +
+
+

Colunas

+
+ + + +
+
+
+ {baseColumnOptionsSingle.map((col) => { + const checked = singleColumns.some((c) => c.key === col.key) + return ( + + ) + })} +
+ {customColumnOptionsSingle.length > 0 ? ( +
+

Campos personalizados

+
+ {customColumnOptionsSingle.map((col) => { + const checked = singleColumns.some((c) => c.key === col.key) + return ( + + ) + })} +
+
+ ) : null} +

{singleColumns.length} coluna{singleColumns.length === 1 ? "" : "s"} selecionada{singleColumns.length === 1 ? "" : "s"}.

+
+ {singleExportError ?

{singleExportError}

: null} + + + + +
+
+ { if (!open) setDeleting(false); setDeleteDialog(open) }}> diff --git a/src/components/admin/users/admin-users-workspace.tsx b/src/components/admin/users/admin-users-workspace.tsx index e4c1050..566dbb8 100644 --- a/src/components/admin/users/admin-users-workspace.tsx +++ b/src/components/admin/users/admin-users-workspace.tsx @@ -22,6 +22,10 @@ import { IconUserOff, } from "@tabler/icons-react" import { toast } from "sonner" +import { useQuery, useMutation } from "convex/react" +import { useAuth } from "@/lib/auth-client" +import type { Id } from "@/convex/_generated/dataModel" +import { api } from "@/convex/_generated/api" import { COMPANY_CONTACT_ROLES, @@ -255,6 +259,41 @@ function AccountsTable({ [accounts, editAccountId] ) + // Tipos de solicitação por usuário + const { convexUserId } = useAuth() + const formSettings = useQuery( + api.ticketFormSettings.list, + convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" + ) as Array<{ template: string; scope: string; userId?: string | null; enabled: boolean; updatedAt: number }> | undefined + const upsertFormSetting = useMutation(api.ticketFormSettings.upsert) + + const resolveUserFormEnabled = useCallback((template: "admissao" | "desligamento") => { + if (!editAccount) return true + const scoped = (formSettings ?? []).filter((s) => s.template === template) + const latest = scoped + .filter((s) => s.scope === "user" && String(s.userId ?? "") === String(editAccount.id)) + .sort((a, b) => b.updatedAt - a.updatedAt)[0] + return typeof latest?.enabled === "boolean" ? latest.enabled : true + }, [formSettings, editAccount]) + + const handleToggleUserForm = useCallback(async (template: "admissao" | "desligamento", enabled: boolean) => { + if (!convexUserId || !editAccount) return + try { + await upsertFormSetting({ + tenantId, + actorId: convexUserId as Id<"users">, + template, + scope: "user", + userId: editAccount.id as unknown as Id<"users">, + enabled, + }) + toast.success("Configuração salva.") + } catch (error) { + console.error("Falha ao salvar configuração de formulário por usuário", error) + toast.error("Não foi possível salvar a configuração.") + } + }, [tenantId, convexUserId, editAccount, upsertFormSetting]) + const roleSelectOptions = useMemo( () => ROLE_OPTIONS_DISPLAY.map((option) => ({ value: option.value, label: option.label })), [], @@ -976,6 +1015,29 @@ function AccountsTable({ disabled={isSavingAccount} />
+ +
+

Tipos de solicitação

+

Disponíveis para este colaborador/gestor no portal. Administradores e agentes sempre veem todas as opções.

+
+ + +
+
diff --git a/src/components/portal/portal-ticket-form.tsx b/src/components/portal/portal-ticket-form.tsx index aafd486..a8d7a5a 100644 --- a/src/components/portal/portal-ticket-form.tsx +++ b/src/components/portal/portal-ticket-form.tsx @@ -56,7 +56,7 @@ export function PortalTicketForm() { const forms = useMemo(() => { const base: TicketFormDefinition = { key: "default", - label: "Chamado padrão", + label: "Chamado", description: "Formulário básico para solicitações gerais.", fields: [], } diff --git a/src/components/tickets/new-ticket-dialog.tsx b/src/components/tickets/new-ticket-dialog.tsx index b43b365..5527ab7 100644 --- a/src/components/tickets/new-ticket-dialog.tsx +++ b/src/components/tickets/new-ticket-dialog.tsx @@ -188,7 +188,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin const forms = useMemo(() => { const base: TicketFormDefinition = { key: "default", - label: "Chamado padrão", + label: "Chamado", description: "Formulário básico para abertura de chamados gerais.", fields: [], } diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index 9d31ee7..2a0d49b 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -1245,7 +1245,18 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) {
) : (
-

{subject}

+
+

{subject}

+ {ticket.formTemplate ? ( + + {ticket.formTemplate === "admissao" + ? "Admissão" + : ticket.formTemplate === "desligamento" + ? "Desligamento" + : "Chamado"} + + ) : null} +
{summary ?

{summary}

: null}
)} diff --git a/src/components/tickets/tickets-table.tsx b/src/components/tickets/tickets-table.tsx index ffbb17d..ac79ff9 100644 --- a/src/components/tickets/tickets-table.tsx +++ b/src/components/tickets/tickets-table.tsx @@ -221,6 +221,17 @@ export function TicketsTable({ tickets, enteringIds }: TicketsTableProps) { {ticket.summary ?? "Sem resumo"} + {ticket.formTemplate ? ( +
+ + {ticket.formTemplate === "admissao" + ? "Admissão" + : ticket.formTemplate === "desligamento" + ? "Desligamento" + : "Chamado"} + +
+ ) : null}
{ticket.category ? ( diff --git a/src/components/ui/dialog.tsx b/src/components/ui/dialog.tsx index 8d251eb..30ac821 100644 --- a/src/components/ui/dialog.tsx +++ b/src/components/ui/dialog.tsx @@ -36,7 +36,7 @@ const DialogContent = React.forwardRef<