feat: adiciona SLA por empresa e modal de exclusao de automacoes
Some checks failed
Some checks failed
## SLA por Empresa - Adiciona tabela companySlaSettings no schema - Cria convex/companySlas.ts com queries e mutations - Modifica resolveTicketSlaSnapshot para verificar SLA da empresa primeiro - Fallback: empresa > categoria > padrao ## Modal de Exclusao de Automacoes - Substitui confirm() nativo por Dialog gracioso - Segue padrao do delete-ticket-dialog 🤖 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
b3fcbcc682
commit
33f0cc2e13
4 changed files with 404 additions and 22 deletions
272
convex/companySlas.ts
Normal file
272
convex/companySlas.ts
Normal file
|
|
@ -0,0 +1,272 @@
|
||||||
|
import { mutation, query } from "./_generated/server"
|
||||||
|
import { ConvexError, v } from "convex/values"
|
||||||
|
|
||||||
|
import { requireAdmin } from "./rbac"
|
||||||
|
|
||||||
|
const PRIORITY_VALUES = ["URGENT", "HIGH", "MEDIUM", "LOW", "DEFAULT"] as const
|
||||||
|
const VALID_STATUSES = ["PENDING", "AWAITING_ATTENDANCE", "PAUSED", "RESOLVED"] as const
|
||||||
|
const VALID_TIME_MODES = ["business", "calendar"] as const
|
||||||
|
|
||||||
|
type CompanySlaRuleInput = {
|
||||||
|
priority: string
|
||||||
|
categoryId?: string | null
|
||||||
|
responseTargetMinutes?: number | null
|
||||||
|
responseMode?: string | null
|
||||||
|
solutionTargetMinutes?: number | null
|
||||||
|
solutionMode?: string | null
|
||||||
|
alertThreshold?: number | null
|
||||||
|
pauseStatuses?: string[] | null
|
||||||
|
calendarType?: string | null
|
||||||
|
}
|
||||||
|
|
||||||
|
const ruleInput = v.object({
|
||||||
|
priority: v.string(),
|
||||||
|
categoryId: v.optional(v.union(v.id("ticketCategories"), v.null())),
|
||||||
|
responseTargetMinutes: v.optional(v.number()),
|
||||||
|
responseMode: v.optional(v.string()),
|
||||||
|
solutionTargetMinutes: v.optional(v.number()),
|
||||||
|
solutionMode: v.optional(v.string()),
|
||||||
|
alertThreshold: v.optional(v.number()),
|
||||||
|
pauseStatuses: v.optional(v.array(v.string())),
|
||||||
|
calendarType: v.optional(v.string()),
|
||||||
|
})
|
||||||
|
|
||||||
|
function normalizePriority(value: string) {
|
||||||
|
const upper = value.trim().toUpperCase()
|
||||||
|
return PRIORITY_VALUES.includes(upper as (typeof PRIORITY_VALUES)[number]) ? upper : "DEFAULT"
|
||||||
|
}
|
||||||
|
|
||||||
|
function sanitizeTime(value?: number | null) {
|
||||||
|
if (typeof value !== "number" || !Number.isFinite(value) || value <= 0) return undefined
|
||||||
|
return Math.round(value)
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeMode(value?: string | null) {
|
||||||
|
if (!value) return "calendar"
|
||||||
|
const normalized = value.toLowerCase()
|
||||||
|
return VALID_TIME_MODES.includes(normalized as (typeof VALID_TIME_MODES)[number]) ? normalized : "calendar"
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizeThreshold(value?: number | null) {
|
||||||
|
if (typeof value !== "number" || Number.isNaN(value)) {
|
||||||
|
return 0.8
|
||||||
|
}
|
||||||
|
const clamped = Math.min(Math.max(value, 0.1), 0.95)
|
||||||
|
return Math.round(clamped * 100) / 100
|
||||||
|
}
|
||||||
|
|
||||||
|
function normalizePauseStatuses(value?: string[] | null) {
|
||||||
|
if (!Array.isArray(value)) return ["PAUSED"]
|
||||||
|
const normalized = new Set<string>()
|
||||||
|
for (const status of value) {
|
||||||
|
if (typeof status !== "string") continue
|
||||||
|
const upper = status.trim().toUpperCase()
|
||||||
|
if (VALID_STATUSES.includes(upper as (typeof VALID_STATUSES)[number])) {
|
||||||
|
normalized.add(upper)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if (normalized.size === 0) {
|
||||||
|
normalized.add("PAUSED")
|
||||||
|
}
|
||||||
|
return Array.from(normalized)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Lista todas as empresas que possuem SLA customizado
|
||||||
|
export const listCompaniesWithCustomSla = query({
|
||||||
|
args: {
|
||||||
|
tenantId: v.string(),
|
||||||
|
viewerId: v.id("users"),
|
||||||
|
},
|
||||||
|
handler: async (ctx, { tenantId, viewerId }) => {
|
||||||
|
await requireAdmin(ctx, viewerId, tenantId)
|
||||||
|
|
||||||
|
// Busca todas as configurações de SLA por empresa
|
||||||
|
const allSettings = await ctx.db
|
||||||
|
.query("companySlaSettings")
|
||||||
|
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId))
|
||||||
|
.take(1000)
|
||||||
|
|
||||||
|
// Agrupa por companyId para evitar duplicatas
|
||||||
|
const companyIds = [...new Set(allSettings.map((s) => s.companyId))]
|
||||||
|
|
||||||
|
// Busca dados das empresas
|
||||||
|
const companies = await Promise.all(
|
||||||
|
companyIds.map(async (companyId) => {
|
||||||
|
const company = await ctx.db.get(companyId)
|
||||||
|
if (!company) return null
|
||||||
|
const rulesCount = allSettings.filter((s) => s.companyId === companyId).length
|
||||||
|
return {
|
||||||
|
companyId,
|
||||||
|
companyName: company.name,
|
||||||
|
companySlug: company.slug,
|
||||||
|
rulesCount,
|
||||||
|
}
|
||||||
|
})
|
||||||
|
)
|
||||||
|
|
||||||
|
return companies.filter(Boolean)
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Busca as regras de SLA de uma empresa específica
|
||||||
|
export const get = query({
|
||||||
|
args: {
|
||||||
|
tenantId: v.string(),
|
||||||
|
viewerId: v.id("users"),
|
||||||
|
companyId: v.id("companies"),
|
||||||
|
},
|
||||||
|
handler: async (ctx, { tenantId, viewerId, companyId }) => {
|
||||||
|
await requireAdmin(ctx, viewerId, tenantId)
|
||||||
|
|
||||||
|
const company = await ctx.db.get(companyId)
|
||||||
|
if (!company || company.tenantId !== tenantId) {
|
||||||
|
throw new ConvexError("Empresa não encontrada")
|
||||||
|
}
|
||||||
|
|
||||||
|
const records = await ctx.db
|
||||||
|
.query("companySlaSettings")
|
||||||
|
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
||||||
|
.take(100)
|
||||||
|
|
||||||
|
// Busca nomes das categorias referenciadas
|
||||||
|
const categoryIds = [...new Set(records.filter((r) => r.categoryId).map((r) => r.categoryId!))]
|
||||||
|
const categories = await Promise.all(categoryIds.map((id) => ctx.db.get(id)))
|
||||||
|
const categoryNames = new Map(
|
||||||
|
categories.filter(Boolean).map((c) => [c!._id, c!.name])
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
companyId,
|
||||||
|
companyName: company.name,
|
||||||
|
rules: records.map((record) => ({
|
||||||
|
priority: record.priority,
|
||||||
|
categoryId: record.categoryId ?? null,
|
||||||
|
categoryName: record.categoryId ? categoryNames.get(record.categoryId) ?? null : null,
|
||||||
|
responseTargetMinutes: record.responseTargetMinutes ?? null,
|
||||||
|
responseMode: record.responseMode ?? "calendar",
|
||||||
|
solutionTargetMinutes: record.solutionTargetMinutes ?? null,
|
||||||
|
solutionMode: record.solutionMode ?? "calendar",
|
||||||
|
alertThreshold: record.alertThreshold ?? 0.8,
|
||||||
|
pauseStatuses: record.pauseStatuses ?? ["PAUSED"],
|
||||||
|
})),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Salva as regras de SLA de uma empresa
|
||||||
|
export const save = mutation({
|
||||||
|
args: {
|
||||||
|
tenantId: v.string(),
|
||||||
|
actorId: v.id("users"),
|
||||||
|
companyId: v.id("companies"),
|
||||||
|
rules: v.array(ruleInput),
|
||||||
|
},
|
||||||
|
handler: async (ctx, { tenantId, actorId, companyId, rules }) => {
|
||||||
|
await requireAdmin(ctx, actorId, tenantId)
|
||||||
|
|
||||||
|
const company = await ctx.db.get(companyId)
|
||||||
|
if (!company || company.tenantId !== tenantId) {
|
||||||
|
throw new ConvexError("Empresa não encontrada")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Valida categorias referenciadas
|
||||||
|
for (const rule of rules) {
|
||||||
|
if (rule.categoryId) {
|
||||||
|
const category = await ctx.db.get(rule.categoryId)
|
||||||
|
if (!category || category.tenantId !== tenantId) {
|
||||||
|
throw new ConvexError(`Categoria inválida: ${rule.categoryId}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const sanitized = sanitizeRules(rules)
|
||||||
|
|
||||||
|
// Remove regras existentes da empresa
|
||||||
|
const existing = await ctx.db
|
||||||
|
.query("companySlaSettings")
|
||||||
|
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
||||||
|
.take(100)
|
||||||
|
|
||||||
|
await Promise.all(existing.map((record) => ctx.db.delete(record._id)))
|
||||||
|
|
||||||
|
// Insere novas regras
|
||||||
|
const now = Date.now()
|
||||||
|
for (const rule of sanitized) {
|
||||||
|
await ctx.db.insert("companySlaSettings", {
|
||||||
|
tenantId,
|
||||||
|
companyId,
|
||||||
|
categoryId: rule.categoryId ?? undefined,
|
||||||
|
priority: rule.priority,
|
||||||
|
responseTargetMinutes: rule.responseTargetMinutes,
|
||||||
|
responseMode: rule.responseMode,
|
||||||
|
solutionTargetMinutes: rule.solutionTargetMinutes,
|
||||||
|
solutionMode: rule.solutionMode,
|
||||||
|
alertThreshold: rule.alertThreshold,
|
||||||
|
pauseStatuses: rule.pauseStatuses,
|
||||||
|
calendarType: rule.calendarType ?? undefined,
|
||||||
|
createdAt: now,
|
||||||
|
updatedAt: now,
|
||||||
|
actorId,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
// Remove todas as regras de SLA de uma empresa
|
||||||
|
export const remove = mutation({
|
||||||
|
args: {
|
||||||
|
tenantId: v.string(),
|
||||||
|
actorId: v.id("users"),
|
||||||
|
companyId: v.id("companies"),
|
||||||
|
},
|
||||||
|
handler: async (ctx, { tenantId, actorId, companyId }) => {
|
||||||
|
await requireAdmin(ctx, actorId, tenantId)
|
||||||
|
|
||||||
|
const company = await ctx.db.get(companyId)
|
||||||
|
if (!company || company.tenantId !== tenantId) {
|
||||||
|
throw new ConvexError("Empresa não encontrada")
|
||||||
|
}
|
||||||
|
|
||||||
|
const existing = await ctx.db
|
||||||
|
.query("companySlaSettings")
|
||||||
|
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
||||||
|
.take(100)
|
||||||
|
|
||||||
|
await Promise.all(existing.map((record) => ctx.db.delete(record._id)))
|
||||||
|
|
||||||
|
return { ok: true }
|
||||||
|
},
|
||||||
|
})
|
||||||
|
|
||||||
|
function sanitizeRules(rules: CompanySlaRuleInput[]) {
|
||||||
|
// Chave única: categoryId + priority
|
||||||
|
const normalized: Map<string, ReturnType<typeof buildRule>> = new Map()
|
||||||
|
|
||||||
|
for (const rule of rules) {
|
||||||
|
const built = buildRule(rule)
|
||||||
|
const key = `${built.categoryId ?? "ALL"}-${built.priority}`
|
||||||
|
normalized.set(key, built)
|
||||||
|
}
|
||||||
|
|
||||||
|
return Array.from(normalized.values())
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildRule(rule: CompanySlaRuleInput) {
|
||||||
|
const priority = normalizePriority(rule.priority)
|
||||||
|
const responseTargetMinutes = sanitizeTime(rule.responseTargetMinutes)
|
||||||
|
const solutionTargetMinutes = sanitizeTime(rule.solutionTargetMinutes)
|
||||||
|
|
||||||
|
return {
|
||||||
|
priority,
|
||||||
|
categoryId: rule.categoryId ?? null,
|
||||||
|
responseTargetMinutes,
|
||||||
|
responseMode: normalizeMode(rule.responseMode),
|
||||||
|
solutionTargetMinutes,
|
||||||
|
solutionMode: normalizeMode(rule.solutionMode),
|
||||||
|
alertThreshold: normalizeThreshold(rule.alertThreshold),
|
||||||
|
pauseStatuses: normalizePauseStatuses(rule.pauseStatuses),
|
||||||
|
calendarType: rule.calendarType ?? null,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
@ -594,6 +594,29 @@ export default defineSchema({
|
||||||
.index("by_tenant_category_priority", ["tenantId", "categoryId", "priority"])
|
.index("by_tenant_category_priority", ["tenantId", "categoryId", "priority"])
|
||||||
.index("by_tenant_category", ["tenantId", "categoryId"]),
|
.index("by_tenant_category", ["tenantId", "categoryId"]),
|
||||||
|
|
||||||
|
// SLA por empresa - permite configurar políticas de SLA específicas por cliente
|
||||||
|
// Quando um ticket é criado, o sistema busca primeiro aqui antes de usar categorySlaSettings
|
||||||
|
companySlaSettings: defineTable({
|
||||||
|
tenantId: v.string(),
|
||||||
|
companyId: v.id("companies"),
|
||||||
|
// Se categoryId for null, aplica-se a todas as categorias da empresa
|
||||||
|
categoryId: v.optional(v.id("ticketCategories")),
|
||||||
|
priority: v.string(), // URGENT, HIGH, MEDIUM, LOW, DEFAULT
|
||||||
|
responseTargetMinutes: v.optional(v.number()),
|
||||||
|
responseMode: v.optional(v.string()), // "business" | "calendar"
|
||||||
|
solutionTargetMinutes: v.optional(v.number()),
|
||||||
|
solutionMode: v.optional(v.string()), // "business" | "calendar"
|
||||||
|
alertThreshold: v.optional(v.number()), // 0.1 a 0.95 (ex: 0.8 = 80%)
|
||||||
|
pauseStatuses: v.optional(v.array(v.string())),
|
||||||
|
calendarType: v.optional(v.string()),
|
||||||
|
createdAt: v.number(),
|
||||||
|
updatedAt: v.number(),
|
||||||
|
actorId: v.optional(v.id("users")),
|
||||||
|
})
|
||||||
|
.index("by_tenant_company", ["tenantId", "companyId"])
|
||||||
|
.index("by_tenant_company_category", ["tenantId", "companyId", "categoryId"])
|
||||||
|
.index("by_tenant_company_category_priority", ["tenantId", "companyId", "categoryId", "priority"]),
|
||||||
|
|
||||||
ticketFields: defineTable({
|
ticketFields: defineTable({
|
||||||
tenantId: v.string(),
|
tenantId: v.string(),
|
||||||
key: v.string(),
|
key: v.string(),
|
||||||
|
|
|
||||||
|
|
@ -272,25 +272,74 @@ async function resolveTicketSlaSnapshot(
|
||||||
ctx: AnyCtx,
|
ctx: AnyCtx,
|
||||||
tenantId: string,
|
tenantId: string,
|
||||||
category: Doc<"ticketCategories"> | null,
|
category: Doc<"ticketCategories"> | null,
|
||||||
priority: string
|
priority: string,
|
||||||
|
companyId?: Id<"companies"> | null
|
||||||
): Promise<TicketSlaSnapshot | null> {
|
): Promise<TicketSlaSnapshot | null> {
|
||||||
if (!category) {
|
if (!category) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
const normalizedPriority = priority.trim().toUpperCase();
|
const normalizedPriority = priority.trim().toUpperCase();
|
||||||
const rule =
|
|
||||||
(await ctx.db
|
// 1. Primeiro, tenta buscar SLA específico da empresa (se companyId foi informado)
|
||||||
.query("categorySlaSettings")
|
let rule: {
|
||||||
.withIndex("by_tenant_category_priority", (q) =>
|
responseTargetMinutes?: number;
|
||||||
q.eq("tenantId", tenantId).eq("categoryId", category._id).eq("priority", normalizedPriority)
|
responseMode?: string;
|
||||||
|
solutionTargetMinutes?: number;
|
||||||
|
solutionMode?: string;
|
||||||
|
alertThreshold?: number;
|
||||||
|
pauseStatuses?: string[];
|
||||||
|
} | null = null;
|
||||||
|
|
||||||
|
if (companyId) {
|
||||||
|
// Tenta: empresa + categoria + prioridade
|
||||||
|
rule = await ctx.db
|
||||||
|
.query("companySlaSettings")
|
||||||
|
.withIndex("by_tenant_company_category_priority", (q) =>
|
||||||
|
q.eq("tenantId", tenantId).eq("companyId", companyId).eq("categoryId", category._id).eq("priority", normalizedPriority)
|
||||||
)
|
)
|
||||||
.first()) ??
|
.first();
|
||||||
(await ctx.db
|
|
||||||
.query("categorySlaSettings")
|
// Fallback: empresa + categoria + DEFAULT
|
||||||
.withIndex("by_tenant_category_priority", (q) =>
|
if (!rule) {
|
||||||
q.eq("tenantId", tenantId).eq("categoryId", category._id).eq("priority", "DEFAULT")
|
rule = await ctx.db
|
||||||
)
|
.query("companySlaSettings")
|
||||||
.first());
|
.withIndex("by_tenant_company_category_priority", (q) =>
|
||||||
|
q.eq("tenantId", tenantId).eq("companyId", companyId).eq("categoryId", category._id).eq("priority", "DEFAULT")
|
||||||
|
)
|
||||||
|
.first();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Fallback: empresa + todas categorias (categoryId null) + prioridade
|
||||||
|
if (!rule) {
|
||||||
|
const allCategoriesRules = await ctx.db
|
||||||
|
.query("companySlaSettings")
|
||||||
|
.withIndex("by_tenant_company", (q) => q.eq("tenantId", tenantId).eq("companyId", companyId))
|
||||||
|
.filter((q) => q.eq(q.field("categoryId"), undefined))
|
||||||
|
.take(10);
|
||||||
|
|
||||||
|
rule = allCategoriesRules.find((r) => r.priority === normalizedPriority) ??
|
||||||
|
allCategoriesRules.find((r) => r.priority === "DEFAULT") ??
|
||||||
|
null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// 2. Se não encontrou SLA da empresa, usa SLA da categoria (comportamento padrão)
|
||||||
|
if (!rule) {
|
||||||
|
rule =
|
||||||
|
(await ctx.db
|
||||||
|
.query("categorySlaSettings")
|
||||||
|
.withIndex("by_tenant_category_priority", (q) =>
|
||||||
|
q.eq("tenantId", tenantId).eq("categoryId", category._id).eq("priority", normalizedPriority)
|
||||||
|
)
|
||||||
|
.first()) ??
|
||||||
|
(await ctx.db
|
||||||
|
.query("categorySlaSettings")
|
||||||
|
.withIndex("by_tenant_category_priority", (q) =>
|
||||||
|
q.eq("tenantId", tenantId).eq("categoryId", category._id).eq("priority", "DEFAULT")
|
||||||
|
)
|
||||||
|
.first());
|
||||||
|
}
|
||||||
|
|
||||||
if (!rule) {
|
if (!rule) {
|
||||||
return null;
|
return null;
|
||||||
}
|
}
|
||||||
|
|
@ -2325,7 +2374,7 @@ export const create = mutation({
|
||||||
avatarUrl: requester.avatarUrl ?? undefined,
|
avatarUrl: requester.avatarUrl ?? undefined,
|
||||||
teams: requester.teams ?? undefined,
|
teams: requester.teams ?? undefined,
|
||||||
}
|
}
|
||||||
const slaSnapshot = await resolveTicketSlaSnapshot(ctx, args.tenantId, category as Doc<"ticketCategories"> | null, args.priority)
|
// Resolve a empresa primeiro para poder verificar SLA específico
|
||||||
let companyDoc = requester.companyId ? (await ctx.db.get(requester.companyId)) : null
|
let companyDoc = requester.companyId ? (await ctx.db.get(requester.companyId)) : null
|
||||||
if (!companyDoc && machineDoc?.companyId) {
|
if (!companyDoc && machineDoc?.companyId) {
|
||||||
const candidateCompany = await ctx.db.get(machineDoc.companyId)
|
const candidateCompany = await ctx.db.get(machineDoc.companyId)
|
||||||
|
|
@ -2337,6 +2386,8 @@ export const create = mutation({
|
||||||
? { name: companyDoc.name, slug: companyDoc.slug, isAvulso: companyDoc.isAvulso ?? undefined }
|
? { name: companyDoc.name, slug: companyDoc.slug, isAvulso: companyDoc.isAvulso ?? undefined }
|
||||||
: undefined
|
: undefined
|
||||||
const resolvedCompanyId = companyDoc?._id ?? requester.companyId ?? undefined
|
const resolvedCompanyId = companyDoc?._id ?? requester.companyId ?? undefined
|
||||||
|
// Resolve SLA passando companyId para verificar regras específicas da empresa
|
||||||
|
const slaSnapshot = await resolveTicketSlaSnapshot(ctx, args.tenantId, category as Doc<"ticketCategories"> | null, args.priority, resolvedCompanyId)
|
||||||
|
|
||||||
let checklist = manualChecklist
|
let checklist = manualChecklist
|
||||||
for (const templateId of args.checklistTemplateIds ?? []) {
|
for (const templateId of args.checklistTemplateIds ?? []) {
|
||||||
|
|
|
||||||
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
import { useMemo, useState } from "react"
|
import { useMemo, useState } from "react"
|
||||||
import { useMutation, useQuery } from "convex/react"
|
import { useMutation, useQuery } from "convex/react"
|
||||||
import { History, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
import { AlertTriangle, History, MoreHorizontal, Pencil, Plus, Trash2 } from "lucide-react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { api } from "@/convex/_generated/api"
|
import { api } from "@/convex/_generated/api"
|
||||||
|
|
@ -12,7 +12,7 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants"
|
||||||
import { Badge } from "@/components/ui/badge"
|
import { Badge } from "@/components/ui/badge"
|
||||||
import { Button } from "@/components/ui/button"
|
import { Button } from "@/components/ui/button"
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
import { Dialog, DialogTrigger } from "@/components/ui/dialog"
|
import { Dialog, DialogContent, DialogDescription, DialogHeader, DialogTitle, DialogTrigger } from "@/components/ui/dialog"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from "@/components/ui/dropdown-menu"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
|
||||||
|
|
@ -81,6 +81,9 @@ export function AutomationsManager() {
|
||||||
const [editing, setEditing] = useState<AutomationRow | null>(null)
|
const [editing, setEditing] = useState<AutomationRow | null>(null)
|
||||||
const [runsOpen, setRunsOpen] = useState(false)
|
const [runsOpen, setRunsOpen] = useState(false)
|
||||||
const [runsAutomation, setRunsAutomation] = useState<AutomationRow | null>(null)
|
const [runsAutomation, setRunsAutomation] = useState<AutomationRow | null>(null)
|
||||||
|
const [deleteOpen, setDeleteOpen] = useState(false)
|
||||||
|
const [deletingAutomation, setDeletingAutomation] = useState<AutomationRow | null>(null)
|
||||||
|
const [deleteLoading, setDeleteLoading] = useState(false)
|
||||||
|
|
||||||
const list = useQuery(
|
const list = useQuery(
|
||||||
api.automations.list,
|
api.automations.list,
|
||||||
|
|
@ -134,19 +137,27 @@ export function AutomationsManager() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const handleDelete = async (row: AutomationRow) => {
|
const handleDeleteClick = (row: AutomationRow) => {
|
||||||
if (!convexUserId) return
|
setDeletingAutomation(row)
|
||||||
const ok = confirm(`Excluir a automação "${row.name}"?`)
|
setDeleteOpen(true)
|
||||||
if (!ok) return
|
}
|
||||||
|
|
||||||
|
const handleDeleteConfirm = async () => {
|
||||||
|
if (!convexUserId || !deletingAutomation) return
|
||||||
|
setDeleteLoading(true)
|
||||||
try {
|
try {
|
||||||
await removeAutomation({
|
await removeAutomation({
|
||||||
tenantId,
|
tenantId,
|
||||||
viewerId: convexUserId as Id<"users">,
|
viewerId: convexUserId as Id<"users">,
|
||||||
automationId: row.id,
|
automationId: deletingAutomation.id,
|
||||||
})
|
})
|
||||||
toast.success("Automação excluída")
|
toast.success("Automação excluída")
|
||||||
|
setDeleteOpen(false)
|
||||||
|
setDeletingAutomation(null)
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
toast.error(error instanceof Error ? error.message : "Falha ao excluir automação")
|
toast.error(error instanceof Error ? error.message : "Falha ao excluir automação")
|
||||||
|
} finally {
|
||||||
|
setDeleteLoading(false)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -340,7 +351,7 @@ export function AutomationsManager() {
|
||||||
<Pencil className="size-4" />
|
<Pencil className="size-4" />
|
||||||
Editar
|
Editar
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
<DropdownMenuItem variant="destructive" onClick={() => handleDelete(row)} className="gap-2">
|
<DropdownMenuItem variant="destructive" onClick={() => handleDeleteClick(row)} className="gap-2">
|
||||||
<Trash2 className="size-4" />
|
<Trash2 className="size-4" />
|
||||||
Excluir
|
Excluir
|
||||||
</DropdownMenuItem>
|
</DropdownMenuItem>
|
||||||
|
|
@ -355,6 +366,31 @@ export function AutomationsManager() {
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
</CardContent>
|
</CardContent>
|
||||||
|
|
||||||
|
{/* Modal de confirmação de exclusão */}
|
||||||
|
<Dialog open={deleteOpen} onOpenChange={(open) => {
|
||||||
|
setDeleteOpen(open)
|
||||||
|
if (!open) setDeletingAutomation(null)
|
||||||
|
}}>
|
||||||
|
<DialogContent className="max-w-md">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2 text-destructive">
|
||||||
|
<AlertTriangle className="size-5" /> Excluir automação
|
||||||
|
</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Esta ação é permanente e removerá a automação <span className="font-semibold text-neutral-700">"{deletingAutomation?.name}"</span> e todo o seu histórico de execuções.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="flex justify-end gap-2 pt-2">
|
||||||
|
<Button variant="outline" onClick={() => setDeleteOpen(false)} disabled={deleteLoading}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={handleDeleteConfirm} disabled={deleteLoading} className="gap-2">
|
||||||
|
{deleteLoading ? "Excluindo..." : (<><Trash2 className="size-4" /> Excluir</>)}
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</Card>
|
</Card>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue