sistema-de-chamados/src/lib/mappers/ticket.ts
esdrasrenan 771e25798d fix(checklist): corrige exibicao da descricao do template no ticket
O campo templateDescription nao estava sendo exibido porque o schema
Zod em src/lib/mappers/ticket.ts nao incluia esse campo, fazendo com
que ele fosse removido durante a validacao dos dados do servidor.

- Adiciona templateDescription ao schema Zod do checklist
- Remove logs de debug dos arquivos de backend e frontend

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2025-12-16 19:34:25 -03:00

463 lines
17 KiB
TypeScript

import { z } from "zod";
import { ticketSchema, ticketStatusSchema, ticketWithDetailsSchema } from "@/lib/schemas/ticket";
type NormalizedTicketStatus = z.infer<typeof ticketStatusSchema>;
const STATUS_MAP: Record<string, NormalizedTicketStatus> = {
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<typeof ticketSchema>;
}
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<string, { label: string; type: string; value?: unknown; displayValue?: string }>
>(
(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<typeof ticketWithDetailsSchema>;
}