Melhora chat ao vivo com anexos e eventos de timeline
- Reestrutura visual do widget de chat (header branco, status emerald) - Adiciona sistema de anexos com upload e drag-and-drop - Substitui select nativo por componente Select do shadcn - Adiciona eventos LIVE_CHAT_STARTED e LIVE_CHAT_ENDED na timeline - Traduz labels de chat para portugues (Chat iniciado/finalizado) - Filtra CHAT_MESSAGE_ADDED da timeline (apenas inicio/fim aparecem) - Restringe inicio de chat a tickets com responsavel atribuido - Exibe duracao da sessao ao finalizar chat 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
parent
9e676b06f9
commit
3b1cde79df
11 changed files with 782 additions and 77 deletions
|
|
@ -52,6 +52,9 @@ export type MachineInventoryRecord = {
|
|||
remoteAccess?: unknown
|
||||
linkedUsers?: LinkedUser[]
|
||||
customFields?: DeviceCustomField[]
|
||||
usbPolicy?: string | null
|
||||
usbPolicyStatus?: string | null
|
||||
ticketCount?: number | null
|
||||
}
|
||||
|
||||
type WorkbookOptions = {
|
||||
|
|
@ -147,6 +150,9 @@ const COLUMN_VALUE_RESOLVERS: Record<string, (machine: MachineInventoryRecord, d
|
|||
fleetUpdatedAt: (_machine, derived) =>
|
||||
derived.fleetInfo?.updatedAt ? formatDateTime(derived.fleetInfo.updatedAt) : null,
|
||||
managementMode: (machine) => describeManagementMode(machine.managementMode),
|
||||
usbPolicy: (machine) => describeUsbPolicy(machine.usbPolicy),
|
||||
usbPolicyStatus: (machine) => describeUsbPolicyStatus(machine.usbPolicyStatus),
|
||||
ticketCount: (machine) => machine.ticketCount ?? 0,
|
||||
}
|
||||
|
||||
const DEFAULT_COLUMN_CONFIG: DeviceInventoryColumnConfig[] = DEVICE_INVENTORY_COLUMN_METADATA.filter((meta) => meta.default !== false).map(
|
||||
|
|
@ -336,6 +342,38 @@ function describeManagementMode(mode: string | null | undefined): string {
|
|||
}
|
||||
}
|
||||
|
||||
function describeUsbPolicy(policy: string | null | undefined): string {
|
||||
if (!policy) return "—"
|
||||
const normalized = policy.toUpperCase()
|
||||
switch (normalized) {
|
||||
case "ALLOW":
|
||||
return "Permitido"
|
||||
case "BLOCK_ALL":
|
||||
return "Bloqueado"
|
||||
case "READONLY":
|
||||
return "Somente leitura"
|
||||
default:
|
||||
return policy
|
||||
}
|
||||
}
|
||||
|
||||
function describeUsbPolicyStatus(status: string | null | undefined): string {
|
||||
if (!status) return "—"
|
||||
const normalized = status.toUpperCase()
|
||||
switch (normalized) {
|
||||
case "PENDING":
|
||||
return "Pendente"
|
||||
case "APPLYING":
|
||||
return "Aplicando"
|
||||
case "APPLIED":
|
||||
return "Aplicado"
|
||||
case "FAILED":
|
||||
return "Falhou"
|
||||
default:
|
||||
return status
|
||||
}
|
||||
}
|
||||
|
||||
const SOFTWARE_HEADERS = ["Hostname", "Aplicativo", "Versão", "Origem", "Publicador", "Instalado em"] as const
|
||||
const SOFTWARE_COLUMN_WIDTHS = [22, 36, 18, 18, 22, 20] as const
|
||||
|
||||
|
|
|
|||
|
|
@ -359,6 +359,22 @@ function buildTimelineMessage(type: string, payload: Record<string, unknown> | n
|
|||
return "CSAT recebido"
|
||||
case "CSAT_RATED":
|
||||
return "CSAT avaliado"
|
||||
case "LIVE_CHAT_STARTED": {
|
||||
const agentName = p.agentName as string | undefined
|
||||
const machineHostname = p.machineHostname as string | undefined
|
||||
const parts: string[] = ["Chat ao vivo iniciado"]
|
||||
if (agentName) parts[0] += ` por ${agentName}`
|
||||
if (machineHostname) parts.push(`Máquina: ${machineHostname}`)
|
||||
return parts.join(" • ")
|
||||
}
|
||||
case "LIVE_CHAT_ENDED": {
|
||||
const agentName = p.agentName as string | undefined
|
||||
const durationMs = p.durationMs as number | undefined
|
||||
const parts: string[] = ["Chat ao vivo finalizado"]
|
||||
if (agentName) parts[0] += ` por ${agentName}`
|
||||
if (durationMs) parts.push(`Duração: ${formatDurationMs(durationMs)}`)
|
||||
return parts.join(" • ")
|
||||
}
|
||||
default:
|
||||
return null
|
||||
}
|
||||
|
|
@ -397,7 +413,11 @@ function TicketPdfDocument({ ticket, logoDataUrl }: { ticket: TicketWithDetails;
|
|||
safeBody: sanitizeToPlainText(comment.body) || "Sem texto",
|
||||
})) as Array<TicketComment & { safeBody: string }>
|
||||
|
||||
// Tipos de eventos que não devem aparecer no PDF
|
||||
const HIDDEN_EVENT_TYPES = ["CHAT_MESSAGE_ADDED"]
|
||||
|
||||
const timeline = [...ticket.timeline]
|
||||
.filter((event) => !HIDDEN_EVENT_TYPES.includes(event.type))
|
||||
.sort((a, b) => a.createdAt.getTime() - b.createdAt.getTime())
|
||||
.map((event) => {
|
||||
const label = TICKET_TIMELINE_LABELS[event.type] ?? event.type
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue