feat(automations): historico expandivel com detalhes das acoes
All checks were successful
CI/CD Web + Desktop / Detect changes (push) Successful in 6s
CI/CD Web + Desktop / Deploy (VPS Linux) (push) Successful in 3m15s
Quality Checks / Lint, Test and Build (push) Successful in 3m35s
CI/CD Web + Desktop / Deploy Convex functions (push) Successful in 1m20s

- 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:
rever-tecnologia 2025-12-18 10:51:37 -03:00
parent c2802b1a4d
commit 70cba99424
3 changed files with 279 additions and 39 deletions

View file

@ -1012,8 +1012,14 @@ async function applyActions(
applied.push({
type: "SEND_EMAIL",
details: {
recipients: to,
toCount: to.length,
subject,
messagePreview: message.length > 100 ? `${message.slice(0, 100)}...` : message,
ctaTarget: effectiveTarget,
ctaLabel,
ctaUrl,
scheduledAt: Date.now(),
},
})
}

View file

@ -161,11 +161,8 @@ async function releaseDashboardLock(ctx: MutationCtx, lockId: Id<"analyticsLocks
}
}
function logDashboardProgress(processed: number, tenantId: string) {
const rssMb = Math.round((process.memoryUsage().rss ?? 0) / (1024 * 1024));
console.log(
`[reports] dashboardAggregate tenant=${tenantId} processed=${processed} rssMB=${rssMb}`,
);
function logDashboardProgress(_processed: number, _tenantId: string) {
// Log de progresso removido para reduzir ruido no console
}
function mapToChronologicalSeries(map: Map<string, number>) {

View file

@ -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,18 +397,37 @@ 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">
<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="space-y-0.5">
<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 ? (
@ -227,6 +456,14 @@ export function AutomationRunsDialog({
{details}
</TableCell>
</TableRow>
{isExpanded && (
<TableRow className="hover:bg-transparent">
<TableCell colSpan={6} className="p-0">
<ExpandedDetails run={run} />
</TableCell>
</TableRow>
)}
</Fragment>
)
})
)}