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>
463 lines
17 KiB
TypeScript
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>;
|
|
}
|