From 7d6f3bea01fdaa562127aa9c880ce2d487b6a8e5 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Mon, 13 Oct 2025 00:08:18 -0300 Subject: [PATCH] feat: improve ticket export and navigation --- apps/desktop/src/main.tsx | 20 +- convex/commentTemplates.ts | 20 +- convex/machines.ts | 56 +- convex/schema.ts | 5 +- convex/tickets.ts | 11 +- package.json | 1 + pnpm-lock.yaml | 313 +++++++++++ src/app/api/machines/session/route.ts | 76 ++- src/app/api/machines/sessions/route.ts | 3 +- src/app/api/tickets/[id]/export/pdf/route.ts | 472 +--------------- src/app/icon.png | Bin 0 -> 50823 bytes src/app/layout.tsx | 4 +- src/app/machines/handshake/route.ts | 3 +- src/app/tickets/resolved/page.tsx | 5 + .../resolved/tickets-resolved-page-client.tsx | 31 ++ src/components/admin/admin-users-manager.tsx | 68 ++- src/components/app-sidebar.tsx | 128 ++++- src/components/portal/portal-shell.tsx | 10 +- src/components/portal/portal-ticket-form.tsx | 13 +- src/components/portal/portal-ticket-list.tsx | 14 +- .../settings/comment-templates-manager.tsx | 65 ++- .../tickets/delete-ticket-dialog.tsx | 15 +- src/components/tickets/status-select.tsx | 324 +++++++++-- .../tickets/ticket-comments.rich.tsx | 2 +- .../tickets/ticket-summary-header.tsx | 7 +- src/components/tickets/tickets-filters.tsx | 16 +- src/components/tickets/tickets-view.tsx | 28 +- src/server/pdf/ticket-pdf-template.tsx | 511 ++++++++++++++++++ 28 files changed, 1612 insertions(+), 609 deletions(-) create mode 100644 src/app/icon.png create mode 100644 src/app/tickets/resolved/page.tsx create mode 100644 src/app/tickets/resolved/tickets-resolved-page-client.tsx create mode 100644 src/server/pdf/ticket-pdf-template.tsx diff --git a/apps/desktop/src/main.tsx b/apps/desktop/src/main.tsx index 8ea6fe0..184d166 100644 --- a/apps/desktop/src/main.tsx +++ b/apps/desktop/src/main.tsx @@ -221,11 +221,17 @@ function App() { async function register() { if (!profile) return if (!provisioningSecret.trim()) { setError("Informe o código de provisionamento."); return } + const normalizedEmail = collabEmail.trim().toLowerCase() + if (!normalizedEmail) { + setError("Informe o e-mail do colaborador ou gestor para vincular esta máquina.") + return + } setBusy(true); setError(null) try { - const collaboratorPayload = collabEmail.trim() - ? { email: collabEmail.trim(), name: collabName.trim() || undefined } - : undefined + const collaboratorPayload = { + email: normalizedEmail, + name: collabName.trim() || undefined, + } const collaboratorMetadata = collaboratorPayload ? { ...collaboratorPayload, role: accessRole } : undefined @@ -437,7 +443,9 @@ function App() {

- + setCollabEmail(e.target.value)} />
@@ -524,7 +532,9 @@ function App() {
- + setCollabEmail(e.target.value)} />
diff --git a/convex/commentTemplates.ts b/convex/commentTemplates.ts index 59baa5e..713f5a0 100644 --- a/convex/commentTemplates.ts +++ b/convex/commentTemplates.ts @@ -50,20 +50,24 @@ export const list = query({ args: { tenantId: v.string(), viewerId: v.id("users"), + kind: v.optional(v.string()), }, - handler: async (ctx, { tenantId, viewerId }) => { + handler: async (ctx, { tenantId, viewerId, kind }) => { await requireStaff(ctx, viewerId, tenantId) + const normalizedKind = (kind ?? "comment").toLowerCase() const templates = await ctx.db .query("commentTemplates") .withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)) .collect() return templates + .filter((template) => (template.kind ?? "comment") === normalizedKind) .sort((a, b) => a.title.localeCompare(b.title, "pt-BR", { sensitivity: "base" })) .map((template) => ({ id: template._id, title: template.title, body: template.body, + kind: template.kind ?? "comment", createdAt: template.createdAt, updatedAt: template.updatedAt, createdBy: template.createdBy, @@ -78,8 +82,9 @@ export const create = mutation({ actorId: v.id("users"), title: v.string(), body: v.string(), + kind: v.optional(v.string()), }, - handler: async (ctx, { tenantId, actorId, title, body }) => { + handler: async (ctx, { tenantId, actorId, title, body, kind }) => { await requireStaff(ctx, actorId, tenantId) const normalizedTitle = normalizeTitle(title) if (!normalizedTitle || normalizedTitle.length < 3) { @@ -89,19 +94,21 @@ export const create = mutation({ if (!sanitizedBody) { throw new ConvexError("Informe o conteúdo do template") } + const normalizedKind = (kind ?? "comment").toLowerCase() const existing = await ctx.db .query("commentTemplates") .withIndex("by_tenant_title", (q) => q.eq("tenantId", tenantId).eq("title", normalizedTitle)) .first() - if (existing) { + if (existing && (existing.kind ?? "comment") === normalizedKind) { throw new ConvexError("Já existe um template com este título") } const now = Date.now() const id = await ctx.db.insert("commentTemplates", { tenantId, + kind: normalizedKind, title: normalizedTitle, body: sanitizedBody, createdBy: actorId, @@ -121,8 +128,9 @@ export const update = mutation({ actorId: v.id("users"), title: v.string(), body: v.string(), + kind: v.optional(v.string()), }, - handler: async (ctx, { templateId, tenantId, actorId, title, body }) => { + handler: async (ctx, { templateId, tenantId, actorId, title, body, kind }) => { await requireStaff(ctx, actorId, tenantId) const template = await ctx.db.get(templateId) if (!template || template.tenantId !== tenantId) { @@ -137,17 +145,19 @@ export const update = mutation({ if (!sanitizedBody) { throw new ConvexError("Informe o conteúdo do template") } + const normalizedKind = (kind ?? "comment").toLowerCase() const duplicate = await ctx.db .query("commentTemplates") .withIndex("by_tenant_title", (q) => q.eq("tenantId", tenantId).eq("title", normalizedTitle)) .first() - if (duplicate && duplicate._id !== templateId) { + if (duplicate && duplicate._id !== templateId && (duplicate.kind ?? "comment") === normalizedKind) { throw new ConvexError("Já existe um template com este título") } const now = Date.now() await ctx.db.patch(templateId, { + kind: normalizedKind, title: normalizedTitle, body: sanitizedBody, updatedBy: actorId, diff --git a/convex/machines.ts b/convex/machines.ts index db67369..12c3e51 100644 --- a/convex/machines.ts +++ b/convex/machines.ts @@ -65,6 +65,24 @@ function computeFingerprint(tenantId: string, companySlug: string | undefined, h return toHex(sha256(payload)) } +function extractCollaboratorEmail(metadata: unknown): string | null { + if (!metadata || typeof metadata !== "object") return null + const record = metadata as Record + const collaborator = record["collaborator"] + if (!collaborator || typeof collaborator !== "object") return null + const email = (collaborator as { email?: unknown }).email + if (typeof email !== "string") return null + const trimmed = email.trim().toLowerCase() + return trimmed || null +} + +function matchesExistingHardware(existing: Doc<"machines">, identifiers: NormalizedIdentifiers, hostname: string): boolean { + const intersectsMac = existing.macAddresses.some((mac) => identifiers.macs.includes(mac)) + const intersectsSerial = existing.serialNumbers.some((serial) => identifiers.serials.includes(serial)) + const sameHostname = existing.hostname.trim().toLowerCase() === hostname.trim().toLowerCase() + return intersectsMac || intersectsSerial || sameHostname +} + function hashToken(token: string) { return toHex(sha256(token)) } @@ -305,11 +323,24 @@ export const register = mutation({ const now = Date.now() const metadataPatch = args.metadata && typeof args.metadata === "object" ? (args.metadata as Record) : undefined - const existing = await ctx.db + let existing = await ctx.db .query("machines") .withIndex("by_tenant_fingerprint", (q) => q.eq("tenantId", tenantId).eq("fingerprint", fingerprint)) .first() + if (!existing) { + const collaboratorEmail = extractCollaboratorEmail(metadataPatch ?? args.metadata) + if (collaboratorEmail) { + const candidate = await ctx.db + .query("machines") + .withIndex("by_tenant_assigned_email", (q) => q.eq("tenantId", tenantId).eq("assignedUserEmail", collaboratorEmail)) + .first() + if (candidate && matchesExistingHardware(candidate, identifiers, args.hostname)) { + existing = candidate + } + } + } + let machineId: Id<"machines"> if (existing) { @@ -323,6 +354,7 @@ export const register = mutation({ architecture: args.os.architecture, macAddresses: identifiers.macs, serialNumbers: identifiers.serials, + fingerprint, metadata: metadataPatch ? mergeMetadata(existing.metadata, metadataPatch) : existing.metadata, lastHeartbeatAt: now, updatedAt: now, @@ -834,6 +866,28 @@ export const getContext = query({ }, }) +export const findByAuthEmail = query({ + args: { + authEmail: v.string(), + }, + handler: async (ctx, args) => { + const normalizedEmail = args.authEmail.trim().toLowerCase() + + const machine = await ctx.db + .query("machines") + .withIndex("by_auth_email", (q) => q.eq("authEmail", normalizedEmail)) + .first() + + if (!machine) { + return null + } + + return { + id: machine._id, + } + }, +}) + export const linkAuthAccount = mutation({ args: { machineId: v.id("machines"), diff --git a/convex/schema.ts b/convex/schema.ts index 7968186..70dd1ba 100644 --- a/convex/schema.ts +++ b/convex/schema.ts @@ -146,6 +146,7 @@ export default defineSchema({ commentTemplates: defineTable({ tenantId: v.string(), + kind: v.optional(v.string()), title: v.string(), body: v.string(), createdBy: v.id("users"), @@ -154,7 +155,8 @@ export default defineSchema({ updatedAt: v.number(), }) .index("by_tenant", ["tenantId"]) - .index("by_tenant_title", ["tenantId", "title"]), + .index("by_tenant_title", ["tenantId", "title"]) + .index("by_tenant_kind", ["tenantId", "kind"]), ticketWorkSessions: defineTable({ ticketId: v.id("tickets"), @@ -267,6 +269,7 @@ export default defineSchema({ .index("by_tenant", ["tenantId"]) .index("by_tenant_company", ["tenantId", "companyId"]) .index("by_tenant_fingerprint", ["tenantId", "fingerprint"]) + .index("by_tenant_assigned_email", ["tenantId", "assignedUserEmail"]) .index("by_auth_email", ["authEmail"]), machineTokens: defineTable({ diff --git a/convex/tickets.ts b/convex/tickets.ts index 35ea9d2..cd5e12b 100644 --- a/convex/tickets.ts +++ b/convex/tickets.ts @@ -3,7 +3,7 @@ import type { MutationCtx, QueryCtx } from "./_generated/server"; import { ConvexError, v } from "convex/values"; import { Id, type Doc } from "./_generated/dataModel"; -import { requireStaff, requireUser } from "./rbac"; +import { requireAdmin, requireStaff, requireUser } from "./rbac"; const STAFF_ROLES = new Set(["ADMIN", "MANAGER", "AGENT"]); const INTERNAL_STAFF_ROLES = new Set(["ADMIN", "AGENT"]); @@ -1343,8 +1343,13 @@ export const playNext = mutation({ }); export const remove = mutation({ - args: { ticketId: v.id("tickets") }, - handler: async (ctx, { ticketId }) => { + args: { ticketId: v.id("tickets"), actorId: v.id("users") }, + handler: async (ctx, { ticketId, actorId }) => { + const ticket = await ctx.db.get(ticketId) + if (!ticket) { + throw new ConvexError("Ticket não encontrado") + } + await requireAdmin(ctx, actorId, ticket.tenantId) // delete comments (and attachments) const comments = await ctx.db .query("ticketComments") diff --git a/package.json b/package.json index 761be3a..903ad33 100644 --- a/package.json +++ b/package.json @@ -54,6 +54,7 @@ "next": "15.5.4", "next-themes": "^0.4.6", "pdfkit": "^0.17.2", + "@react-pdf/renderer": "^4.1.5", "postcss": "^8.5.6", "react": "19.2.0", "react-dom": "19.2.0", diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index 9f57c85..b7dd1c6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -71,6 +71,9 @@ importers: '@radix-ui/react-tooltip': specifier: ^1.2.8 version: 1.2.8(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) + '@react-pdf/renderer': + specifier: ^4.1.5 + version: 4.3.1(react@19.2.0) '@react-three/fiber': specifier: ^9.3.0 version: 9.3.0(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.180.0) @@ -1519,6 +1522,49 @@ packages: '@radix-ui/rect@1.1.1': resolution: {integrity: sha512-HPwpGIzkl28mWyZqG52jiqDJ12waP11Pa1lGoiyUkIEuMLBP0oeK/C89esbXrxsky5we7dfd8U58nm0SgAWpVw==} + '@react-pdf/fns@3.1.2': + resolution: {integrity: sha512-qTKGUf0iAMGg2+OsUcp9ffKnKi41RukM/zYIWMDJ4hRVYSr89Q7e3wSDW/Koqx3ea3Uy/z3h2y3wPX6Bdfxk6g==} + + '@react-pdf/font@4.0.3': + resolution: {integrity: sha512-N1qQDZr6phXYQOp033Hvm2nkUkx2LkszjGPbmRavs9VOYzi4sp31MaccMKptL24ii6UhBh/z9yPUhnuNe/qHwA==} + + '@react-pdf/image@3.0.3': + resolution: {integrity: sha512-lvP5ryzYM3wpbO9bvqLZYwEr5XBDX9jcaRICvtnoRqdJOo7PRrMnmB4MMScyb+Xw10mGeIubZAAomNAG5ONQZQ==} + + '@react-pdf/layout@4.4.1': + resolution: {integrity: sha512-GVzdlWoZWldRDzlWj3SttRXmVDxg7YfraAohwy+o9gb9hrbDJaaAV6jV3pc630Evd3K46OAzk8EFu8EgPDuVuA==} + + '@react-pdf/pdfkit@4.0.4': + resolution: {integrity: sha512-/nITLggsPlB66bVLnm0X7MNdKQxXelLGZG6zB5acF5cCgkFwmXHnLNyxYOUD4GMOMg1HOPShXDKWrwk2ZeHsvw==} + + '@react-pdf/png-js@3.0.0': + resolution: {integrity: sha512-eSJnEItZ37WPt6Qv5pncQDxLJRK15eaRwPT+gZoujP548CodenOVp49GST8XJvKMFt9YqIBzGBV/j9AgrOQzVA==} + + '@react-pdf/primitives@4.1.1': + resolution: {integrity: sha512-IuhxYls1luJb7NUWy6q5avb1XrNaVj9bTNI40U9qGRuS6n7Hje/8H8Qi99Z9UKFV74bBP3DOf3L1wV2qZVgVrQ==} + + '@react-pdf/reconciler@1.1.4': + resolution: {integrity: sha512-oTQDiR/t4Z/Guxac88IavpU2UgN7eR0RMI9DRKvKnvPz2DUasGjXfChAdMqDNmJJxxV26mMy9xQOUV2UU5/okg==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-pdf/render@4.3.1': + resolution: {integrity: sha512-v1WAaAhQShQZGcBxfjkEThGCHVH9CSuitrZ1bIOLvB5iBKM14abYK5D6djKhWCwF6FTzYeT2WRjRMVgze/ND2A==} + + '@react-pdf/renderer@4.3.1': + resolution: {integrity: sha512-dPKHiwGTaOsKqNWCHPYYrx8CDfAGsUnV4tvRsEu0VPGxuot1AOq/M+YgfN/Pb+MeXCTe2/lv6NvA8haUtj3tsA==} + peerDependencies: + react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + + '@react-pdf/stylesheet@6.1.1': + resolution: {integrity: sha512-Iyw0A3wRIeQLN4EkaKf8yF9MvdMxiZ8JjoyzLzDHSxnKYoOA4UGu84veCb8dT9N8MxY5x7a0BUv/avTe586Plg==} + + '@react-pdf/textkit@6.0.0': + resolution: {integrity: sha512-fDt19KWaJRK/n2AaFoVm31hgGmpygmTV7LsHGJNGZkgzXcFyLsx+XUl63DTDPH3iqxj3xUX128t104GtOz8tTw==} + + '@react-pdf/types@2.9.1': + resolution: {integrity: sha512-5GoCgG0G5NMgpPuHbKG2xcVRQt7+E5pg3IyzVIIozKG3nLcnsXW4zy25vG1ZBQA0jmo39q34au/sOnL/0d1A4w==} + '@react-three/fiber@9.3.0': resolution: {integrity: sha512-myPe3YL/C8+Eq939/4qIVEPBW/uxV0iiUbmjfwrs9sGKYDG8ib8Dz3Okq7BQt8P+0k4igedONbjXMQy84aDFmQ==} peerDependencies: @@ -2320,6 +2366,9 @@ packages: '@webgpu/types@0.1.65': resolution: {integrity: sha512-cYrHab4d6wuVvDW5tdsfI6/o6vcLMDe6w2Citd1oS51Xxu2ycLCnVo4fqwujfKWijrZMInTJIKcXxteoy21nVA==} + abs-svg-path@0.1.1: + resolution: {integrity: sha512-d8XPSGjfyzlXC3Xx891DJRyZfqk5JU0BJrDQcsWomFIV1/BIzPW5HDH5iDdWpqWaav0YVIEzT1RHTwWr0FFshA==} + acorn-jsx@5.3.2: resolution: {integrity: sha512-rq9s+JNhf0IChjtDXxllJ7g41oZk5SlXtp0LHwyA5cejwn7vKmKp4pPri6YEePv2PU65sAsegbXtIinmDFDXgQ==} peerDependencies: @@ -2453,6 +2502,9 @@ packages: better-call@1.0.19: resolution: {integrity: sha512-sI3GcA1SCVa3H+CDHl8W8qzhlrckwXOTKhqq3OOPXjgn5aTOMIqGY34zLY/pHA6tRRMjTUC3lz5Mi7EbDA24Kw==} + bidi-js@1.0.3: + resolution: {integrity: sha512-RKshQI1R3YQ+n9YJz2QQ147P66ELpa1FQEg20Dk8oW9t2KgLbpDLLp9aGZ7y8WHSshDknG0bknqGw5/tyCs5tw==} + brace-expansion@1.1.12: resolution: {integrity: sha512-9T9UjW3r0UW5c1Q7GTwllptXwhvYmEzFhzMfZ9H7FQWt+uZePjZPjBP/W1ZEyZ1twGWom5/56TF4lPcqjnDHcg==} @@ -2466,6 +2518,9 @@ packages: brotli@1.3.3: resolution: {integrity: sha512-oTKjJdShmDuGW94SyyaoQvAjf30dZaHnjJ8uAF+u2/vGJkJbJPJAT1gDiOJP5v1Zb6f9KEyW/1HpuaWIXtGHPg==} + browserify-zlib@0.2.0: + resolution: {integrity: sha512-Z942RysHXmJrhqk88FmKBVq/v5tqmSkDz7p54G/MGyjMnCFFnC79XWNbg+Vta8W6Wb2qtSZTSxIGkJrRpCFEiA==} + browserslist@4.26.3: resolution: {integrity: sha512-lAUU+02RFBuCKQPj/P6NgjlbCnLBMp4UtgTx7vNHd3XSIJF87s9a5rA3aH2yw3GS9DqZAUbOtZdCCiZeVRqt0w==} engines: {node: ^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7} @@ -2549,6 +2604,9 @@ packages: color-name@1.1.4: resolution: {integrity: sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==} + color-string@1.9.1: + resolution: {integrity: sha512-shrVawQFojnZv6xM40anx4CkoDP+fZsw/ZerEMsW/pyzsRbElpsL/DBVW7q3ExxwusdNXI3lXpuhEZkzs8p5Eg==} + concat-map@0.0.1: resolution: {integrity: sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==} @@ -2746,6 +2804,9 @@ packages: electron-to-chromium@1.5.234: resolution: {integrity: sha512-RXfEp2x+VRYn8jbKfQlRImzoJU01kyDvVPBmG39eU2iuRVhuS6vQNocB8J0/8GrIMLnPzgz4eW6WiRnJkTuNWg==} + emoji-regex-xs@1.0.0: + resolution: {integrity: sha512-LRlerrMYoIDrT6jgpeZ2YYl/L8EulRTt5hQcYjy5AInh7HWXKimpqx68aknBFpGL2+/IcogTcaydJEgaTmOpDg==} + emoji-regex@9.2.2: resolution: {integrity: sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==} @@ -2936,6 +2997,10 @@ packages: eventemitter3@4.0.7: resolution: {integrity: sha512-8guHBZCwKnFhYdHr2ysuRWErTwhoN2X8XELRlrRwpmfeY2jjuUN4taQMsULKUVo1K4DvZl+0pgfyoysHxvmvEw==} + events@3.3.0: + resolution: {integrity: sha512-mQw+2fkQbALzQ7V0MY0IqdnXNOeTtP4r0lN9z7AAawCXgqea7bDii20AYrIBrFd/Hx0M2Ocz6S111CaFkUcb0Q==} + engines: {node: '>=0.8.x'} + expect-type@1.2.2: resolution: {integrity: sha512-JhFGDVJ7tmDJItKhYgJCGLOWjuK9vPxiXoUFLwLDc99NlmklilbiQJwoctZtt13+xMw91MCk/REan6MWHqDjyA==} engines: {node: '>=12.0.0'} @@ -3108,9 +3173,18 @@ packages: resolution: {integrity: sha512-0hJU9SCPvmMzIBdZFqNPXWa6dqh7WdH0cII9y+CyS8rG3nL48Bclra9HmKhVVUHyPWNH5Y7xDwAB7bfgSjkUMQ==} engines: {node: '>= 0.4'} + hsl-to-hex@1.0.0: + resolution: {integrity: sha512-K6GVpucS5wFf44X0h2bLVRDsycgJmf9FF2elg+CrqD8GcFU8c6vYhgXn8NjUkFCwj+xDFb70qgLbTUm6sxwPmA==} + + hsl-to-rgb-for-reals@1.1.1: + resolution: {integrity: sha512-LgOWAkrN0rFaQpfdWBQlv/VhkOxb5AsBjk6NQVx4yEzWS923T07X0M1Y0VNko2H52HeSpZrZNNMJ0aFqsdVzQg==} + htmlparser2@8.0.2: resolution: {integrity: sha512-GYdjWKDkbRLkZ5geuHs5NY1puJ+PXwP7+fHPRz06Eirsb9ugf6d8kkXav6ADhcODhFFPMIXyxkxSuMf3D6NCFA==} + hyphen@1.10.6: + resolution: {integrity: sha512-fXHXcGFTXOvZTSkPJuGOQf5Lv5T/R2itiiCVPg9LxAje5D00O0pP83yJShFq5V89Ly//Gt6acj7z8pbBr34stw==} + ieee754@1.2.1: resolution: {integrity: sha512-dcyqhDvX1C46lXZcVqCpK+FtMRQVdIMN6/Df5js2zouUsqG7I6sFxitIC+7KYK29KdXOLHdu9zL4sFnoVQnqaA==} @@ -3130,6 +3204,9 @@ packages: resolution: {integrity: sha512-JmXMZ6wuvDmLiHEml9ykzqO6lwFbof0GG4IkcGaENdCRDDmMVnny7s5HsIgHCbaq0w2MyPhDqkhTUgS2LU2PHA==} engines: {node: '>=0.8.19'} + inherits@2.0.4: + resolution: {integrity: sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==} + internal-slot@1.1.0: resolution: {integrity: sha512-4gd7VpWNQNB4UKKCFFVcp1AVv+FMOgs9NKzjHKusc8jTMhd5eL1NqQqOpE0KzMds804/yHlglp3uxgluOqAPLw==} engines: {node: '>= 0.4'} @@ -3142,6 +3219,9 @@ packages: resolution: {integrity: sha512-DDfANUiiG2wC1qawP66qlTugJeL5HyzMpfr8lLK+jMQirGzNod0B12cFB/9q838Ru27sBwfw78/rdoU7RERz6A==} engines: {node: '>= 0.4'} + is-arrayish@0.3.4: + resolution: {integrity: sha512-m6UrgzFVUYawGBh1dUsWR5M2Clqic9RVXC/9f8ceNlv2IcO9j9J/z8UoCLPqtsPBFNzEpfR3xftohbfqDx8EQA==} + is-async-function@2.1.1: resolution: {integrity: sha512-9dgM/cZBnNvjzaMYHVoxxfPj2QXt22Ev7SuuPrs+xav0ukGB0S6d4ydZdEiM48kLx5kDV+QBPrpVnFyefL8kkQ==} engines: {node: '>= 0.4'} @@ -3233,6 +3313,9 @@ packages: resolution: {integrity: sha512-p3EcsicXjit7SaskXHs1hA91QxgTw46Fv6EFKKGS5DRFLD8yKnohjF3hxoju94b/OcMZoQukzpPpBE9uLVKzgQ==} engines: {node: '>= 0.4'} + is-url@1.2.4: + resolution: {integrity: sha512-ITvGim8FhRiYe4IQ5uHSkj7pVaPDrCTkNd3yq3cV7iZAcJdHTUMPMEHcqSOy9xZ9qFenQCvi+2wjH9a1nXqHww==} + is-weakmap@2.0.2: resolution: {integrity: sha512-K5pXYOm9wqY1RgjpL3YTkF39tni1XajUIkawTLUo9EZEVUFga5gSQJF8nNS7ZwJQ02y+1YCNYcMh+HIf1ZqE+w==} engines: {node: '>= 0.4'} @@ -3260,6 +3343,9 @@ packages: peerDependencies: react: ^19.0.0 + jay-peg@1.1.1: + resolution: {integrity: sha512-D62KEuBxz/ip2gQKOEhk/mx14o7eiFRaU+VNNSP4MOiIkwb/D6B3G1Mfas7C/Fit8EsSV2/IWjZElx/Gs6A4ww==} + jiti@2.6.1: resolution: {integrity: sha512-ekilCSN1jwRvIbgeg/57YFh8qQDNbwDb9xT/qu2DAHbFFZUicIl4ygVaAvzveMhMVr3LnpSKTNnwt8PoOfmKhQ==} hasBin: true @@ -3438,6 +3524,9 @@ packages: mdurl@2.0.0: resolution: {integrity: sha512-Lf+9+2r+Tdp5wXDXC4PcIBjTDtq4UKjCPMQhKIuzpJNW0b96kVqSwW0bT7FhRSfmAiFYgP+SCRvdrDozfh0U5w==} + media-engine@1.0.3: + resolution: {integrity: sha512-aa5tG6sDoK+k70B9iEX1NeyfT8ObCKhNDs6lJVpwF6r8vhUfuKMslIcirq6HIUYuuUYLefcEQOn9bSBOvawtwg==} + merge2@1.4.1: resolution: {integrity: sha512-8q7VEgMJW4J8tcfVPy8g09NcQwZdbwFEqhe/WZkoIzjn/3TGDwtOCYtXGxA3O8tPzpczCCDgv+P2P5y00ZJOOg==} engines: {node: '>= 8'} @@ -3520,6 +3609,9 @@ packages: node-releases@2.0.23: resolution: {integrity: sha512-cCmFDMSm26S6tQSDpBCg/NR8NENrVPhAJSf+XbxBG4rPFaaonlEoE9wHQmun+cls499TQGSb7ZyPBRlzgKfpeg==} + normalize-svg-path@1.1.0: + resolution: {integrity: sha512-r9KHKG2UUeB5LoTouwDzBy2VxXlHsiM6fyLQvnJa0S5hrhzqElH/CH7TUGhT1fVvIYBIKf3OpY4YJ4CK+iaqHg==} + nypm@0.6.2: resolution: {integrity: sha512-7eM+hpOtrKrBDCh7Ypu2lJ9Z7PNZBdi/8AT3AX8xoCj43BBVHD0hPSTEvMtkMpfs8FCqBGhxB+uToIQimA111g==} engines: {node: ^14.16.0 || >=16.10.0} @@ -3582,6 +3674,9 @@ packages: pako@0.2.9: resolution: {integrity: sha512-NUcwaKxUxWrZLpDG+z/xZaCgQITkA/Dv4V/T6bw7VON6l1Xz/VnrBqrYjZQ12TamKHzITTfOEIYUj48y2KXImA==} + pako@1.0.11: + resolution: {integrity: sha512-4hLB8Py4zZce5s4yd9XzopqwVv/yGNhV1Bl8NTmCq1763HeK2+EwVTv+leGeL13Dnh2wfbqowVPXCIO0z4taYw==} + parent-module@1.0.1: resolution: {integrity: sha512-GQ2EWRpQV8/o+Aw8YqtfZZPfNRWZYkbidE9k5rpl/hC3vtHHBfGm2Ifi6qWV+coDGkrUKZAxE3Lot5kcsRlh+g==} engines: {node: '>=6'} @@ -3589,6 +3684,9 @@ packages: parse-srcset@1.0.2: resolution: {integrity: sha512-/2qh0lav6CmI15FzA3i/2Bzk2zCgQhGMkvhOhKNcBVQ1ldgpbfiNTVslmooUmWJcADi1f1kIeynbDRVzNlfR6Q==} + parse-svg-path@0.1.2: + resolution: {integrity: sha512-JyPSBnkTJ0AI8GGJLfMXvKq42cj5c006fnLz6fXy6zfoVjJizi8BNTpu8on8ziI1cKy9d9DGNuY17Ce7wuejpQ==} + path-exists@4.0.0: resolution: {integrity: sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==} engines: {node: '>=8'} @@ -3637,6 +3735,9 @@ packages: resolution: {integrity: sha512-/+5VFTchJDoVj3bhoqi6UeymcD00DAwb1nJwamzPvHEszJ4FpF6SNNbUbOS8yI56qHzdV8eK0qEfOSiodkTdxg==} engines: {node: '>= 0.4'} + postcss-value-parser@4.2.0: + resolution: {integrity: sha512-1NNCs6uurfkVbeXG4S8JFT9t19m45ICnif8zWLd5oPSZ50QnwMfK+H3jv408d4jw/7Bttv5axS5IiHoLaVNHeQ==} + postcss@8.4.31: resolution: {integrity: sha512-PS08Iboia9mts/2ygV3eLpY5ghnUcfLV/EXTOW1E2qYxJKGGBUtNjN76FYHnMs36RmARn41bC0AZmn+rR0OVpQ==} engines: {node: ^10 || ^12 || >=14} @@ -3746,6 +3847,9 @@ packages: queue-microtask@1.2.3: resolution: {integrity: sha512-NuaNSa6flKT5JaSYQzJok04JzTL1CA6aGhv5rfLW3PgqA+M2ChpZQnAC8h8i4ZFkBS8X5RqkDBHA7r4hej3K9A==} + queue@6.0.2: + resolution: {integrity: sha512-iHZWu+q3IdFZFX36ro/lKBkSvfkztY5Y7HMiPlOUjhupPcG2JMfst2KKEpu5XndviX/3UhFbRngUPNKtgvtZiA==} + rc9@2.1.2: resolution: {integrity: sha512-btXCnMmRIBINM2LDZoEmOogIZU7Qe7zn4BpomSKZ/ykbLObuBdvG+mFq11DL6fjH1DRwHhrlgtYWG96bJiC7Cg==} @@ -3856,6 +3960,10 @@ packages: resolution: {integrity: sha512-dYqgNSZbDwkaJ2ceRd9ojCGjBq+mOm9LmtXnAnEGyHhN/5R7iDW2TRw3h+o/jCFxus3P2LfWIIiwowAjANm7IA==} engines: {node: '>= 0.4'} + require-from-string@2.0.2: + resolution: {integrity: sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==} + engines: {node: '>=0.10.0'} + resolve-from@4.0.0: resolution: {integrity: sha512-pb/MYmXstAkysRFx8piNI1tGFNQIFA3vkE3Gq4EuA1dF6gHp/+vgZqsCGJapvy8N3Q+4o7FwvquPJcnZ7RYy4g==} engines: {node: '>=4'} @@ -3897,6 +4005,9 @@ packages: resolution: {integrity: sha512-AURm5f0jYEOydBj7VQlVvDrjeFgthDdEF5H1dP+6mNpoXOMo1quQqJ4wvJDyRZ9+pO3kGWoOdmV08cSv2aJV6Q==} engines: {node: '>=0.4'} + safe-buffer@5.2.1: + resolution: {integrity: sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==} + safe-push-apply@1.0.0: resolution: {integrity: sha512-iKE9w/Z7xCzUMIZqdBsp6pEQvwuEebH4vdpjcDWnyzaI6yl6O9FHvVpmGelvEHNsoY6wGblkxR6Zty/h00WiSA==} engines: {node: '>= 0.4'} @@ -3911,6 +4022,9 @@ packages: scheduler@0.25.0: resolution: {integrity: sha512-xFVuu11jh+xcO7JOAGJNOXld8/TcEHK/4CituBUeUb5hqxJLj9YuemAEuvm9gQ/+pgXYfbQuqAkiYu+u7YEsNA==} + scheduler@0.25.0-rc-603e6108-20241029: + resolution: {integrity: sha512-pFwF6H1XrSdYYNLfOcGlM28/j8CGLu8IvdrxqhjWULe2bPcKiKW4CV+OWqR/9fT52mywx65l7ysNkjLKBda7eA==} + scheduler@0.27.0: resolution: {integrity: sha512-eNv+WrVbKu1f3vbYJT/xtiF5syA5HPIMtf9IgY/nKg0sWqzAUEvqY/xm7OcZc/qafLx/iO9FgOmeSAp4v5ti/Q==} @@ -3969,6 +4083,9 @@ packages: siginfo@2.0.0: resolution: {integrity: sha512-ybx0WO1/8bSBLEWXZvEd7gMW3Sn3JFlW3TvX1nREbDLRNQNaeNN8WK0meBwPdAaOI7TtRRRJn/Es1zhrrCHu7g==} + simple-swizzle@0.2.4: + resolution: {integrity: sha512-nAu1WFPQSMNr2Zn9PGSZK9AGn4t/y97lEm+MXTtUDwfP0ksAIX4nO+6ruD9Jwut4C49SB1Ws+fbXsm/yScWOHw==} + sonner@2.0.7: resolution: {integrity: sha512-W6ZN4p58k8aDKA4XPcx2hpIQXBRAgyiWVkYhT7CvK6D3iAu7xjvVyhQHg2/iaKJZ1XVJ4r7XuwGL+WGEK37i9w==} peerDependencies: @@ -4015,6 +4132,9 @@ packages: resolution: {integrity: sha512-UXSH262CSZY1tfu3G3Secr6uGLCFVPMhIqHjlgCUtCCcgihYc/xKs9djMTMUOb2j1mVSeU8EU6NWc/iQKU6Gfg==} engines: {node: '>= 0.4'} + string_decoder@1.3.0: + resolution: {integrity: sha512-hkRX8U1WjJFd8LsDJ2yQ/wWWxaopEsABU1XfkM8A+j0+85JAGppt16cr1Whg6KIbb4okU6Mql6BOj+uup/wKeA==} + strip-bom@3.0.0: resolution: {integrity: sha512-vavAMRXOgBVNF6nyEEmL3DBK19iRpDcoIwW+swQ+CbGiu7lju6t+JklA1MHweoWtadgt4ISVUsXLyDq34ddcwA==} engines: {node: '>=4'} @@ -4049,6 +4169,9 @@ packages: peerDependencies: react: '>=17.0' + svg-arc-to-cubic-bezier@3.2.0: + resolution: {integrity: sha512-djbJ/vZKZO+gPoSDThGNpKDO+o+bAeA4XQKovvkNCqnIS2t+S4qnLAGQhyyrulhCFRl1WWzAp0wUDV8PpTVU3g==} + tailwind-merge@3.3.1: resolution: {integrity: sha512-gBXpgUm/3rp1lMZZrM/w7D8GKqshif0zAymAhbCyIt8KMe+0v9DQ7cdYLR4FHH/cKpdTXb+A/tKKU3eolfsI+g==} @@ -4220,6 +4343,9 @@ packages: peerDependencies: react: ^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 + util-deprecate@1.0.2: + resolution: {integrity: sha512-EPD5q1uXyFxJpCrLnCc1nHnq3gOa6DZBocAIiI2TaSCA7VCJ1UJDMagCzIkXNsUYfD1daK//LTEQ8xiIbrHtcw==} + vaul@1.1.2: resolution: {integrity: sha512-ZFkClGpWyI2WUQjdLJ/BaGuV6AVQiJ3uELGk3OYtP+B6yCO7Cmn9vPFXVJkRaGkOJu3m8bQMgtyzNHixULceQA==} peerDependencies: @@ -4229,6 +4355,10 @@ packages: victory-vendor@36.9.2: resolution: {integrity: sha512-PnpQQMuxlwYdocC8fIJqVXvkeViHYzotI+NJrCuav0ZYFoq912ZHBk3mCeuj+5/VpodOjPe1z0Fk2ihgzlXqjQ==} + vite-compatible-readable-stream@3.6.1: + resolution: {integrity: sha512-t20zYkrSf868+j/p31cRIGN28Phrjm3nRSLR2fyc2tiWi4cZGVdv68yNlwnIINTkMTmPoMiSlc0OadaO7DXZaQ==} + engines: {node: '>= 6'} + vite-node@2.1.9: resolution: {integrity: sha512-AM9aQ/IPrW/6ENLQg3AGY4K1N2TGZdR5e4gu/MmmR2xR3Ll1+dib+nook92g4TV3PXVyeyxdWwtaCAiUL0hMxA==} engines: {node: ^18.0.0 || >=20.0.0} @@ -4374,6 +4504,9 @@ packages: resolution: {integrity: sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==} engines: {node: '>=10'} + yoga-layout@3.2.1: + resolution: {integrity: sha512-0LPOt3AxKqMdFBZA3HBAt/t/8vIKq7VaQYbuA8WxCgung+p9TVyKRYdpvCb80HcdTN2NkbIKbhNwKUfm3tQywQ==} + zod@4.1.11: resolution: {integrity: sha512-WPsqwxITS2tzx1bzhIKsEs19ABD5vmCVa4xBo2tq/SrV4RNZtfws1EnCWQXM6yh8bD08a1idvkB5MZSBiZsjwg==} @@ -5554,6 +5687,107 @@ snapshots: '@radix-ui/rect@1.1.1': {} + '@react-pdf/fns@3.1.2': {} + + '@react-pdf/font@4.0.3': + dependencies: + '@react-pdf/pdfkit': 4.0.4 + '@react-pdf/types': 2.9.1 + fontkit: 2.0.4 + is-url: 1.2.4 + + '@react-pdf/image@3.0.3': + dependencies: + '@react-pdf/png-js': 3.0.0 + jay-peg: 1.1.1 + + '@react-pdf/layout@4.4.1': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/image': 3.0.3 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/stylesheet': 6.1.1 + '@react-pdf/textkit': 6.0.0 + '@react-pdf/types': 2.9.1 + emoji-regex-xs: 1.0.0 + queue: 6.0.2 + yoga-layout: 3.2.1 + + '@react-pdf/pdfkit@4.0.4': + dependencies: + '@babel/runtime': 7.28.4 + '@react-pdf/png-js': 3.0.0 + browserify-zlib: 0.2.0 + crypto-js: 4.2.0 + fontkit: 2.0.4 + jay-peg: 1.1.1 + linebreak: 1.1.0 + vite-compatible-readable-stream: 3.6.1 + + '@react-pdf/png-js@3.0.0': + dependencies: + browserify-zlib: 0.2.0 + + '@react-pdf/primitives@4.1.1': {} + + '@react-pdf/reconciler@1.1.4(react@19.2.0)': + dependencies: + object-assign: 4.1.1 + react: 19.2.0 + scheduler: 0.25.0-rc-603e6108-20241029 + + '@react-pdf/render@4.3.1': + dependencies: + '@babel/runtime': 7.28.4 + '@react-pdf/fns': 3.1.2 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/textkit': 6.0.0 + '@react-pdf/types': 2.9.1 + abs-svg-path: 0.1.1 + color-string: 1.9.1 + normalize-svg-path: 1.1.0 + parse-svg-path: 0.1.2 + svg-arc-to-cubic-bezier: 3.2.0 + + '@react-pdf/renderer@4.3.1(react@19.2.0)': + dependencies: + '@babel/runtime': 7.28.4 + '@react-pdf/fns': 3.1.2 + '@react-pdf/font': 4.0.3 + '@react-pdf/layout': 4.4.1 + '@react-pdf/pdfkit': 4.0.4 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/reconciler': 1.1.4(react@19.2.0) + '@react-pdf/render': 4.3.1 + '@react-pdf/types': 2.9.1 + events: 3.3.0 + object-assign: 4.1.1 + prop-types: 15.8.1 + queue: 6.0.2 + react: 19.2.0 + + '@react-pdf/stylesheet@6.1.1': + dependencies: + '@react-pdf/fns': 3.1.2 + '@react-pdf/types': 2.9.1 + color-string: 1.9.1 + hsl-to-hex: 1.0.0 + media-engine: 1.0.3 + postcss-value-parser: 4.2.0 + + '@react-pdf/textkit@6.0.0': + dependencies: + '@react-pdf/fns': 3.1.2 + bidi-js: 1.0.3 + hyphen: 1.10.6 + unicode-properties: 1.4.1 + + '@react-pdf/types@2.9.1': + dependencies: + '@react-pdf/font': 4.0.3 + '@react-pdf/primitives': 4.1.1 + '@react-pdf/stylesheet': 6.1.1 + '@react-three/fiber@9.3.0(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0)(three@0.180.0)': dependencies: '@babel/runtime': 7.28.4 @@ -6319,6 +6553,8 @@ snapshots: '@webgpu/types@0.1.65': {} + abs-svg-path@0.1.1: {} + acorn-jsx@5.3.2(acorn@8.15.0): dependencies: acorn: 8.15.0 @@ -6467,6 +6703,10 @@ snapshots: set-cookie-parser: 2.7.1 uncrypto: 0.1.3 + bidi-js@1.0.3: + dependencies: + require-from-string: 2.0.2 + brace-expansion@1.1.12: dependencies: balanced-match: 1.0.2 @@ -6484,6 +6724,10 @@ snapshots: dependencies: base64-js: 1.5.1 + browserify-zlib@0.2.0: + dependencies: + pako: 1.0.11 + browserslist@4.26.3: dependencies: baseline-browser-mapping: 2.8.16 @@ -6576,6 +6820,11 @@ snapshots: color-name@1.1.4: {} + color-string@1.9.1: + dependencies: + color-name: 1.1.4 + simple-swizzle: 0.2.4 + concat-map@0.0.1: {} confbox@0.2.2: {} @@ -6746,6 +6995,8 @@ snapshots: electron-to-chromium@1.5.234: {} + emoji-regex-xs@1.0.0: {} + emoji-regex@9.2.2: {} empathic@2.0.0: {} @@ -7121,6 +7372,8 @@ snapshots: eventemitter3@4.0.7: {} + events@3.3.0: {} + expect-type@1.2.2: {} exsolve@1.0.7: {} @@ -7301,6 +7554,12 @@ snapshots: dependencies: function-bind: 1.1.2 + hsl-to-hex@1.0.0: + dependencies: + hsl-to-rgb-for-reals: 1.1.1 + + hsl-to-rgb-for-reals@1.1.1: {} + htmlparser2@8.0.2: dependencies: domelementtype: 2.3.0 @@ -7308,6 +7567,8 @@ snapshots: domutils: 3.2.2 entities: 4.5.0 + hyphen@1.10.6: {} + ieee754@1.2.1: {} ignore@5.3.2: {} @@ -7321,6 +7582,8 @@ snapshots: imurmurhash@0.1.4: {} + inherits@2.0.4: {} + internal-slot@1.1.0: dependencies: es-errors: 1.3.0 @@ -7335,6 +7598,8 @@ snapshots: call-bound: 1.0.4 get-intrinsic: 1.3.0 + is-arrayish@0.3.4: {} + is-async-function@2.1.1: dependencies: async-function: 1.0.0 @@ -7432,6 +7697,8 @@ snapshots: dependencies: which-typed-array: 1.1.19 + is-url@1.2.4: {} + is-weakmap@2.0.2: {} is-weakref@1.1.1: @@ -7463,6 +7730,10 @@ snapshots: transitivePeerDependencies: - '@types/react' + jay-peg@1.1.1: + dependencies: + restructure: 3.0.2 + jiti@2.6.1: {} jose@6.1.0: {} @@ -7610,6 +7881,8 @@ snapshots: mdurl@2.0.0: {} + media-engine@1.0.3: {} + merge2@1.4.1: {} meshoptimizer@0.22.0: {} @@ -7677,6 +7950,10 @@ snapshots: node-releases@2.0.23: {} + normalize-svg-path@1.1.0: + dependencies: + svg-arc-to-cubic-bezier: 3.2.0 + nypm@0.6.2: dependencies: citty: 0.1.6 @@ -7756,12 +8033,16 @@ snapshots: pako@0.2.9: {} + pako@1.0.11: {} + parent-module@1.0.1: dependencies: callsites: 3.1.0 parse-srcset@1.0.2: {} + parse-svg-path@0.1.2: {} + path-exists@4.0.0: {} path-key@3.1.1: {} @@ -7800,6 +8081,8 @@ snapshots: possible-typed-array-names@1.1.0: {} + postcss-value-parser@4.2.0: {} + postcss@8.4.31: dependencies: nanoid: 3.3.11 @@ -7948,6 +8231,10 @@ snapshots: queue-microtask@1.2.3: {} + queue@6.0.2: + dependencies: + inherits: 2.0.4 + rc9@2.1.2: dependencies: defu: 6.1.4 @@ -8066,6 +8353,8 @@ snapshots: gopd: 1.2.0 set-function-name: 2.0.2 + require-from-string@2.0.2: {} + resolve-from@4.0.0: {} resolve-pkg-maps@1.0.0: {} @@ -8130,6 +8419,8 @@ snapshots: has-symbols: 1.1.0 isarray: 2.0.5 + safe-buffer@5.2.1: {} + safe-push-apply@1.0.0: dependencies: es-errors: 1.3.0 @@ -8152,6 +8443,8 @@ snapshots: scheduler@0.25.0: {} + scheduler@0.25.0-rc-603e6108-20241029: {} + scheduler@0.27.0: {} semver@6.3.1: {} @@ -8248,6 +8541,10 @@ snapshots: siginfo@2.0.0: {} + simple-swizzle@0.2.4: + dependencies: + is-arrayish: 0.3.4 + sonner@2.0.7(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: react: 19.2.0 @@ -8316,6 +8613,10 @@ snapshots: define-properties: 1.2.1 es-object-atoms: 1.1.1 + string_decoder@1.3.0: + dependencies: + safe-buffer: 5.2.1 + strip-bom@3.0.0: {} strip-json-comments@3.1.1: {} @@ -8335,6 +8636,8 @@ snapshots: dependencies: react: 19.2.0 + svg-arc-to-cubic-bezier@3.2.0: {} + tailwind-merge@3.3.1: {} tailwindcss@4.1.14: {} @@ -8521,6 +8824,8 @@ snapshots: dependencies: react: 19.2.0 + util-deprecate@1.0.2: {} + vaul@1.1.2(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0): dependencies: '@radix-ui/react-dialog': 1.1.15(@types/react-dom@19.2.0(@types/react@19.2.0))(@types/react@19.2.0)(react-dom@19.2.0(react@19.2.0))(react@19.2.0) @@ -8547,6 +8852,12 @@ snapshots: d3-time: 3.1.0 d3-timer: 3.0.1 + vite-compatible-readable-stream@3.6.1: + dependencies: + inherits: 2.0.4 + string_decoder: 1.3.0 + util-deprecate: 1.0.2 + vite-node@2.1.9(@types/node@20.19.19)(lightningcss@1.30.1): dependencies: cac: 6.7.14 @@ -8684,6 +8995,8 @@ snapshots: yocto-queue@0.1.0: {} + yoga-layout@3.2.1: {} + zod@4.1.11: {} zustand@5.0.8(@types/react@19.2.0)(react@19.2.0)(use-sync-external-store@1.6.0(react@19.2.0)): diff --git a/src/app/api/machines/session/route.ts b/src/app/api/machines/session/route.ts index 11f54a0..a6412d0 100644 --- a/src/app/api/machines/session/route.ts +++ b/src/app/api/machines/session/route.ts @@ -1,4 +1,4 @@ -import { NextResponse } from "next/server" +import { NextRequest, NextResponse } from "next/server" import { cookies } from "next/headers" import { ConvexHttpClient } from "convex/browser" @@ -25,33 +25,55 @@ function decodeMachineCookie(value: string) { } } -export async function GET() { +function encodeMachineCookie(payload: { + machineId: string + persona: string | null + assignedUserId: string | null + assignedUserEmail: string | null + assignedUserName: string | null + assignedUserRole: string | null +}) { + return Buffer.from(JSON.stringify(payload)).toString("base64url") +} + +export async function GET(request: NextRequest) { const session = await assertAuthenticatedSession() if (!session || session.user?.role !== "machine") { return NextResponse.json({ error: "Sessão de máquina não encontrada." }, { status: 403 }) } - const cookieStore = await cookies() - const cookieValue = cookieStore.get(MACHINE_CTX_COOKIE)?.value - if (!cookieValue) { - return NextResponse.json({ error: "Contexto da máquina ausente." }, { status: 404 }) - } - - const decoded = decodeMachineCookie(cookieValue) - if (!decoded?.machineId) { - return NextResponse.json({ error: "Contexto da máquina inválido." }, { status: 400 }) - } - const convexUrl = env.NEXT_PUBLIC_CONVEX_URL if (!convexUrl) { return NextResponse.json({ error: "Convex não configurado." }, { status: 500 }) } const client = new ConvexHttpClient(convexUrl) + const cookieStore = await cookies() + const cookieValue = cookieStore.get(MACHINE_CTX_COOKIE)?.value ?? null + + const decoded = cookieValue ? decodeMachineCookie(cookieValue) : null + let machineId: Id<"machines"> | null = decoded?.machineId ? (decoded.machineId as Id<"machines">) : null + + if (!machineId) { + try { + const lookup = (await client.query(api.machines.findByAuthEmail, { + authEmail: session.user.email.toLowerCase(), + })) as { id: string } | null + + if (!lookup?.id) { + return NextResponse.json({ error: "Máquina não vinculada à sessão atual." }, { status: 404 }) + } + + machineId = lookup.id as Id<"machines"> + } catch (error) { + console.error("[machines.session] Falha ao localizar máquina por e-mail", error) + return NextResponse.json({ error: "Não foi possível localizar a máquina." }, { status: 500 }) + } + } try { const context = (await client.query(api.machines.getContext, { - machineId: decoded.machineId as Id<"machines">, + machineId, })) as { id: string tenantId: string @@ -66,10 +88,32 @@ export async function GET() { authEmail: string | null } - return NextResponse.json({ + const responsePayload = { + machineId: context.id, + persona: context.persona, + assignedUserId: context.assignedUserId, + assignedUserEmail: context.assignedUserEmail, + assignedUserName: context.assignedUserName, + assignedUserRole: context.assignedUserRole, + } + + const response = NextResponse.json({ machine: context, - cookie: decoded, + cookie: responsePayload, }) + + const isSecure = request.nextUrl.protocol === "https:" + response.cookies.set({ + name: MACHINE_CTX_COOKIE, + value: encodeMachineCookie(responsePayload), + httpOnly: true, + sameSite: "lax", + secure: isSecure, + path: "/", + maxAge: 60 * 60 * 24 * 30, + }) + + return response } catch (error) { console.error("[machines.session] Falha ao obter contexto da máquina", error) return NextResponse.json({ error: "Falha ao obter contexto da máquina." }, { status: 500 }) diff --git a/src/app/api/machines/sessions/route.ts b/src/app/api/machines/sessions/route.ts index 756d184..de17f21 100644 --- a/src/app/api/machines/sessions/route.ts +++ b/src/app/api/machines/sessions/route.ts @@ -55,12 +55,13 @@ export async function POST(request: Request) { assignedUserName: session.machine.assignedUserName, assignedUserRole: session.machine.assignedUserRole, } + const isSecure = new URL(request.url).protocol === "https:" response.cookies.set({ name: "machine_ctx", value: Buffer.from(JSON.stringify(machineCookiePayload)).toString("base64url"), httpOnly: true, sameSite: "lax", - secure: true, + secure: isSecure, path: "/", maxAge: 60 * 60 * 24 * 30, }) diff --git a/src/app/api/tickets/[id]/export/pdf/route.ts b/src/app/api/tickets/[id]/export/pdf/route.ts index a27665a..0373455 100644 --- a/src/app/api/tickets/[id]/export/pdf/route.ts +++ b/src/app/api/tickets/[id]/export/pdf/route.ts @@ -1,13 +1,6 @@ -import { NextResponse } from "next/server" -// Use the standalone build to avoid AFM filesystem lookups -// and ensure compatibility in serverless/traced environments. -// eslint-disable-next-line @typescript-eslint/ban-ts-comment -// @ts-ignore – no ambient types for this path; declared in types/ -import PDFDocument from "pdfkit/js/pdfkit.standalone.js" -import { format } from "date-fns" -import fs from "fs" import path from "path" -import { ptBR } from "date-fns/locale" +import fs from "fs" +import { NextResponse } from "next/server" import { ConvexHttpClient } from "convex/browser" import { api } from "@/convex/_generated/api" @@ -16,161 +9,19 @@ import { env } from "@/lib/env" import { assertAuthenticatedSession } from "@/lib/auth-server" import { mapTicketWithDetailsFromServer } from "@/lib/mappers/ticket" import { DEFAULT_TENANT_ID } from "@/lib/constants" -import { TICKET_TIMELINE_LABELS } from "@/lib/ticket-timeline-labels" +import { renderTicketPdfBuffer } from "@/server/pdf/ticket-pdf-template" -// Force Node.js runtime for pdfkit compatibility export const runtime = "nodejs" -const statusLabel: Record = { - PENDING: "Pendente", - AWAITING_ATTENDANCE: "Aguardando atendimento", - PAUSED: "Pausado", - RESOLVED: "Resolvido", -} - -const statusColors: Record = { - PENDING: "#64748B", // slate-500 - AWAITING_ATTENDANCE: "#0EA5E9", // sky-500 - PAUSED: "#F59E0B", // amber-500 - RESOLVED: "#10B981", // emerald-500 -} - -const priorityLabel: Record = { - LOW: "Baixa", - MEDIUM: "Média", - HIGH: "Alta", - URGENT: "Urgente", - CRITICAL: "Crítica", -} - -const channelLabel: Record = { - EMAIL: "E-mail", - PHONE: "Telefone", - CHAT: "Chat", - PORTAL: "Portal", - WEB: "Portal", - API: "API", - SOCIAL: "Redes sociais", - OTHER: "Outro", -} - - -function formatDateTime(date: Date | null | undefined) { - if (!date) return "—" - return format(date, "dd/MM/yyyy HH:mm", { locale: ptBR }) -} - -function htmlToPlainText(html?: string | null) { - if (!html) return "" - const withBreaks = html - .replace(/<\s*br\s*\/?>/gi, "\n") - .replace(/<\/p>/gi, "\n\n") - const stripped = withBreaks.replace(/<[^>]+>/g, "") - return decodeHtmlEntities(stripped).replace(/\u00A0/g, " ").trim() -} - -function decodeHtmlEntities(input: string) { - return input - .replace(/&/g, "&") - .replace(/</g, "<") - .replace(/>/g, ">") - .replace(/"/g, '"') - .replace(/'/g, "'") - .replace(/ /g, " ") -} - -function stringifyPayload(payload: unknown): string | null { - if (!payload) return null - if (typeof payload === "object") { - if (Array.isArray(payload)) { - if (payload.length === 0) return null - } else if (payload) { - if (Object.keys(payload as Record).length === 0) return null - } - } - if (typeof payload === "string" && payload.trim() === "") return null +async function readLogoAsDataUrl() { + const logoPath = path.join(process.cwd(), "public", "raven.png") try { - return JSON.stringify(payload, null, 2) - } catch { - return String(payload) - } -} - -function formatDurationMs(ms: number | null | undefined) { - if (!ms || ms <= 0) return null - const totalSeconds = Math.floor(ms / 1000) - const hours = Math.floor(totalSeconds / 3600) - const minutes = Math.floor((totalSeconds % 3600) / 60) - const seconds = totalSeconds % 60 - if (hours > 0) return `${hours}h ${String(minutes).padStart(2, "0")}m` - if (minutes > 0) return `${minutes}m ${String(seconds).padStart(2, "0")}s` - return `${seconds}s` -} - -function buildTimelineMessage(type: string, payload: Record | null | undefined): string | null { - const p = payload ?? {} - const to = (p.toLabel as string | undefined) ?? (p.to as string | undefined) - const assignee = (p.assigneeName as string | undefined) ?? (p.assigneeId as string | undefined) - const queue = (p.queueName as string | undefined) ?? (p.queueId as string | undefined) - const requester = p.requesterName as string | undefined - const author = (p.authorName as string | undefined) ?? (p.authorId as string | undefined) - const actor = (p.actorName as string | undefined) ?? (p.actorId as string | undefined) - const attachmentName = p.attachmentName as string | undefined - const subjectTo = p.to as string | undefined - const pauseReason = (p.pauseReasonLabel as string | undefined) ?? (p.pauseReason as string | undefined) - const pauseNote = p.pauseNote as string | undefined - const sessionDuration = formatDurationMs((p.sessionDurationMs as number | undefined) ?? null) - const categoryName = p.categoryName as string | undefined - const subcategoryName = p.subcategoryName as string | undefined - - switch (type) { - case "STATUS_CHANGED": - return to ? `Status alterado para ${to}` : "Status alterado" - case "ASSIGNEE_CHANGED": - return assignee ? `Responsável alterado para ${assignee}` : "Responsável alterado" - case "QUEUE_CHANGED": - return queue ? `Fila alterada para ${queue}` : "Fila alterada" - case "PRIORITY_CHANGED": - return to ? `Prioridade alterada para ${to}` : "Prioridade alterada" - case "CREATED": - return requester ? `Criado por ${requester}` : "Criado" - case "COMMENT_ADDED": - return author ? `Comentário adicionado por ${author}` : "Comentário adicionado" - case "COMMENT_EDITED": { - const who = actor ?? author - return who ? `Comentário editado por ${who}` : "Comentário editado" - } - case "SUBJECT_CHANGED": - return subjectTo ? `Assunto alterado para "${subjectTo}"` : "Assunto alterado" - case "SUMMARY_CHANGED": - return "Resumo atualizado" - case "ATTACHMENT_REMOVED": - return attachmentName ? `Anexo removido: ${attachmentName}` : "Anexo removido" - case "WORK_PAUSED": { - const parts: string[] = [] - if (pauseReason) parts.push(`Motivo: ${pauseReason}`) - if (sessionDuration) parts.push(`Tempo registrado: ${sessionDuration}`) - if (pauseNote) parts.push(`Observação: ${pauseNote}`) - return parts.length > 0 ? parts.join(" • ") : "Atendimento pausado" - } - case "WORK_STARTED": - return "Atendimento iniciado" - case "CATEGORY_CHANGED": { - if (categoryName || subcategoryName) { - return `Categoria alterada para ${categoryName ?? ""}${subcategoryName ? ` • ${subcategoryName}` : ""}`.trim() - } - return "Categoria removida" - } - case "MANAGER_NOTIFIED": - return "Gestor notificado" - case "VISIT_SCHEDULED": - return "Visita agendada" - case "CSAT_RECEIVED": - return "CSAT recebido" - case "CSAT_RATED": - return "CSAT avaliado" - default: - return null + const buffer = await fs.promises.readFile(logoPath) + const base64 = buffer.toString("base64") + return `data:image/png;base64,${base64}` + } catch (error) { + console.warn("[tickets.export.pdf] Logo não encontrado, seguindo sem imagem", error) + return null } } @@ -188,6 +39,7 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st const client = new ConvexHttpClient(convexUrl) const tenantId = session.user.tenantId ?? DEFAULT_TENANT_ID + let viewerId: string | null = null try { const ensuredUser = await client.mutation(api.users.ensureUser, { @@ -199,8 +51,8 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st }) viewerId = ensuredUser?._id ?? null } catch (error) { - console.error("Failed to synchronize user with Convex for PDF export", error) - return NextResponse.json({ error: "Falha ao sincronizar usuário com Convex" }, { status: 500 }) + console.error("[tickets.export.pdf] Falha ao sincronizar usuário no Convex", error) + return NextResponse.json({ error: "Não foi possível preparar a exportação" }, { status: 500 }) } if (!viewerId) { @@ -215,12 +67,8 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st viewerId: viewerId as unknown as Id<"users">, }) } catch (error) { - console.error("Failed to load ticket from Convex for PDF export", error, { - tenantId, - ticketId, - viewerId, - }) - return NextResponse.json({ error: "Falha ao carregar ticket no Convex" }, { status: 500 }) + console.error("[tickets.export.pdf] Falha ao carregar ticket", error, { tenantId, ticketId, viewerId }) + return NextResponse.json({ error: "Não foi possível carregar o ticket" }, { status: 500 }) } if (!ticketRaw) { @@ -228,284 +76,20 @@ export async function GET(_request: Request, context: { params: Promise<{ id: st } const ticket = mapTicketWithDetailsFromServer(ticketRaw) - const doc = new PDFDocument({ size: "A4", margin: 56 }) - const chunks: Buffer[] = [] - doc.on("data", (chunk: unknown) => { - const buf = typeof chunk === "string" ? Buffer.from(chunk) : (chunk as Buffer) - chunks.push(buf) - }) - - const pdfBufferPromise = new Promise((resolve, reject) => { - doc.on("end", () => resolve(Buffer.concat(chunks))) - doc.on("error", reject) - }) - - // Register custom fonts (Inter) if available + const logoDataUrl = await readLogoAsDataUrl() try { - const pubRegular = path.join(process.cwd(), "public", "fonts", "Inter-Regular.ttf") - const pubBold = path.join(process.cwd(), "public", "fonts", "Inter-Bold.ttf") - const fontRegular = fs.existsSync(pubRegular) - ? pubRegular - : path.join(process.cwd(), "Inter,Manrope", "Inter", "static", "Inter_24pt-Regular.ttf") - const fontBold = fs.existsSync(pubBold) - ? pubBold - : path.join(process.cwd(), "Inter,Manrope", "Inter", "static", "Inter_24pt-Bold.ttf") - const D = doc as unknown as { - registerFont?: (name: string, src: string) => void - _fontFamilies?: Record - roundedRect?: (x: number, y: number, w: number, h: number, r: number) => void - } - if (fs.existsSync(fontRegular)) { - D.registerFont?.("Inter", fontRegular) - } - if (fs.existsSync(fontBold)) { - D.registerFont?.("Inter-Bold", fontBold) - } - } catch {} - - const D = doc as unknown as { _fontFamilies?: Record; roundedRect?: (x:number,y:number,w:number,h:number,r:number)=>void } - const hasInter = Boolean(D._fontFamilies && (D._fontFamilies as Record)["Inter-Bold"]) - - // Header with logo and brand bar - try { - const logoPath = path.join(process.cwd(), "public", "raven.png") - if (fs.existsSync(logoPath)) { - doc.image(logoPath, doc.page.margins.left, doc.y, { width: 120 }) - } - } catch {} - doc.moveDown(0.5) - doc - .fillColor("#00e8ff") - .rect(doc.page.margins.left, doc.y, doc.page.width - doc.page.margins.left - doc.page.margins.right, 3) - .fill() - doc.moveDown(0.5) - - // Título - doc.fillColor("#0F172A").font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(18).text(`Ticket #${ticket.reference} — ${ticket.subject}`) - doc.moveDown(0.25) - // Linha abaixo do título - doc - .strokeColor("#E2E8F0") - .moveTo(doc.page.margins.left, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - - // Badge de status - doc.moveDown(0.5) - const statusText = statusLabel[ticket.status] ?? ticket.status - const badgeColor = statusColors[ticket.status] ?? "#475569" - const badgeFontSize = 10 - const badgePaddingX = 6 - const badgePaddingY = 3 - const badgeX = doc.page.margins.left - const badgeY = doc.y - doc.save() - doc.font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(badgeFontSize) - const badgeTextWidth = doc.widthOfString(statusText) - const badgeHeight = badgeFontSize + badgePaddingY * 2 - const badgeWidth = badgeTextWidth + badgePaddingX * 2 - if (typeof D.roundedRect === "function") { - D.roundedRect(badgeX, badgeY, badgeWidth, badgeHeight, 4) - } else { - doc.rect(badgeX, badgeY, badgeWidth, badgeHeight) - } - doc.fill(badgeColor) - doc.fillColor("#FFFFFF").text(statusText, badgeX + badgePaddingX, badgeY + badgePaddingY) - doc.restore() - doc.y = badgeY + badgeHeight + 8 - - // Metadados em duas colunas - const leftX = doc.page.margins.left - const colGap = 24 - const colWidth = (doc.page.width - doc.page.margins.left - doc.page.margins.right - colGap) / 2 - const rightX = leftX + colWidth + colGap - // const startY = doc.y - const drawMeta = (x: number, lines: string[]) => { - doc.save() - doc.x = x - doc.fillColor("#0F172A").font(hasInter ? "Inter" : "Helvetica").fontSize(11) - for (const line of lines) { - doc.text(line, { width: colWidth, lineGap: 2 }) - } - const currY = doc.y - doc.restore() - return currY - } - const leftLines = [ - `Status: ${statusText}`, - `Prioridade: ${priorityLabel[ticket.priority] ?? ticket.priority}`, - `Canal: ${channelLabel[ticket.channel] ?? ticket.channel ?? "—"}`, - `Fila: ${ticket.queue ?? "—"}`, - ] - const rightLines = [ - `Solicitante: ${ticket.requester.name} (${ticket.requester.email})`, - `Responsável: ${ticket.assignee ? `${ticket.assignee.name} (${ticket.assignee.email})` : "Não atribuído"}`, - `Criado em: ${formatDateTime(ticket.createdAt)}`, - `Atualizado em: ${formatDateTime(ticket.updatedAt)}`, - ] - const leftY = drawMeta(leftX, leftLines) - const rightY = drawMeta(rightX, rightLines) - doc.y = Math.max(leftY, rightY) - doc.moveDown(0.5) - - doc.moveDown(0.75) - doc - .font(hasInter ? "Inter-Bold" : "Helvetica-Bold") - .fontSize(12) - .text("Solicitante") - doc - .strokeColor("#E2E8F0") - .moveTo(doc.page.margins.left, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - doc.moveDown(0.3) - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(11) - .text(`${ticket.requester.name} (${ticket.requester.email})`) - - doc.moveDown(0.5) - doc.font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(12).text("Responsável") - doc - .strokeColor("#E2E8F0") - .moveTo(doc.page.margins.left, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - doc.moveDown(0.3) - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(11) - .text(ticket.assignee ? `${ticket.assignee.name} (${ticket.assignee.email})` : "Não atribuído") - - doc.moveDown(0.75) - doc.font("Helvetica-Bold").fontSize(12).text("Datas") - doc - .strokeColor("#E2E8F0") - .moveTo(doc.page.margins.left, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - doc.moveDown(0.3) - doc - .font("Helvetica") - .fontSize(11) - .text(`Criado em: ${formatDateTime(ticket.createdAt)}`) - .moveDown(0.15) - .text(`Atualizado em: ${formatDateTime(ticket.updatedAt)}`) - .moveDown(0.15) - .text(`Resolvido em: ${formatDateTime(ticket.resolvedAt ?? null)}`) - - if (ticket.summary) { - doc.moveDown(0.75) - doc.font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(12).text("Resumo") - doc - .strokeColor("#E2E8F0") - .moveTo(doc.page.margins.left, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(11) - .text(ticket.summary, { align: "justify", lineGap: 2 }) - } - - if (ticket.description) { - doc.moveDown(0.75) - doc.font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(12).text("Descrição") - doc - .strokeColor("#E2E8F0") - .moveTo(doc.page.margins.left, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(11) - .text(htmlToPlainText(ticket.description), { align: "justify", lineGap: 2 }) - } - - if (ticket.comments.length > 0) { - doc.addPage() - doc.font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(14).text("Comentários") - doc.moveDown(0.6) - const commentsSorted = [...ticket.comments].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - commentsSorted.forEach((comment, index) => { - const visibility = - comment.visibility === "PUBLIC" ? "Público" : "Interno" - doc - .font(hasInter ? "Inter-Bold" : "Helvetica-Bold") - .fontSize(11) - .text(`${comment.author.name} • ${visibility} • ${formatDateTime(comment.createdAt)}`) - doc.moveDown(0.15) - const body = htmlToPlainText(comment.body) - if (body) { - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(11) - .text(body, { align: "justify", lineGap: 2, indent: 6 }) - } - if (comment.attachments.length > 0) { - doc.moveDown(0.25) - doc.font(hasInter ? "Inter" : "Helvetica").fontSize(10).text("Anexos:") - comment.attachments.forEach((attachment) => { - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(10) - .text(`• ${attachment.name ?? attachment.id}`, { indent: 16, lineGap: 1 }) - }) - } - if (index < commentsSorted.length - 1) { - doc.moveDown(1) - doc - .strokeColor("#E2E8F0") - .moveTo(doc.x, doc.y) - .lineTo(doc.page.width - doc.page.margins.right, doc.y) - .stroke() - doc.moveDown(0.9) - } + const pdfBuffer = await renderTicketPdfBuffer({ ticket, logoDataUrl }) + const payload = pdfBuffer instanceof Uint8Array ? pdfBuffer : new Uint8Array(pdfBuffer) + return new NextResponse(payload as unknown as BodyInit, { + headers: { + "Content-Type": "application/pdf", + "Content-Disposition": `attachment; filename="ticket-${ticket.reference}.pdf"`, + "Cache-Control": "no-store", + }, }) + } catch (error) { + console.error("[tickets.export.pdf] Falha ao renderizar PDF", error) + return NextResponse.json({ error: "Não foi possível gerar o PDF" }, { status: 500 }) } - - if (ticket.timeline.length > 0) { - doc.addPage() - doc.font(hasInter ? "Inter-Bold" : "Helvetica-Bold").fontSize(14).text("Linha do tempo") - doc.moveDown(0.6) - const timelineSorted = [...ticket.timeline].sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime()) - timelineSorted.forEach((event) => { - const label = TICKET_TIMELINE_LABELS[event.type] ?? event.type - doc - .font(hasInter ? "Inter-Bold" : "Helvetica-Bold") - .fontSize(11) - .text(`${label} • ${formatDateTime(event.createdAt)}`) - doc.moveDown(0.15) - - const friendly = buildTimelineMessage(event.type, event.payload) - if (friendly) { - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(10) - .text(friendly, { indent: 16, lineGap: 1 }) - } else { - const payloadText = stringifyPayload(event.payload) - if (payloadText) { - doc - .font(hasInter ? "Inter" : "Helvetica") - .fontSize(10) - .text(payloadText, { indent: 16, lineGap: 1 }) - } - } - doc.moveDown(0.7) - }) - } - - doc.end() - const pdfBuffer = await pdfBufferPromise - const pdfBytes = new Uint8Array(pdfBuffer) - - return new NextResponse(pdfBytes, { - headers: { - "Content-Type": "application/pdf", - "Content-Disposition": `attachment; filename="ticket-${ticket.reference}.pdf"`, - "Cache-Control": "no-store", - }, - }) } diff --git a/src/app/icon.png b/src/app/icon.png new file mode 100644 index 0000000000000000000000000000000000000000..b1c7cf7ec36e98c6fa1fd0faddcd7b66f8a2a561 GIT binary patch literal 50823 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 literal 0 HcmV?d00001 diff --git a/src/app/layout.tsx b/src/app/layout.tsx index a088d2c..9933fe9 100644 --- a/src/app/layout.tsx +++ b/src/app/layout.tsx @@ -18,10 +18,10 @@ const jetBrainsMono = JetBrains_Mono({ }) export const metadata: Metadata = { - title: "Raven", + title: "Raven - Sistema de chamados", description: "Plataforma Raven da Rever", icons: { - icon: "/raven.png", + icon: "/icon.png", }, } diff --git a/src/app/machines/handshake/route.ts b/src/app/machines/handshake/route.ts index 3185228..7d2bcb6 100644 --- a/src/app/machines/handshake/route.ts +++ b/src/app/machines/handshake/route.ts @@ -67,12 +67,13 @@ export async function GET(request: NextRequest) { assignedUserRole: session.machine.assignedUserRole, } const encodedContext = Buffer.from(JSON.stringify(machineCookiePayload)).toString("base64url") + const isSecure = redirectUrl.protocol === "https:" response.cookies.set({ name: "machine_ctx", value: encodedContext, httpOnly: true, sameSite: "lax", - secure: true, + secure: isSecure, path: "/", maxAge: 60 * 60 * 24 * 30, }) diff --git a/src/app/tickets/resolved/page.tsx b/src/app/tickets/resolved/page.tsx new file mode 100644 index 0000000..83c275a --- /dev/null +++ b/src/app/tickets/resolved/page.tsx @@ -0,0 +1,5 @@ +import { TicketsResolvedPageClient } from "./tickets-resolved-page-client" + +export default function TicketsResolvedPage() { + return +} diff --git a/src/app/tickets/resolved/tickets-resolved-page-client.tsx b/src/app/tickets/resolved/tickets-resolved-page-client.tsx new file mode 100644 index 0000000..295d3bb --- /dev/null +++ b/src/app/tickets/resolved/tickets-resolved-page-client.tsx @@ -0,0 +1,31 @@ +"use client" + +import dynamic from "next/dynamic" + +import { AppShell } from "@/components/app-shell" +import { SiteHeader } from "@/components/site-header" + +const TicketsView = dynamic( + () => + import("@/components/tickets/tickets-view").then((module) => ({ + default: module.TicketsView, + })), + { ssr: false } +) + +export function TicketsResolvedPageClient() { + return ( + + } + > +
+ +
+
+ ) +} diff --git a/src/components/admin/admin-users-manager.tsx b/src/components/admin/admin-users-manager.tsx index 883989f..32c8ee2 100644 --- a/src/components/admin/admin-users-manager.tsx +++ b/src/components/admin/admin-users-manager.tsx @@ -55,6 +55,25 @@ type Props = { defaultTenantId: string } +const ROLE_LABELS: Record = { + admin: "Administrador", + manager: "Gestor", + agent: "Agente", + collaborator: "Colaborador", + machine: "Agente de máquina", +} + +function formatRole(role: string) { + const key = role?.toLowerCase?.() ?? "" + return ROLE_LABELS[key] ?? role +} + +function formatTenantLabel(tenantId: string, defaultTenantId: string) { + if (!tenantId) return "Principal" + if (tenantId === defaultTenantId) return "Principal" + return tenantId +} + function formatDate(dateIso: string) { const date = new Date(dateIso) return new Intl.DateTimeFormat("pt-BR", { @@ -206,8 +225,6 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d Convites Usuários - Filas - Categorias @@ -255,25 +272,23 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d {normalizedRoles.map((item) => ( - {item === "admin" - ? "Administrador" - : item === "manager" - ? "Gestor" - : item === "agent" - ? "Agente" - : "Colaborador"} + {formatRole(item)} ))}
- + setTenantId(event.target.value)} + placeholder="ex.: principal" /> +

+ Use este campo apenas se trabalhar com múltiplos espaços de clientes. Caso contrário, mantenha o valor padrão. +

@@ -377,7 +392,7 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d Colaborador Papel - Tenant + Espaço Expira em Status Ações @@ -392,8 +407,8 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d {invite.email}
- {invite.role} - {invite.tenantId} + {formatRole(invite.role)} + {formatTenantLabel(invite.tenantId, defaultTenantId)} {formatDate(invite.expiresAt)} Nome E-mail Papel - Tenant + Espaço Criado em @@ -458,8 +473,8 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d {user.name || "—"} {user.email} - {user.role} - {user.tenantId} + {formatRole(user.role)} + {formatTenantLabel(user.tenantId, defaultTenantId)} {formatDate(user.createdAt)} ))} @@ -476,27 +491,6 @@ export function AdminUsersManager({ initialUsers, initialInvites, roleOptions, d
- - - - Gestão de filas - - Em breve será possível criar e reordenar as filas utilizadas na triagem dos tickets. - - - - - - - - - Gestão de categorias - - Estamos preparando o painel completo para organizar categorias e subcategorias do catálogo. - - - - ) } diff --git a/src/components/app-sidebar.tsx b/src/components/app-sidebar.tsx index a9eccdd..2037dc5 100644 --- a/src/components/app-sidebar.tsx +++ b/src/components/app-sidebar.tsx @@ -18,11 +18,12 @@ import { Layers3, UserPlus, BellRing, + ChevronDown, } from "lucide-react" -import { usePathname } from "next/navigation" - -import { SearchForm } from "@/components/search-form" -import { VersionSwitcher } from "@/components/version-switcher" +import { usePathname } from "next/navigation" + +import { SearchForm } from "@/components/search-form" +import { VersionSwitcher } from "@/components/version-switcher" import { Sidebar, SidebarContent, @@ -39,6 +40,7 @@ import { import { Skeleton } from "@/components/ui/skeleton" import { NavUser } from "@/components/nav-user" import { useAuth } from "@/lib/auth-client" +import { cn } from "@/lib/utils" import type { LucideIcon } from "lucide-react" @@ -47,9 +49,10 @@ type NavRoleRequirement = "staff" | "admin" type NavigationItem = { title: string url: string - icon: LucideIcon + icon?: LucideIcon requiredRole?: NavRoleRequirement exact?: boolean + children?: NavigationItem[] } type NavigationGroup = { @@ -65,7 +68,13 @@ const navigation: { versions: string[]; navMain: NavigationGroup[] } = { title: "Operação", items: [ { title: "Dashboard", url: "/dashboard", icon: LayoutDashboard, requiredRole: "staff" }, - { title: "Tickets", url: "/tickets", icon: Ticket, requiredRole: "staff" }, + { + title: "Tickets", + url: "/tickets", + icon: Ticket, + requiredRole: "staff", + children: [{ title: "Resolvidos", url: "/tickets/resolved", requiredRole: "staff" }], + }, { title: "Visualizações", url: "/views", icon: PanelsTopLeft, requiredRole: "staff" }, { title: "Modo Play", url: "/play", icon: PlayCircle, requiredRole: "staff" }, ], @@ -105,9 +114,34 @@ const navigation: { versions: string[]; navMain: NavigationGroup[] } = { } export function AppSidebar({ ...props }: React.ComponentProps) { - const pathname = usePathname() + const pathname = usePathname() const { session, isLoading, isAdmin, isStaff } = useAuth() const [isHydrated, setIsHydrated] = React.useState(false) + const initialExpanded = React.useMemo(() => { + const open = new Set() + navigation.navMain.forEach((group) => { + group.items.forEach((item) => { + if (!item.children || item.children.length === 0) return + const shouldOpen = item.children.some((child) => { + if (!canAccess(child.requiredRole)) return false + return pathname === child.url || pathname.startsWith(`${child.url}/`) + }) + if (shouldOpen) { + open.add(item.title) + } + }) + }) + return open + }, [pathname]) + const [expanded, setExpanded] = React.useState>(initialExpanded) + + React.useEffect(() => { + setExpanded((prev) => { + const next = new Set(prev) + initialExpanded.forEach((key) => next.add(key)) + return next + }) + }, [initialExpanded]) React.useEffect(() => { setIsHydrated(true) @@ -131,6 +165,18 @@ export function AppSidebar({ ...props }: React.ComponentProps) { if (requiredRole === "staff") return isStaff return false } + + const toggleExpanded = React.useCallback((title: string) => { + setExpanded((prev) => { + const next = new Set(prev) + if (next.has(title)) { + next.delete(title) + } else { + next.add(title) + } + return next + }) + }, []) if (!isHydrated) { return ( @@ -180,16 +226,64 @@ export function AppSidebar({ ...props }: React.ComponentProps) { {group.title} - {visibleItems.map((item) => ( - - - - - {item.title} - - - - ))} + {visibleItems.map((item) => { + if (item.children && item.children.length > 0) { + const childItems = item.children.filter((child) => canAccess(child.requiredRole)) + const isExpanded = expanded.has(item.title) + const isChildActive = childItems.some((child) => isActive(child)) + const parentActive = isActive(item) || isChildActive + + return ( + + + + + {item.icon ? : null} + {item.title} + { + event.preventDefault() + event.stopPropagation() + toggleExpanded(item.title) + }} + className={cn( + "absolute right-1.5 top-1/2 inline-flex h-6 w-6 -translate-y-1/2 items-center justify-center rounded-md text-neutral-500 transition hover:bg-slate-200 hover:text-neutral-700", + isExpanded && "rotate-180" + )} + > + + + + + + {isExpanded + ? childItems.map((child) => ( + + + + {child.title} + + + + )) + : null} + + ) + } + + return ( + + + + {item.icon ? : null} + {item.title} + + + + ) + })} diff --git a/src/components/portal/portal-shell.tsx b/src/components/portal/portal-shell.tsx index 3c3d28e..db0a04f 100644 --- a/src/components/portal/portal-shell.tsx +++ b/src/components/portal/portal-shell.tsx @@ -26,9 +26,11 @@ export function PortalShell({ children }: PortalShellProps) { const { session, machineContext } = useAuth() const [isSigningOut, setIsSigningOut] = useState(false) + const isMachineSession = session?.user.role === "machine" + const personaValue = machineContext?.persona ?? session?.user.machinePersona ?? null const displayName = machineContext?.assignedUserName ?? session?.user.name ?? session?.user.email ?? "Cliente" const displayEmail = machineContext?.assignedUserEmail ?? session?.user.email ?? "" - const personaLabel = machineContext?.persona === "manager" ? "Gestor" : "Colaborador" + const personaLabel = personaValue === "manager" ? "Gestor" : "Colaborador" const initials = useMemo(() => { const name = displayName || displayEmail || "Cliente" @@ -64,7 +66,7 @@ export function PortalShell({ children }: PortalShellProps) {
- + Portal do cliente Raven @@ -100,12 +102,12 @@ export function PortalShell({ children }: PortalShellProps) {
{displayName} {displayEmail} - {machineContext ? ( + {personaValue ? ( {personaLabel} ) : null}
- {!machineContext ? ( + {!isMachineSession ? ( + ))} +
+ )} +

+ Use {"{{cliente}}"} dentro do template para inserir automaticamente o nome do solicitante. +

+ +
+

Mensagem de encerramento

+ +

Você pode editar o conteúdo antes de enviar. Deixe em branco para encerrar sem comentário adicional.

+
+ + +
+ O comentário será público e ficará registrado no histórico do ticket. +
+
+ + +
+
+ + ) } diff --git a/src/components/tickets/ticket-comments.rich.tsx b/src/components/tickets/ticket-comments.rich.tsx index 352977a..c185fb8 100644 --- a/src/components/tickets/ticket-comments.rich.tsx +++ b/src/components/tickets/ticket-comments.rich.tsx @@ -51,7 +51,7 @@ export function TicketComments({ ticket }: TicketCommentsProps) { const [localBodies, setLocalBodies] = useState>({}) const templateArgs = convexUserId && isStaff - ? { tenantId: ticket.tenantId, viewerId: convexUserId as Id<"users"> } + ? { tenantId: ticket.tenantId, viewerId: convexUserId as Id<"users">, kind: "comment" as const } : "skip" const templatesResult = useQuery(convexUserId && isStaff ? api.commentTemplates.list : "skip", templateArgs) as | { id: string; title: string; body: string }[] diff --git a/src/components/tickets/ticket-summary-header.tsx b/src/components/tickets/ticket-summary-header.tsx index 8bf339f..5556fbb 100644 --- a/src/components/tickets/ticket-summary-header.tsx +++ b/src/components/tickets/ticket-summary-header.tsx @@ -499,7 +499,12 @@ export function TicketSummaryHeader({ ticket }: TicketHeaderProps) { ) : null} - + {isPlaying ? (