import { z } from "zod"; import { ticketSchema, ticketStatusSchema, ticketWithDetailsSchema } from "@/lib/schemas/ticket"; type NormalizedTicketStatus = z.infer; const STATUS_MAP: Record = { NEW: "PENDING", PENDING: "PENDING", OPEN: "AWAITING_ATTENDANCE", AWAITING_ATTENDANCE: "AWAITING_ATTENDANCE", ON_HOLD: "PAUSED", PAUSED: "PAUSED", RESOLVED: "RESOLVED", CLOSED: "RESOLVED", }; function normalizeTicketStatus(status: unknown): NormalizedTicketStatus { if (typeof status !== "string") return "PENDING"; const normalized = STATUS_MAP[status.toUpperCase()]; return normalized ?? "PENDING"; } const DATE_ONLY_REGEX = /^\d{4}-\d{2}-\d{2}$/; function normalizeCustomFieldDateValue(raw: unknown): string | null { if (raw === null || raw === undefined) { return null; } if (typeof raw === "string") { const trimmed = raw.trim(); if (!trimmed) return null; if (DATE_ONLY_REGEX.test(trimmed)) { return trimmed; } const parsed = new Date(trimmed); if (Number.isNaN(parsed.getTime())) { return null; } return parsed.toISOString().slice(0, 10); } const date = raw instanceof Date ? raw : typeof raw === "number" ? new Date(raw) : null; if (!date || Number.isNaN(date.getTime())) { return null; } return date.toISOString().slice(0, 10); } // Server shapes: datas como number (epoch ms) e alguns nullables // Relaxamos email/urls no shape do servidor para evitar que payloads parciais quebrem o app. const serverUserSchema = z.object({ id: z.string(), name: z.string(), email: z.string().optional(), avatarUrl: z.string().optional(), teams: z.array(z.string()).optional(), }); const serverMachineSummarySchema = z.object({ id: z.string().nullable().optional(), hostname: z.string().nullable().optional(), persona: z.string().nullable().optional(), assignedUserName: z.string().nullable().optional(), assignedUserEmail: z.string().nullable().optional(), status: z.string().nullable().optional(), }); const serverTicketSchema = z.object({ id: z.string(), reference: z.number(), tenantId: z.string(), subject: z.string(), summary: z.string().optional().nullable(), status: z.string(), priority: z.string(), channel: z.string(), queue: z.string().nullable(), formTemplate: z.string().nullable().optional(), formTemplateLabel: z.string().nullable().optional(), checklist: z .array( z.object({ id: z.string(), text: z.string(), description: z.string().optional(), type: z.enum(["checkbox", "question"]).optional(), options: z.array(z.string()).optional(), answer: z.string().optional(), done: z.boolean(), required: z.boolean().optional(), templateId: z.string().optional(), templateItemId: z.string().optional(), templateDescription: z.string().optional(), createdAt: z.number().optional(), createdBy: z.string().optional(), doneAt: z.number().optional(), doneBy: z.string().optional(), }), ) .optional(), requester: serverUserSchema, assignee: serverUserSchema.nullable(), company: z .object({ id: z.string(), name: z.string(), isAvulso: z.boolean().optional() }) .optional() .nullable(), machine: serverMachineSummarySchema.optional().nullable(), slaPolicy: z.any().nullable().optional(), slaSnapshot: z .object({ categoryId: z.any().optional(), categoryName: z.string().optional(), priority: z.string().optional(), responseTargetMinutes: z.number().optional().nullable(), responseMode: z.string().optional(), solutionTargetMinutes: z.number().optional().nullable(), solutionMode: z.string().optional(), alertThreshold: z.number().optional(), pauseStatuses: z.array(z.string()).optional(), }) .nullable() .optional(), slaResponseDueAt: z.number().nullable().optional(), slaSolutionDueAt: z.number().nullable().optional(), slaResponseStatus: z.string().nullable().optional(), slaSolutionStatus: z.string().nullable().optional(), slaPausedAt: z.number().nullable().optional(), slaPausedBy: z.string().nullable().optional(), slaPausedMs: z.number().nullable().optional(), dueAt: z.number().nullable().optional(), visitStatus: z.string().nullable().optional(), visitPerformedAt: z.number().nullable().optional(), firstResponseAt: z.number().nullable().optional(), resolvedAt: z.number().nullable().optional(), closedAt: z.number().nullable().optional(), reopenDeadline: z.number().nullable().optional(), reopenWindowDays: z.number().nullable().optional(), reopenedAt: z.number().nullable().optional(), updatedAt: z.number(), createdAt: z.number(), tags: z.array(z.string()).default([]).optional(), lastTimelineEntry: z.string().nullable().optional(), metrics: z.any().nullable().optional(), csatScore: z.number().nullable().optional(), csatMaxScore: z.number().nullable().optional(), csatComment: z.string().nullable().optional(), csatRatedAt: z.number().nullable().optional(), csatRatedBy: z.string().nullable().optional(), category: z .object({ id: z.string(), name: z.string(), }) .nullable() .optional(), subcategory: z .object({ id: z.string(), name: z.string(), categoryId: z.string().optional(), }) .nullable() .optional(), workSummary: z .object({ totalWorkedMs: z.number(), internalWorkedMs: z.number().optional(), externalWorkedMs: z.number().optional(), serverNow: z.number().optional(), activeSession: z .object({ id: z.string(), agentId: z.string(), startedAt: z.number(), }) .nullable(), perAgentTotals: z .array( z.object({ agentId: z.string(), agentName: z.string().nullable().optional(), agentEmail: z.string().nullable().optional(), avatarUrl: z.string().nullable().optional(), totalWorkedMs: z.number(), internalWorkedMs: z.number().optional(), externalWorkedMs: z.number().optional(), }), ) .optional(), }) .nullable() .optional(), }); const serverAttachmentSchema = z.object({ id: z.any(), name: z.string(), size: z.number().optional(), url: z.string().url().optional(), }); const serverCommentSchema = z.object({ id: z.string(), author: serverUserSchema, visibility: z.string(), body: z.string(), attachments: z.array(serverAttachmentSchema).default([]).optional(), createdAt: z.number(), updatedAt: z.number(), }); const serverEventSchema = z.object({ id: z.string(), type: z.string(), payload: z.any().optional(), createdAt: z.number(), }); const serverCustomFieldValueSchema = z.object({ label: z.string(), type: z.string(), value: z.any().optional(), displayValue: z.string().optional(), }); const serverTicketWithDetailsSchema = serverTicketSchema.extend({ description: z.string().optional().nullable(), customFields: z.record(z.string(), serverCustomFieldValueSchema).optional(), timeline: z.array(serverEventSchema), comments: z.array(serverCommentSchema), company: z .object({ id: z.string(), name: z.string(), isAvulso: z.boolean().optional() }) .optional() .nullable(), }); export function mapTicketFromServer(input: unknown) { const { csatScore, csatMaxScore, csatComment, csatRatedAt, csatRatedBy, ...base } = serverTicketSchema.parse(input); const s = { csatScore, csatMaxScore, csatComment, csatRatedAt, csatRatedBy, ...base }; const checklist = (s.checklist ?? []).map((item) => ({ ...item, required: item.required ?? true, })); const slaSnapshot = s.slaSnapshot ? { categoryId: s.slaSnapshot.categoryId ? String(s.slaSnapshot.categoryId) : undefined, categoryName: s.slaSnapshot.categoryName ?? undefined, priority: s.slaSnapshot.priority ?? "", responseTargetMinutes: s.slaSnapshot.responseTargetMinutes ?? null, responseMode: (s.slaSnapshot.responseMode ?? "calendar") as "business" | "calendar", solutionTargetMinutes: s.slaSnapshot.solutionTargetMinutes ?? null, solutionMode: (s.slaSnapshot.solutionMode ?? "calendar") as "business" | "calendar", alertThreshold: typeof s.slaSnapshot.alertThreshold === "number" ? s.slaSnapshot.alertThreshold : null, pauseStatuses: s.slaSnapshot.pauseStatuses ?? [], } : null; const ui = { ...base, status: normalizeTicketStatus(s.status), checklist, company: s.company ? { id: s.company.id, name: s.company.name, isAvulso: s.company.isAvulso ?? false } : undefined, machine: s.machine ? { id: s.machine.id ?? null, hostname: s.machine.hostname ?? null, persona: s.machine.persona ?? null, assignedUserName: s.machine.assignedUserName ?? null, assignedUserEmail: s.machine.assignedUserEmail ?? null, status: s.machine.status ?? null, } : null, category: s.category ?? undefined, subcategory: s.subcategory ?? undefined, lastTimelineEntry: s.lastTimelineEntry ?? undefined, updatedAt: new Date(s.updatedAt), createdAt: new Date(s.createdAt), dueAt: s.dueAt ? new Date(s.dueAt) : null, firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null, resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : null, closedAt: s.closedAt ? new Date(s.closedAt) : null, reopenDeadline: typeof s.reopenDeadline === "number" ? s.reopenDeadline : null, reopenWindowDays: typeof s.reopenWindowDays === "number" ? s.reopenWindowDays : null, reopenedAt: typeof s.reopenedAt === "number" ? s.reopenedAt : null, csatScore: typeof csatScore === "number" ? csatScore : null, csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null, csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : null, csatRatedAt: csatRatedAt ? new Date(csatRatedAt) : null, csatRatedBy: csatRatedBy ?? null, formTemplateLabel: base.formTemplateLabel ?? null, slaSnapshot, slaResponseDueAt: s.slaResponseDueAt ? new Date(s.slaResponseDueAt) : null, slaSolutionDueAt: s.slaSolutionDueAt ? new Date(s.slaSolutionDueAt) : null, slaResponseStatus: typeof s.slaResponseStatus === "string" ? (s.slaResponseStatus as string) : null, slaSolutionStatus: typeof s.slaSolutionStatus === "string" ? (s.slaSolutionStatus as string) : null, slaPausedAt: s.slaPausedAt ? new Date(s.slaPausedAt) : null, slaPausedBy: s.slaPausedBy ?? null, slaPausedMs: typeof s.slaPausedMs === "number" ? s.slaPausedMs : null, visitStatus: typeof s.visitStatus === "string" ? s.visitStatus : null, visitPerformedAt: s.visitPerformedAt ? new Date(s.visitPerformedAt) : null, workSummary: s.workSummary ? { totalWorkedMs: s.workSummary.totalWorkedMs, internalWorkedMs: s.workSummary.internalWorkedMs ?? 0, externalWorkedMs: s.workSummary.externalWorkedMs ?? 0, serverNow: s.workSummary.serverNow, activeSession: s.workSummary.activeSession ? { ...s.workSummary.activeSession, startedAt: new Date(s.workSummary.activeSession.startedAt), } : null, perAgentTotals: (s.workSummary.perAgentTotals ?? []).map((item) => ({ agentId: item.agentId, agentName: item.agentName ?? null, agentEmail: item.agentEmail ?? null, avatarUrl: item.avatarUrl ?? null, totalWorkedMs: item.totalWorkedMs, internalWorkedMs: item.internalWorkedMs ?? 0, externalWorkedMs: item.externalWorkedMs ?? 0, })), } : undefined, }; return ui as unknown as z.infer; } export function mapTicketsFromServerList(arr: unknown[]) { return arr.map(mapTicketFromServer); } export function mapTicketWithDetailsFromServer(input: unknown) { const { csatScore, csatMaxScore, csatComment, csatRatedAt, csatRatedBy, ...base } = serverTicketWithDetailsSchema.parse(input); const s = { csatScore, csatMaxScore, csatComment, csatRatedAt, csatRatedBy, ...base }; const checklist = (s.checklist ?? []).map((item) => ({ ...item, required: item.required ?? true, })); const slaSnapshot = s.slaSnapshot ? { categoryId: s.slaSnapshot.categoryId ? String(s.slaSnapshot.categoryId) : undefined, categoryName: s.slaSnapshot.categoryName ?? undefined, priority: s.slaSnapshot.priority ?? "", responseTargetMinutes: s.slaSnapshot.responseTargetMinutes ?? null, responseMode: (s.slaSnapshot.responseMode ?? "calendar") as "business" | "calendar", solutionTargetMinutes: s.slaSnapshot.solutionTargetMinutes ?? null, solutionMode: (s.slaSnapshot.solutionMode ?? "calendar") as "business" | "calendar", alertThreshold: typeof s.slaSnapshot.alertThreshold === "number" ? s.slaSnapshot.alertThreshold : null, pauseStatuses: s.slaSnapshot.pauseStatuses ?? [], } : null; const customFields = Object.entries(s.customFields ?? {}).reduce< Record >( (acc, [key, value]) => { let parsedValue: unknown = value.value; if (value.type === "date") { parsedValue = normalizeCustomFieldDateValue(value.value) ?? value.value; } acc[key] = { label: value.label, type: value.type, value: parsedValue, displayValue: value.displayValue, }; return acc; }, {} ); const ui = { ...base, customFields, status: normalizeTicketStatus(base.status), checklist, category: base.category ?? undefined, subcategory: base.subcategory ?? undefined, lastTimelineEntry: base.lastTimelineEntry ?? undefined, updatedAt: new Date(base.updatedAt), createdAt: new Date(base.createdAt), dueAt: base.dueAt ? new Date(base.dueAt) : null, visitStatus: typeof base.visitStatus === "string" ? base.visitStatus : null, visitPerformedAt: base.visitPerformedAt ? new Date(base.visitPerformedAt) : null, firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null, resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : null, closedAt: base.closedAt ? new Date(base.closedAt) : null, reopenDeadline: typeof base.reopenDeadline === "number" ? base.reopenDeadline : null, reopenWindowDays: typeof base.reopenWindowDays === "number" ? base.reopenWindowDays : null, reopenedAt: typeof base.reopenedAt === "number" ? base.reopenedAt : null, csatScore: typeof csatScore === "number" ? csatScore : null, csatMaxScore: typeof csatMaxScore === "number" ? csatMaxScore : null, csatComment: typeof csatComment === "string" && csatComment.trim().length > 0 ? csatComment.trim() : null, csatRatedAt: csatRatedAt ? new Date(csatRatedAt) : null, csatRatedBy: csatRatedBy ?? null, company: base.company ? { id: base.company.id, name: base.company.name, isAvulso: base.company.isAvulso ?? false } : undefined, machine: base.machine ? { id: base.machine.id ?? null, hostname: base.machine.hostname ?? null, persona: base.machine.persona ?? null, assignedUserName: base.machine.assignedUserName ?? null, assignedUserEmail: base.machine.assignedUserEmail ?? null, status: base.machine.status ?? null, } : null, slaSnapshot, slaResponseDueAt: base.slaResponseDueAt ? new Date(base.slaResponseDueAt) : null, slaSolutionDueAt: base.slaSolutionDueAt ? new Date(base.slaSolutionDueAt) : null, slaResponseStatus: typeof base.slaResponseStatus === "string" ? (base.slaResponseStatus as string) : null, slaSolutionStatus: typeof base.slaSolutionStatus === "string" ? (base.slaSolutionStatus as string) : null, slaPausedAt: base.slaPausedAt ? new Date(base.slaPausedAt) : null, slaPausedBy: base.slaPausedBy ?? null, slaPausedMs: typeof base.slaPausedMs === "number" ? base.slaPausedMs : null, timeline: base.timeline.map((e) => ({ ...e, createdAt: new Date(e.createdAt) })), comments: base.comments.map((c) => ({ ...c, createdAt: new Date(c.createdAt), updatedAt: new Date(c.updatedAt), })), workSummary: base.workSummary ? { totalWorkedMs: base.workSummary.totalWorkedMs, internalWorkedMs: base.workSummary.internalWorkedMs ?? 0, externalWorkedMs: base.workSummary.externalWorkedMs ?? 0, serverNow: base.workSummary.serverNow, activeSession: base.workSummary.activeSession ? { ...base.workSummary.activeSession, startedAt: new Date(base.workSummary.activeSession.startedAt), } : null, perAgentTotals: (base.workSummary.perAgentTotals ?? []).map((item) => ({ agentId: item.agentId, agentName: item.agentName ?? null, agentEmail: item.agentEmail ?? null, avatarUrl: item.avatarUrl ?? null, totalWorkedMs: item.totalWorkedMs, internalWorkedMs: item.internalWorkedMs ?? 0, externalWorkedMs: item.externalWorkedMs ?? 0, })), } : undefined, }; return ui as unknown as z.infer; }