diff --git a/agents.md b/agents.md
index 341fdc8..412e509 100644
--- a/agents.md
+++ b/agents.md
@@ -42,6 +42,7 @@
- Relatórios, dashboards e páginas administrativas utilizam `AppShell`, garantindo header/sidebar consistentes.
- Use `SiteHeader` no `header` do `AppShell` para título/lead e ações.
- O conteúdo deve ficar dentro de `
`.
+ - Persistir filtro global de empresa com `usePersistentCompanyFilter` (localStorage) para manter consistência entre relatórios.
## Entregas recentes
- Exportações CSV (Backlog, Canais, CSAT, SLA e Horas por cliente) com parâmetros de período.
diff --git a/convex/tickets.ts b/convex/tickets.ts
index 42ae8f8..6844dbb 100644
--- a/convex/tickets.ts
+++ b/convex/tickets.ts
@@ -738,6 +738,93 @@ export const addComment = mutation({
});
// bump ticket updatedAt
await ctx.db.patch(args.ticketId, { updatedAt: now });
+
+ // If public comment, attempt to notify requester by e-mail (best-effort)
+ if (args.visibility === "PUBLIC") {
+ try {
+ 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 || "no-reply@example.com",
+ }
+ if (smtp.host && smtp.username && smtp.password) {
+ const requester = await ctx.db.get(ticketDoc.requesterId)
+ const to = (requester as any)?.email as string | undefined
+ if (to) {
+ // Minimal SMTP sender (AUTH LOGIN over implicit TLS)
+ const tls = await import("tls")
+ const b64 = (input: string) => Buffer.from(input, "utf8").toString("base64")
+ const sendSmtpMail = async (cfg: { host: string; port: number; username: string; password: string; from: string }, toAddr: 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 /)
+ const fromAddr = (cfg.from.match(/<\s*([^>\s]+)\s*>/)?.[1] ?? cfg.from)
+ send(`MAIL FROM:<${fromAddr}>`)
+ await wait(/^250 /)
+ send(`RCPT TO:<${toAddr}>`)
+ await wait(/^250 /)
+ send("DATA")
+ await wait(/^354 /)
+ const headers = [
+ `From: ${cfg.from}`,
+ `To: ${toAddr}`,
+ `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)
+ })
+ }
+ const ticketRef = (ticketDoc as any).reference ? `#${(ticketDoc as any).reference}` : "Chamado"
+ const subject = `${ticketRef} atualizado: novo comentário público`
+ const body = `
+ Olá,
+ Seu chamado ${ticketRef} recebeu um novo comentário:
+ ${args.body}
+ Atenciosamente,
Equipe de suporte
+ `
+ await sendSmtpMail(smtp, to, subject, body)
+ }
+ }
+ } catch (error) {
+ console.error("Failed to send public comment notification", error)
+ }
+ }
return id;
},
});
diff --git a/src/app/dashboard/page.tsx b/src/app/dashboard/page.tsx
index 9f35c60..69ce147 100644
--- a/src/app/dashboard/page.tsx
+++ b/src/app/dashboard/page.tsx
@@ -1,22 +1,31 @@
+import dynamic from "next/dynamic"
import { AppShell } from "@/components/app-shell"
import { SectionCards } from "@/components/section-cards"
import { SiteHeader } from "@/components/site-header"
import { RecentTicketsPanel } from "@/components/tickets/recent-tickets-panel"
import { TicketQueueSummaryCards } from "@/components/tickets/ticket-queue-summary"
import { ChartAreaInteractive } from "@/components/chart-area-interactive"
-
-export default function Dashboard() {
- return (
- Abrir ticket}
- primaryAction={Modo play}
- />
- }
- >
+
+const NewTicketDialog = dynamic(
+ () =>
+ import("@/components/tickets/new-ticket-dialog").then((module) => ({
+ default: module.NewTicketDialog,
+ })),
+ { ssr: false }
+)
+
+export default function Dashboard() {
+ return (
+ Modo play}
+ primaryAction={}
+ />
+ }
+ >