feat(automations): historico expandivel com detalhes das acoes
All checks were successful
All checks were successful
- Adiciona linhas expandiveis no historico de execucoes - Mostra detalhes completos de cada acao (destinatarios, assunto, etc.) - Salva mais informacoes no backend para acoes de e-mail - Remove log de progresso do dashboard de reports 🤖 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
c2802b1a4d
commit
70cba99424
3 changed files with 279 additions and 39 deletions
|
|
@ -1,13 +1,15 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { Fragment, useMemo, useState } from "react"
|
||||
import { usePaginatedQuery } from "convex/react"
|
||||
import { toast } from "sonner"
|
||||
import { ChevronDown, ChevronRight, Mail, ArrowRight, UserCheck, MessageSquare, ListChecks, ToggleRight, FileText, AlertTriangle } from "lucide-react"
|
||||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
import type { Id } from "@/convex/_generated/dataModel"
|
||||
import { useAuth } from "@/lib/auth-client"
|
||||
import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||
import { cn } from "@/lib/utils"
|
||||
import { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DialogClose, DialogContent, DialogDescription, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
|
|
@ -15,6 +17,11 @@ import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@
|
|||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
|
||||
type ActionApplied = {
|
||||
type: string
|
||||
details?: Record<string, unknown>
|
||||
}
|
||||
|
||||
type AutomationRunRow = {
|
||||
id: Id<"ticketAutomationRuns">
|
||||
createdAt: number
|
||||
|
|
@ -22,7 +29,7 @@ type AutomationRunRow = {
|
|||
matched: boolean
|
||||
eventType: string
|
||||
error: string | null
|
||||
actionsApplied: Array<{ type: string; details?: Record<string, unknown> }> | null
|
||||
actionsApplied: ActionApplied[] | null
|
||||
ticket: { id: Id<"tickets">; reference: number; subject: string } | null
|
||||
automation: { id: Id<"ticketAutomations">; name: string } | null
|
||||
}
|
||||
|
|
@ -50,6 +57,208 @@ function statusBadge(status: AutomationRunRow["status"]) {
|
|||
return { label: "Erro", variant: "destructive" as const }
|
||||
}
|
||||
|
||||
const ACTION_TYPE_LABELS: Record<string, { label: string; icon: typeof Mail }> = {
|
||||
SEND_EMAIL: { label: "Enviar e-mail", icon: Mail },
|
||||
SET_PRIORITY: { label: "Alterar prioridade", icon: ArrowRight },
|
||||
MOVE_QUEUE: { label: "Mover para fila", icon: ArrowRight },
|
||||
ASSIGN_TO: { label: "Atribuir responsavel", icon: UserCheck },
|
||||
ADD_INTERNAL_COMMENT: { label: "Comentario interno", icon: MessageSquare },
|
||||
APPLY_CHECKLIST_TEMPLATE: { label: "Aplicar checklist", icon: ListChecks },
|
||||
SET_CHAT_ENABLED: { label: "Chat habilitado", icon: ToggleRight },
|
||||
SET_FORM_TEMPLATE: { label: "Aplicar formulario", icon: FileText },
|
||||
}
|
||||
|
||||
const PRIORITY_LABELS: Record<string, string> = {
|
||||
LOW: "Baixa",
|
||||
MEDIUM: "Media",
|
||||
HIGH: "Alta",
|
||||
URGENT: "Urgente",
|
||||
}
|
||||
|
||||
function ActionDetails({ action }: { action: ActionApplied }) {
|
||||
const details = action.details ?? {}
|
||||
const config = ACTION_TYPE_LABELS[action.type] ?? { label: action.type, icon: ArrowRight }
|
||||
const Icon = config.icon
|
||||
|
||||
return (
|
||||
<div className="rounded-lg border border-slate-200 bg-white p-3">
|
||||
<div className="flex items-center gap-2 mb-2">
|
||||
<div className="flex size-6 items-center justify-center rounded bg-slate-100">
|
||||
<Icon className="size-3.5 text-slate-600" />
|
||||
</div>
|
||||
<span className="font-medium text-sm text-slate-900">{config.label}</span>
|
||||
</div>
|
||||
|
||||
{action.type === "SEND_EMAIL" && (
|
||||
<div className="space-y-2 text-sm">
|
||||
{details.recipients && Array.isArray(details.recipients) && (
|
||||
<div>
|
||||
<span className="text-slate-500">Destinatarios:</span>
|
||||
<div className="mt-1 flex flex-wrap gap-1">
|
||||
{(details.recipients as string[]).map((email, i) => (
|
||||
<Badge key={i} variant="outline" className="text-xs font-mono">
|
||||
{email}
|
||||
</Badge>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
{details.subject && (
|
||||
<div>
|
||||
<span className="text-slate-500">Assunto:</span>{" "}
|
||||
<span className="text-slate-900">{String(details.subject)}</span>
|
||||
</div>
|
||||
)}
|
||||
{details.messagePreview && (
|
||||
<div>
|
||||
<span className="text-slate-500">Mensagem:</span>{" "}
|
||||
<span className="text-slate-700 italic">{String(details.messagePreview)}</span>
|
||||
</div>
|
||||
)}
|
||||
{details.ctaTarget && (
|
||||
<div>
|
||||
<span className="text-slate-500">Link:</span>{" "}
|
||||
<span className="text-slate-900">
|
||||
{details.ctaTarget === "PORTAL" ? "Portal (cliente)" : details.ctaTarget === "STAFF" ? "Painel (agente)" : "Auto"}
|
||||
</span>
|
||||
</div>
|
||||
)}
|
||||
{details.scheduledAt && (
|
||||
<div className="text-xs text-slate-400 mt-1">
|
||||
Agendado em: {formatDateTime(Number(details.scheduledAt)).date} {formatDateTime(Number(details.scheduledAt)).time}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "SET_PRIORITY" && details.priority && (
|
||||
<div className="text-sm">
|
||||
<span className="text-slate-500">Nova prioridade:</span>{" "}
|
||||
<Badge variant="outline">{PRIORITY_LABELS[String(details.priority)] ?? details.priority}</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "MOVE_QUEUE" && (
|
||||
<div className="text-sm">
|
||||
<span className="text-slate-500">Fila:</span>{" "}
|
||||
<span className="text-slate-900 font-medium">{String(details.queueName ?? details.queueId ?? "—")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "ASSIGN_TO" && (
|
||||
<div className="text-sm">
|
||||
<span className="text-slate-500">Responsavel:</span>{" "}
|
||||
<span className="text-slate-900 font-medium">{String(details.assigneeName ?? "—")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "SET_FORM_TEMPLATE" && (
|
||||
<div className="text-sm">
|
||||
<span className="text-slate-500">Formulario:</span>{" "}
|
||||
<span className="text-slate-900">{String(details.formTemplateLabel ?? details.formTemplate ?? "—")}</span>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "SET_CHAT_ENABLED" && (
|
||||
<div className="text-sm">
|
||||
<span className="text-slate-500">Chat:</span>{" "}
|
||||
<Badge variant={details.enabled ? "secondary" : "outline"}>
|
||||
{details.enabled ? "Habilitado" : "Desabilitado"}
|
||||
</Badge>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "ADD_INTERNAL_COMMENT" && (
|
||||
<div className="text-sm text-slate-600">
|
||||
Comentario interno adicionado ao ticket
|
||||
</div>
|
||||
)}
|
||||
|
||||
{action.type === "APPLY_CHECKLIST_TEMPLATE" && (
|
||||
<div className="text-sm">
|
||||
<span className="text-slate-500">Template:</span>{" "}
|
||||
<span className="text-slate-900">{String(details.templateName ?? "—")}</span>
|
||||
{typeof details.added === "number" && (
|
||||
<span className="text-slate-500 ml-2">({details.added} itens adicionados)</span>
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
function ExpandedDetails({ run }: { run: AutomationRunRow }) {
|
||||
const hasActions = run.actionsApplied && run.actionsApplied.length > 0
|
||||
|
||||
return (
|
||||
<div className="bg-slate-50 px-4 py-3 border-t border-slate-100">
|
||||
<div className="grid gap-4 md:grid-cols-2">
|
||||
{/* Info do Ticket */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Ticket</h4>
|
||||
{run.ticket ? (
|
||||
<div className="text-sm space-y-1">
|
||||
<div>
|
||||
<span className="text-slate-500">Referencia:</span>{" "}
|
||||
<span className="font-mono font-semibold">#{run.ticket.reference}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Assunto:</span>{" "}
|
||||
<span className="text-slate-900">{run.ticket.subject || "—"}</span>
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
<p className="text-sm text-slate-400">Ticket nao disponivel</p>
|
||||
)}
|
||||
</div>
|
||||
|
||||
{/* Info da Execucao */}
|
||||
<div className="space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">Execucao</h4>
|
||||
<div className="text-sm space-y-1">
|
||||
<div>
|
||||
<span className="text-slate-500">Evento:</span>{" "}
|
||||
<span className="text-slate-900">{EVENT_LABELS[run.eventType] ?? run.eventType}</span>
|
||||
</div>
|
||||
<div>
|
||||
<span className="text-slate-500">Condicoes:</span>{" "}
|
||||
<Badge variant={run.matched ? "secondary" : "outline"}>
|
||||
{run.matched ? "Atendidas" : "Nao atendidas"}
|
||||
</Badge>
|
||||
</div>
|
||||
{run.error && (
|
||||
<div className="flex items-start gap-2 mt-2 p-2 rounded bg-red-50 border border-red-200">
|
||||
<AlertTriangle className="size-4 text-red-500 mt-0.5 shrink-0" />
|
||||
<span className="text-red-700 text-xs">{run.error}</span>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Acoes Aplicadas */}
|
||||
{hasActions && (
|
||||
<div className="mt-4 space-y-2">
|
||||
<h4 className="text-xs font-semibold uppercase tracking-wide text-slate-500">
|
||||
Acoes aplicadas ({run.actionsApplied!.length})
|
||||
</h4>
|
||||
<div className="grid gap-2 md:grid-cols-2">
|
||||
{run.actionsApplied!.map((action, i) => (
|
||||
<ActionDetails key={i} action={action} />
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
|
||||
{!hasActions && run.status === "SUCCESS" && (
|
||||
<div className="mt-4">
|
||||
<p className="text-sm text-slate-500">Nenhuma acao foi aplicada nesta execucao.</p>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
export function AutomationRunsDialog({
|
||||
open,
|
||||
automationId,
|
||||
|
|
@ -65,6 +274,7 @@ export function AutomationRunsDialog({
|
|||
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "SUCCESS" | "SKIPPED" | "ERROR">("all")
|
||||
const [expandedId, setExpandedId] = useState<Id<"ticketAutomationRuns"> | null>(null)
|
||||
|
||||
const args = useMemo(() => {
|
||||
if (!open) return "skip" as const
|
||||
|
|
@ -179,7 +389,7 @@ export function AutomationRunsDialog({
|
|||
const eventLabel = EVENT_LABELS[run.eventType] ?? run.eventType
|
||||
const actionsCount = run.actionsApplied?.length ?? 0
|
||||
const actionsLabel =
|
||||
actionsCount === 1 ? "Aplicou 1 ação" : `Aplicou ${actionsCount} ações`
|
||||
actionsCount === 1 ? "Aplicou 1 acao" : `Aplicou ${actionsCount} acoes`
|
||||
const createdAtLabel = formatDateTime(run.createdAt)
|
||||
const details =
|
||||
run.status === "ERROR"
|
||||
|
|
@ -187,46 +397,73 @@ export function AutomationRunsDialog({
|
|||
: run.status === "SKIPPED"
|
||||
? run.matched
|
||||
? "Ignorada"
|
||||
: "Condições não atendidas"
|
||||
: "Condicoes nao atendidas"
|
||||
: actionsCount > 0
|
||||
? actionsLabel
|
||||
: "Sem alterações"
|
||||
: "Sem alteracoes"
|
||||
|
||||
const isExpanded = expandedId === run.id
|
||||
const toggleExpand = () => setExpandedId(isExpanded ? null : run.id)
|
||||
|
||||
return (
|
||||
<TableRow key={run.id} className="transition-colors hover:bg-cyan-50/30">
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-medium text-neutral-900">{createdAtLabel.date}</div>
|
||||
<div className="text-xs text-neutral-600">{createdAtLabel.time}</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
{run.ticket ? (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-semibold text-neutral-900">#{run.ticket.reference}</div>
|
||||
<div className="truncate text-xs text-neutral-600" title={run.ticket.subject}>
|
||||
{run.ticket.subject || "—"}
|
||||
<Fragment key={run.id}>
|
||||
<TableRow
|
||||
className={cn(
|
||||
"transition-colors cursor-pointer",
|
||||
isExpanded ? "bg-cyan-50/50" : "hover:bg-cyan-50/30"
|
||||
)}
|
||||
onClick={toggleExpand}
|
||||
>
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<span className="text-slate-400">
|
||||
{isExpanded ? (
|
||||
<ChevronDown className="size-4" />
|
||||
) : (
|
||||
<ChevronRight className="size-4" />
|
||||
)}
|
||||
</span>
|
||||
<div className="space-y-0.5 text-left">
|
||||
<div className="font-medium text-neutral-900">{createdAtLabel.date}</div>
|
||||
<div className="text-xs text-neutral-600">{createdAtLabel.time}</div>
|
||||
</div>
|
||||
</div>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
{run.ticket ? (
|
||||
<div className="space-y-0.5">
|
||||
<div className="font-semibold text-neutral-900">#{run.ticket.reference}</div>
|
||||
<div className="truncate text-xs text-neutral-600" title={run.ticket.subject}>
|
||||
{run.ticket.subject || "—"}
|
||||
</div>
|
||||
</div>
|
||||
) : (
|
||||
"—"
|
||||
)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
<span className="truncate" title={eventLabel}>
|
||||
{eventLabel}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={badge.variant} className="rounded-full">
|
||||
{badge.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">{actionsCount}</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700 truncate" title={details}>
|
||||
{details}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
<span className="truncate" title={eventLabel}>
|
||||
{eventLabel}
|
||||
</span>
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<Badge variant={badge.variant} className="rounded-full">
|
||||
{badge.label}
|
||||
</Badge>
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">{actionsCount}</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700 truncate" title={details}>
|
||||
{details}
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
{isExpanded && (
|
||||
<TableRow className="hover:bg-transparent">
|
||||
<TableCell colSpan={6} className="p-0">
|
||||
<ExpandedDetails run={run} />
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
)}
|
||||
</Fragment>
|
||||
)
|
||||
})
|
||||
)}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue