fix: automações (gatilhos, histórico) e chat desktop
This commit is contained in:
parent
8ab510bfe9
commit
e4d0c95791
7 changed files with 670 additions and 53 deletions
|
|
@ -90,6 +90,8 @@ const CHANNELS = [
|
|||
const TRIGGERS = [
|
||||
{ value: "TICKET_CREATED", label: "Abertura" },
|
||||
{ value: "STATUS_CHANGED", label: "Alteração de status" },
|
||||
{ value: "PRIORITY_CHANGED", label: "Alteração de prioridade" },
|
||||
{ value: "QUEUE_CHANGED", label: "Alteração de fila" },
|
||||
{ value: "COMMENT_ADDED", label: "Inclusão de comentário" },
|
||||
{ value: "TICKET_RESOLVED", label: "Finalização" },
|
||||
]
|
||||
|
|
@ -320,12 +322,16 @@ export function AutomationEditorDialog({
|
|||
|
||||
return (
|
||||
<DialogContent className="max-w-4xl">
|
||||
<DialogHeader className="gap-2">
|
||||
<DialogHeader className="gap-4 pb-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<DialogTitle>{automation ? "Editar automação" : "Nova automação"}</DialogTitle>
|
||||
<div className="flex items-center gap-3">
|
||||
<span className="text-sm text-neutral-600">Ativa</span>
|
||||
<Switch checked={enabled} onCheckedChange={setEnabled} />
|
||||
<span className="text-sm font-medium text-neutral-700">Ativa</span>
|
||||
<Switch
|
||||
checked={enabled}
|
||||
onCheckedChange={setEnabled}
|
||||
className="data-[state=checked]:bg-black data-[state=unchecked]:bg-slate-300"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex flex-wrap items-center gap-2">
|
||||
|
|
@ -654,7 +660,7 @@ export function AutomationEditorDialog({
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveCondition(c.id)}
|
||||
className="mt-6 h-8 w-8 text-slate-500 hover:bg-white"
|
||||
className="mt-6 h-8 w-8 text-slate-500 hover:bg-red-50 hover:text-red-700"
|
||||
title="Remover"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
|
|
@ -799,6 +805,7 @@ export function AutomationEditorDialog({
|
|||
onCheckedChange={(checked) =>
|
||||
setActions((prev) => prev.map((item) => (item.id === a.id ? { ...item, enabled: checked } : item)))
|
||||
}
|
||||
className="data-[state=checked]:bg-black data-[state=unchecked]:bg-slate-300"
|
||||
/>
|
||||
</div>
|
||||
) : (
|
||||
|
|
@ -818,7 +825,7 @@ export function AutomationEditorDialog({
|
|||
variant="ghost"
|
||||
size="icon"
|
||||
onClick={() => handleRemoveAction(a.id)}
|
||||
className="mt-6 h-8 w-8 text-slate-500 hover:bg-slate-50"
|
||||
className="mt-6 h-8 w-8 text-slate-500 hover:bg-red-50 hover:text-red-700"
|
||||
title="Remover"
|
||||
>
|
||||
<Trash2 className="size-4" />
|
||||
|
|
|
|||
241
src/components/automations/automation-runs-dialog.tsx
Normal file
241
src/components/automations/automation-runs-dialog.tsx
Normal file
|
|
@ -0,0 +1,241 @@
|
|||
"use client"
|
||||
|
||||
import { useMemo, useState } from "react"
|
||||
import { usePaginatedQuery } from "convex/react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
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 { Badge } from "@/components/ui/badge"
|
||||
import { Button } from "@/components/ui/button"
|
||||
import { DialogClose, DialogContent, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
|
||||
type AutomationRunRow = {
|
||||
id: Id<"ticketAutomationRuns">
|
||||
createdAt: number
|
||||
status: "SUCCESS" | "SKIPPED" | "ERROR"
|
||||
matched: boolean
|
||||
eventType: string
|
||||
error: string | null
|
||||
actionsApplied: Array<{ type: string; details?: Record<string, unknown> }> | null
|
||||
ticket: { id: Id<"tickets">; reference: number; subject: string } | null
|
||||
automation: { id: Id<"ticketAutomations">; name: string } | null
|
||||
}
|
||||
|
||||
const EVENT_LABELS: Record<string, string> = {
|
||||
TICKET_CREATED: "Abertura",
|
||||
STATUS_CHANGED: "Status",
|
||||
PRIORITY_CHANGED: "Prioridade",
|
||||
QUEUE_CHANGED: "Fila",
|
||||
COMMENT_ADDED: "Comentário",
|
||||
TICKET_RESOLVED: "Finalização",
|
||||
}
|
||||
|
||||
function formatDateTime(timestamp: number) {
|
||||
return new Date(timestamp).toLocaleString("pt-BR")
|
||||
}
|
||||
|
||||
function statusBadge(status: AutomationRunRow["status"]) {
|
||||
if (status === "SUCCESS") return { label: "Sucesso", variant: "secondary" as const }
|
||||
if (status === "SKIPPED") return { label: "Ignorada", variant: "outline" as const }
|
||||
return { label: "Erro", variant: "destructive" as const }
|
||||
}
|
||||
|
||||
export function AutomationRunsDialog({
|
||||
open,
|
||||
automationId,
|
||||
automationName,
|
||||
onClose,
|
||||
}: {
|
||||
open: boolean
|
||||
automationId: Id<"ticketAutomations"> | null
|
||||
automationName: string | null
|
||||
onClose: () => void
|
||||
}) {
|
||||
const { session, convexUserId } = useAuth()
|
||||
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
||||
const [statusFilter, setStatusFilter] = useState<"all" | "SUCCESS" | "SKIPPED" | "ERROR">("all")
|
||||
|
||||
const args = useMemo(() => {
|
||||
if (!open) return "skip" as const
|
||||
if (!convexUserId) return "skip" as const
|
||||
if (!automationId) return "skip" as const
|
||||
return {
|
||||
tenantId,
|
||||
viewerId: convexUserId as Id<"users">,
|
||||
automationId,
|
||||
status: statusFilter === "all" ? undefined : statusFilter,
|
||||
}
|
||||
}, [open, convexUserId, automationId, tenantId, statusFilter])
|
||||
|
||||
const { results, status, loadMore } = usePaginatedQuery(api.automations.listRunsPaginated, args, {
|
||||
initialNumItems: 20,
|
||||
})
|
||||
|
||||
const isLoadingFirstPage = status === "LoadingFirstPage"
|
||||
const isLoadingMore = status === "LoadingMore"
|
||||
const canLoadMore = status === "CanLoadMore"
|
||||
|
||||
const rows = (results ?? []) as unknown as AutomationRunRow[]
|
||||
|
||||
return (
|
||||
<DialogContent className="max-w-5xl">
|
||||
<DialogHeader className="gap-4 pb-2">
|
||||
<div className="flex flex-wrap items-center justify-between gap-3">
|
||||
<div className="space-y-1">
|
||||
<DialogTitle>Histórico de execuções</DialogTitle>
|
||||
{automationName ? (
|
||||
<p className="text-sm text-neutral-600">{automationName}</p>
|
||||
) : null}
|
||||
</div>
|
||||
<div className="flex items-center gap-2">
|
||||
<Select value={statusFilter} onValueChange={(value) => setStatusFilter(value as typeof statusFilter)}>
|
||||
<SelectTrigger className="w-[160px]">
|
||||
<SelectValue placeholder="Status" />
|
||||
</SelectTrigger>
|
||||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="all">Todos</SelectItem>
|
||||
<SelectItem value="SUCCESS">Sucesso</SelectItem>
|
||||
<SelectItem value="SKIPPED">Ignorada</SelectItem>
|
||||
<SelectItem value="ERROR">Erro</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<DialogClose asChild>
|
||||
<Button type="button" variant="outline" onClick={onClose}>
|
||||
Fechar
|
||||
</Button>
|
||||
</DialogClose>
|
||||
</div>
|
||||
</div>
|
||||
</DialogHeader>
|
||||
|
||||
<div className="space-y-3">
|
||||
<div className="overflow-x-auto">
|
||||
<Table className="w-full" style={{ tableLayout: "fixed", minWidth: "980px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "210px" }} />
|
||||
<col style={{ width: "260px" }} />
|
||||
<col style={{ width: "120px" }} />
|
||||
<col style={{ width: "140px" }} />
|
||||
<col style={{ width: "110px" }} />
|
||||
<col style={{ width: "280px" }} />
|
||||
</colgroup>
|
||||
<TableHeader className="bg-slate-100/80">
|
||||
<TableRow className="bg-transparent text-[11px] uppercase tracking-wide text-neutral-600 hover:bg-transparent">
|
||||
<TableHead className="px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Data
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Ticket
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Evento
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Resultado
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Ações
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Detalhes
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{isLoadingFirstPage ? (
|
||||
Array.from({ length: 6 }).map((_, i) => (
|
||||
<TableRow key={`skeleton-${i}`} className="animate-pulse">
|
||||
<TableCell className="text-center"><Skeleton className="mx-auto h-4 w-28" /></TableCell>
|
||||
<TableCell className="text-center"><Skeleton className="mx-auto h-4 w-44" /></TableCell>
|
||||
<TableCell className="text-center"><Skeleton className="mx-auto h-4 w-20" /></TableCell>
|
||||
<TableCell className="text-center"><Skeleton className="mx-auto h-8 w-24 rounded-full" /></TableCell>
|
||||
<TableCell className="text-center"><Skeleton className="mx-auto h-4 w-10" /></TableCell>
|
||||
<TableCell className="text-center"><Skeleton className="mx-auto h-4 w-56" /></TableCell>
|
||||
</TableRow>
|
||||
))
|
||||
) : rows.length === 0 ? (
|
||||
<TableRow>
|
||||
<TableCell colSpan={6} className="py-10 text-center text-sm text-neutral-500">
|
||||
Nenhuma execução encontrada.
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
) : (
|
||||
rows.map((run) => {
|
||||
const badge = statusBadge(run.status)
|
||||
const eventLabel = EVENT_LABELS[run.eventType] ?? run.eventType
|
||||
const actionsCount = run.actionsApplied?.length ?? 0
|
||||
const details =
|
||||
run.status === "ERROR"
|
||||
? run.error ?? "Erro desconhecido"
|
||||
: run.status === "SKIPPED"
|
||||
? run.matched
|
||||
? "Ignorada"
|
||||
: "Condições não atendidas"
|
||||
: actionsCount > 0
|
||||
? `Aplicou ${actionsCount} ação(ões)`
|
||||
: "Sem alterações"
|
||||
|
||||
return (
|
||||
<TableRow key={run.id} className="transition-colors hover:bg-cyan-50/30">
|
||||
<TableCell className="text-center text-sm text-neutral-700">{formatDateTime(run.createdAt)}</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">{eventLabel}</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>
|
||||
)
|
||||
})
|
||||
)}
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
|
||||
{canLoadMore ? (
|
||||
<div className="flex justify-center">
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
try {
|
||||
loadMore(20)
|
||||
} catch (error) {
|
||||
toast.error(error instanceof Error ? error.message : "Falha ao carregar mais")
|
||||
}
|
||||
}}
|
||||
disabled={isLoadingMore}
|
||||
className="rounded-full"
|
||||
>
|
||||
{isLoadingMore ? "Carregando..." : "Carregar mais"}
|
||||
</Button>
|
||||
</div>
|
||||
) : null}
|
||||
</div>
|
||||
</DialogContent>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
@ -2,7 +2,7 @@
|
|||
|
||||
import { useMemo, useState } from "react"
|
||||
import { useMutation, useQuery } from "convex/react"
|
||||
import { MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
||||
import { History, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
||||
import { toast } from "sonner"
|
||||
|
||||
import { api } from "@/convex/_generated/api"
|
||||
|
|
@ -18,8 +18,10 @@ import { Input } from "@/components/ui/input"
|
|||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||
import { Skeleton } from "@/components/ui/skeleton"
|
||||
import { Switch } from "@/components/ui/switch"
|
||||
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from "@/components/ui/table"
|
||||
|
||||
import { AutomationEditorDialog } from "@/components/automations/automation-editor-dialog"
|
||||
import { AutomationRunsDialog } from "@/components/automations/automation-runs-dialog"
|
||||
|
||||
type AutomationRow = {
|
||||
id: Id<"ticketAutomations">
|
||||
|
|
@ -38,6 +40,8 @@ type AutomationRow = {
|
|||
const TRIGGER_LABELS: Record<string, string> = {
|
||||
TICKET_CREATED: "Abertura",
|
||||
STATUS_CHANGED: "Alteração de status",
|
||||
PRIORITY_CHANGED: "Alteração de prioridade",
|
||||
QUEUE_CHANGED: "Alteração de fila",
|
||||
COMMENT_ADDED: "Inclusão de comentário",
|
||||
TICKET_RESOLVED: "Finalização",
|
||||
}
|
||||
|
|
@ -51,6 +55,16 @@ function formatLastRun(timestamp: number | null) {
|
|||
return new Date(timestamp).toLocaleString("pt-BR")
|
||||
}
|
||||
|
||||
function formatConditionsSummary(conditions: unknown | null) {
|
||||
if (!conditions || typeof conditions !== "object" || Array.isArray(conditions)) return "—"
|
||||
const group = conditions as { op?: unknown; conditions?: unknown }
|
||||
const list = Array.isArray(group.conditions) ? group.conditions : []
|
||||
if (list.length === 0) return "—"
|
||||
const op = typeof group.op === "string" ? group.op.toUpperCase() : "AND"
|
||||
const opLabel = op === "OR" ? "OU" : "E"
|
||||
return `${list.length} (${opLabel})`
|
||||
}
|
||||
|
||||
export function AutomationsManager() {
|
||||
const { session, convexUserId } = useAuth()
|
||||
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
|
||||
|
|
@ -61,6 +75,8 @@ export function AutomationsManager() {
|
|||
|
||||
const [editorOpen, setEditorOpen] = useState(false)
|
||||
const [editing, setEditing] = useState<AutomationRow | null>(null)
|
||||
const [runsOpen, setRunsOpen] = useState(false)
|
||||
const [runsAutomation, setRunsAutomation] = useState<AutomationRow | null>(null)
|
||||
|
||||
const list = useQuery(
|
||||
api.automations.list,
|
||||
|
|
@ -94,6 +110,11 @@ export function AutomationsManager() {
|
|||
setEditorOpen(true)
|
||||
}
|
||||
|
||||
const handleOpenRuns = (row: AutomationRow) => {
|
||||
setRunsAutomation(row)
|
||||
setRunsOpen(true)
|
||||
}
|
||||
|
||||
const handleToggle = async (row: AutomationRow, nextEnabled: boolean) => {
|
||||
if (!convexUserId) return
|
||||
try {
|
||||
|
|
@ -147,7 +168,9 @@ export function AutomationsManager() {
|
|||
<SelectContent className="rounded-xl">
|
||||
<SelectItem value="all">Todos</SelectItem>
|
||||
<SelectItem value="TICKET_CREATED">Abertura</SelectItem>
|
||||
<SelectItem value="STATUS_CHANGED">Alteração</SelectItem>
|
||||
<SelectItem value="STATUS_CHANGED">Status</SelectItem>
|
||||
<SelectItem value="PRIORITY_CHANGED">Prioridade</SelectItem>
|
||||
<SelectItem value="QUEUE_CHANGED">Fila</SelectItem>
|
||||
<SelectItem value="COMMENT_ADDED">Comentário</SelectItem>
|
||||
<SelectItem value="TICKET_RESOLVED">Finalização</SelectItem>
|
||||
</SelectContent>
|
||||
|
|
@ -178,6 +201,23 @@ export function AutomationsManager() {
|
|||
</CardAction>
|
||||
</CardHeader>
|
||||
<CardContent>
|
||||
<Dialog
|
||||
open={runsOpen}
|
||||
onOpenChange={(next) => {
|
||||
setRunsOpen(next)
|
||||
if (!next) {
|
||||
setRunsAutomation(null)
|
||||
}
|
||||
}}
|
||||
>
|
||||
<AutomationRunsDialog
|
||||
open={runsOpen}
|
||||
automationId={runsAutomation?.id ?? null}
|
||||
automationName={runsAutomation?.name ?? null}
|
||||
onClose={() => setRunsOpen(false)}
|
||||
/>
|
||||
</Dialog>
|
||||
|
||||
{!list ? (
|
||||
<div className="space-y-2">
|
||||
{Array.from({ length: 6 }).map((_, i) => (
|
||||
|
|
@ -190,76 +230,109 @@ export function AutomationsManager() {
|
|||
</p>
|
||||
) : (
|
||||
<div className="overflow-x-auto">
|
||||
<table className="min-w-full border-separate border-spacing-y-2">
|
||||
<thead>
|
||||
<tr className="text-left text-xs font-semibold uppercase tracking-wide text-neutral-500">
|
||||
<th className="px-2 py-1">Nome</th>
|
||||
<th className="px-2 py-1">Quando</th>
|
||||
<th className="px-2 py-1">Ações</th>
|
||||
<th className="px-2 py-1">Execuções</th>
|
||||
<th className="px-2 py-1">Última</th>
|
||||
<th className="px-2 py-1">Status</th>
|
||||
<th className="px-2 py-1 text-right"> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<Table className="w-full" style={{ tableLayout: "fixed", minWidth: "980px" }}>
|
||||
<colgroup>
|
||||
<col style={{ width: "320px" }} />
|
||||
<col style={{ width: "190px" }} />
|
||||
<col style={{ width: "120px" }} />
|
||||
<col style={{ width: "100px" }} />
|
||||
<col style={{ width: "120px" }} />
|
||||
<col style={{ width: "230px" }} />
|
||||
<col style={{ width: "140px" }} />
|
||||
<col style={{ width: "70px" }} />
|
||||
</colgroup>
|
||||
<TableHeader className="bg-slate-100/80">
|
||||
<TableRow className="bg-transparent text-[11px] uppercase tracking-wide text-neutral-600 hover:bg-transparent">
|
||||
<TableHead className="px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Nome
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Quando
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Condições
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Ações
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Execuções
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Última
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Status
|
||||
</TableHead>
|
||||
<TableHead className="border-l border-slate-200 px-3 py-3 text-center text-[11px] font-semibold uppercase tracking-wide text-neutral-600">
|
||||
Ações
|
||||
</TableHead>
|
||||
</TableRow>
|
||||
</TableHeader>
|
||||
<TableBody>
|
||||
{filtered.map((row) => (
|
||||
<tr key={row.id} className="rounded-xl border border-slate-200 bg-white">
|
||||
<td className="px-2 py-2 text-sm font-medium text-neutral-900">{row.name}</td>
|
||||
<td className="px-2 py-2 text-sm text-neutral-700">
|
||||
<div className="flex items-center gap-2">
|
||||
<TableRow key={row.id} className="transition-colors hover:bg-cyan-50/30">
|
||||
<TableCell className="text-center font-semibold text-neutral-900 truncate" title={row.name}>
|
||||
{row.name}
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Badge variant="secondary" className="rounded-full">
|
||||
{triggerLabel(row.trigger)}
|
||||
</Badge>
|
||||
{row.timing === "DELAYED" && row.delayMs ? (
|
||||
<span className="text-xs text-neutral-500">
|
||||
<Badge variant="outline" className="rounded-full">
|
||||
+{Math.round(row.delayMs / 60000)}m
|
||||
</span>
|
||||
</Badge>
|
||||
) : null}
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-sm text-neutral-700">{row.actions?.length ?? 0}</td>
|
||||
<td className="px-2 py-2 text-sm text-neutral-700">{row.runCount ?? 0}</td>
|
||||
<td className="px-2 py-2 text-sm text-neutral-700">{formatLastRun(row.lastRunAt)}</td>
|
||||
<td className="px-2 py-2">
|
||||
<div className="flex items-center gap-2">
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">
|
||||
{formatConditionsSummary(row.conditions)}
|
||||
</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">{row.actions?.length ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">{row.runCount ?? 0}</TableCell>
|
||||
<TableCell className="text-center text-sm text-neutral-700">{formatLastRun(row.lastRunAt)}</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<div className="flex items-center justify-center gap-2">
|
||||
<Switch
|
||||
checked={row.enabled}
|
||||
onCheckedChange={(checked) => handleToggle(row, checked)}
|
||||
className="data-[state=checked]:bg-black data-[state=unchecked]:bg-slate-300"
|
||||
/>
|
||||
<span className="text-xs text-neutral-600">{row.enabled ? "Ativa" : "Inativa"}</span>
|
||||
<span className="text-xs font-medium text-neutral-700">{row.enabled ? "Ativa" : "Inativa"}</span>
|
||||
</div>
|
||||
</td>
|
||||
<td className="px-2 py-2 text-right">
|
||||
</TableCell>
|
||||
<TableCell className="text-center">
|
||||
<DropdownMenu>
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="ghost" size="icon" className="h-8 w-8">
|
||||
<Button variant="ghost" size="icon" className="h-9 w-9 rounded-full">
|
||||
<MoreHorizontal className="size-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
<DropdownMenuContent align="end" className="rounded-xl">
|
||||
<DropdownMenuContent align="end" className="rounded-xl">
|
||||
<DropdownMenuItem onClick={() => handleOpenRuns(row)} className="gap-2">
|
||||
<History className="size-4" />
|
||||
Histórico
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem onClick={() => handleEdit(row)} className="gap-2">
|
||||
<Pencil className="size-4" />
|
||||
Editar
|
||||
</DropdownMenuItem>
|
||||
<DropdownMenuItem
|
||||
onClick={() => handleDelete(row)}
|
||||
className="gap-2 text-red-600 focus:text-red-600"
|
||||
>
|
||||
<DropdownMenuItem variant="destructive" onClick={() => handleDelete(row)} className="gap-2">
|
||||
<Trash2 className="size-4" />
|
||||
Excluir
|
||||
</DropdownMenuItem>
|
||||
</DropdownMenuContent>
|
||||
</DropdownMenu>
|
||||
</td>
|
||||
</tr>
|
||||
</TableCell>
|
||||
</TableRow>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
</TableBody>
|
||||
</Table>
|
||||
</div>
|
||||
)}
|
||||
</CardContent>
|
||||
</Card>
|
||||
)
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue