feat: dispositivos e ajustes de csat e relatórios

This commit is contained in:
codex-bot 2025-11-03 19:29:50 -03:00
parent 25d2a9b062
commit e0ef66555d
86 changed files with 5811 additions and 992 deletions

View file

@ -103,7 +103,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
}
}, [session?.user])
// Sempre tenta obter o contexto da máquina.
// Sempre tenta obter o contexto da dispositivo.
// 1) Se a sessão Better Auth indicar role "machine", buscamos normalmente.
// 2) Se a sessão vier nula (alguns ambientes WebView), ainda assim tentamos
// carregar o contexto — se a API responder 200, assumimos que há sessão válida
@ -303,7 +303,7 @@ export function AuthProvider({ children }: { children: React.ReactNode }) {
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [ensureUser, session?.user?.email, session?.user?.tenantId, session?.user?.role, convexUserId])
// Se não houver sessão mas tivermos contexto de máquina, tratamos como "machine"
// Se não houver sessão mas tivermos contexto de dispositivo, tratamos como "machine"
const baseRole = session?.user?.role ? session.user.role.toLowerCase() : (machineContext ? "machine" : null)
const personaRole = session?.user?.machinePersona ? session.user.machinePersona.toLowerCase() : null
const normalizedRole =

View file

@ -0,0 +1,67 @@
export type DeviceInventoryColumnMetadata = {
key: string
label: string
width: number
default?: boolean
description?: string
}
export type DeviceInventoryColumnConfig = {
key: string
label?: string
}
export const DEVICE_INVENTORY_COLUMN_METADATA: DeviceInventoryColumnMetadata[] = [
{ key: "displayName", label: "Dispositivo", width: 26, default: true },
{ key: "hostname", label: "Hostname", width: 24, default: true },
{ key: "deviceType", label: "Tipo", width: 14, default: true },
{ key: "devicePlatform", label: "Plataforma", width: 18, default: true },
{ key: "company", label: "Empresa", width: 26, default: true },
{ key: "status", label: "Status", width: 16, default: true },
{ key: "persona", label: "Persona", width: 16, default: true },
{ key: "active", label: "Ativo", width: 10, default: true },
{ key: "lastHeartbeat", label: "Último heartbeat", width: 20, default: true },
{ key: "assignedUser", label: "Responsável", width: 24, default: true },
{ key: "assignedEmail", label: "E-mail responsável", width: 26, default: true },
{ key: "linkedUsers", label: "Usuários vinculados", width: 28, default: true },
{ key: "authEmail", label: "E-mail autenticado", width: 26, default: true },
{ key: "osName", label: "Sistema operacional", width: 20, default: true },
{ key: "osVersion", label: "Versão SO", width: 18, default: true },
{ key: "architecture", label: "Arquitetura", width: 14, default: true },
{ key: "hardwareVendor", label: "Fabricante", width: 20, default: true },
{ key: "hardwareModel", label: "Modelo", width: 22, default: true },
{ key: "hardwareSerial", label: "Serial hardware", width: 24, default: true },
{ key: "cpu", label: "Processador", width: 22, default: true },
{ key: "physicalCores", label: "Cores físicas", width: 14, default: true },
{ key: "logicalCores", label: "Cores lógicas", width: 14, default: true },
{ key: "memoryGiB", label: "Memória (GiB)", width: 20, default: true },
{ key: "gpus", label: "GPUs", width: 24, default: true },
{ key: "labels", label: "Labels", width: 24, default: true },
{ key: "macs", label: "MACs", width: 24, default: true },
{ key: "serials", label: "Seriais", width: 24, default: true },
{ key: "primaryIp", label: "IP principal", width: 18, default: true },
{ key: "publicIp", label: "IP público", width: 18, default: true },
{ key: "registeredBy", label: "Registrado via", width: 18, default: true },
{ key: "tokenExpiresAt", label: "Token expira em", width: 20, default: true },
{ key: "tokenLastUsedAt", label: "Token último uso", width: 20, default: true },
{ key: "tokenUsageCount", label: "Uso do token", width: 14, default: true },
{ key: "createdAt", label: "Criado em", width: 20, default: true },
{ key: "updatedAt", label: "Atualizado em", width: 20, default: true },
{ key: "softwareCount", label: "Softwares instalados", width: 20, default: true },
{ key: "osBuild", label: "Build SO", width: 18, default: true },
{ key: "osLicense", label: "Licença ativada", width: 18, default: true },
{ key: "osExperience", label: "Experiência SO", width: 18, default: true },
{ key: "domain", label: "Domínio", width: 18, default: true },
{ key: "workgroup", label: "Grupo de trabalho", width: 20, default: true },
{ key: "deviceName", label: "Nome do dispositivo", width: 24, default: true },
{ key: "boardSerial", label: "Serial placa-mãe", width: 24, default: true },
{ key: "collaboratorName", label: "Colaborador (nome)", width: 24, default: true },
{ key: "collaboratorEmail", label: "Colaborador (e-mail)", width: 26, default: true },
{ key: "remoteAccessCount", label: "Acessos remotos", width: 18, default: true },
{ key: "fleetId", label: "Fleet ID", width: 18, default: true },
{ key: "fleetTeam", label: "Equipe Fleet", width: 18, default: true },
{ key: "fleetUpdatedAt", label: "Fleet atualizado em", width: 20, default: true },
{ key: "managementMode", label: "Modo de gestão", width: 20, default: false },
]
export type DeviceInventoryColumnKey = (typeof DEVICE_INVENTORY_COLUMN_METADATA)[number]["key"]

View file

@ -65,6 +65,11 @@ const serverTicketSchema = z.object({
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(),
@ -154,9 +159,17 @@ const serverTicketWithDetailsSchema = serverTicketSchema.extend({
});
export function mapTicketFromServer(input: unknown) {
const s = serverTicketSchema.parse(input);
const {
csatScore,
csatMaxScore,
csatComment,
csatRatedAt,
csatRatedBy,
...base
} = serverTicketSchema.parse(input);
const s = { csatScore, csatMaxScore, csatComment, csatRatedAt, csatRatedBy, ...base };
const ui = {
...s,
...base,
status: normalizeTicketStatus(s.status),
company: s.company
? { id: s.company.id, name: s.company.name, isAvulso: s.company.isAvulso ?? false }
@ -179,6 +192,11 @@ export function mapTicketFromServer(input: unknown) {
dueAt: s.dueAt ? new Date(s.dueAt) : null,
firstResponseAt: s.firstResponseAt ? new Date(s.firstResponseAt) : null,
resolvedAt: s.resolvedAt ? new Date(s.resolvedAt) : 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,
workSummary: s.workSummary
? {
totalWorkedMs: s.workSummary.totalWorkedMs,
@ -211,7 +229,15 @@ export function mapTicketsFromServerList(arr: unknown[]) {
}
export function mapTicketWithDetailsFromServer(input: unknown) {
const s = serverTicketWithDetailsSchema.parse(input);
const {
csatScore,
csatMaxScore,
csatComment,
csatRatedAt,
csatRatedBy,
...base
} = serverTicketWithDetailsSchema.parse(input);
const s = { csatScore, csatMaxScore, csatComment, csatRatedAt, csatRatedBy, ...base };
const customFields = Object.entries(s.customFields ?? {}).reduce<
Record<string, { label: string; type: string; value?: unknown; displayValue?: string }>
>(
@ -231,47 +257,52 @@ export function mapTicketWithDetailsFromServer(input: unknown) {
{}
);
const ui = {
...s,
...base,
customFields,
status: normalizeTicketStatus(s.status),
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,
company: s.company ? { id: s.company.id, name: s.company.name, isAvulso: s.company.isAvulso ?? false } : undefined,
machine: s.machine
status: normalizeTicketStatus(base.status),
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,
firstResponseAt: base.firstResponseAt ? new Date(base.firstResponseAt) : null,
resolvedAt: base.resolvedAt ? new Date(base.resolvedAt) : 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: 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,
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,
timeline: s.timeline.map((e) => ({ ...e, createdAt: new Date(e.createdAt) })),
comments: s.comments.map((c) => ({
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: s.workSummary
workSummary: base.workSummary
? {
totalWorkedMs: s.workSummary.totalWorkedMs,
internalWorkedMs: s.workSummary.internalWorkedMs ?? 0,
externalWorkedMs: s.workSummary.externalWorkedMs ?? 0,
serverNow: s.workSummary.serverNow,
activeSession: s.workSummary.activeSession
totalWorkedMs: base.workSummary.totalWorkedMs,
internalWorkedMs: base.workSummary.internalWorkedMs ?? 0,
externalWorkedMs: base.workSummary.externalWorkedMs ?? 0,
serverNow: base.workSummary.serverNow,
activeSession: base.workSummary.activeSession
? {
...s.workSummary.activeSession,
startedAt: new Date(s.workSummary.activeSession.startedAt),
...base.workSummary.activeSession,
startedAt: new Date(base.workSummary.activeSession.startedAt),
}
: null,
perAgentTotals: (s.workSummary.perAgentTotals ?? []).map((item) => ({
perAgentTotals: (base.workSummary.perAgentTotals ?? []).map((item) => ({
agentId: item.agentId,
agentName: item.agentName ?? null,
agentEmail: item.agentEmail ?? null,

View file

@ -139,17 +139,30 @@ export const ticketSchema = z.object({
.nullable(),
dueAt: z.coerce.date().nullable(),
firstResponseAt: z.coerce.date().nullable(),
resolvedAt: z.coerce.date().nullable(),
updatedAt: z.coerce.date(),
createdAt: z.coerce.date(),
tags: z.array(z.string()).default([]),
lastTimelineEntry: z.string().optional(),
metrics: z
.object({
timeWaitingMinutes: z.number().nullable(),
timeOpenedMinutes: z.number().nullable(),
})
.nullable(),
resolvedAt: z.coerce.date().nullable(),
updatedAt: z.coerce.date(),
createdAt: z.coerce.date(),
tags: z.array(z.string()).default([]),
lastTimelineEntry: z.string().optional(),
metrics: z
.object({
timeWaitingMinutes: z.number().nullable(),
timeOpenedMinutes: z.number().nullable(),
})
.nullable(),
relatedTicketIds: z.array(z.string()).optional(),
resolvedWithTicketId: z.string().nullable().optional(),
reopenDeadline: z.number().nullable().optional(),
reopenWindowDays: z.number().nullable().optional(),
reopenedAt: z.number().nullable().optional(),
reopenedBy: z.string().nullable().optional(),
chatEnabled: z.boolean().optional(),
formTemplate: z.string().nullable().optional(),
csatScore: z.number().nullable().optional(),
csatMaxScore: z.number().nullable().optional(),
csatComment: z.string().nullable().optional(),
csatRatedAt: z.coerce.date().nullable().optional(),
csatRatedBy: z.string().nullable().optional(),
category: ticketCategorySummarySchema.nullable().optional(),
subcategory: ticketSubcategorySummarySchema.nullable().optional(),
workSummary: z