From 7951bc25a38c1f817c8259e08707a9d227533615 Mon Sep 17 00:00:00 2001 From: Esdras Renan Date: Thu, 16 Oct 2025 22:28:12 -0300 Subject: [PATCH] feat: allow company deletion by detaching dependents --- docs/DEPLOY-RUNBOOK.md | 2 +- src/app/api/admin/companies/[id]/route.ts | 29 +++++++++---------- .../companies/admin-companies-manager.tsx | 22 +++++++++++--- 3 files changed, 32 insertions(+), 21 deletions(-) diff --git a/docs/DEPLOY-RUNBOOK.md b/docs/DEPLOY-RUNBOOK.md index 3cb8b30..1bcaa41 100644 --- a/docs/DEPLOY-RUNBOOK.md +++ b/docs/DEPLOY-RUNBOOK.md @@ -23,6 +23,7 @@ Resultado: front/back sobem com o novo código sem editar o stack a cada release - Mount fixo: `/home/renan/apps/sistema.current:/app` (não interpolar APP_DIR). - Comando inline (sem script), com migrations na subida: - `command: ["bash","-lc","corepack enable && corepack prepare pnpm@9 --activate && pnpm exec prisma migrate deploy && pnpm start -p 3000"]` + - **Se você optar por usar `/app/scripts/start-web.sh`** (como no workflow atual), ele já garante `pnpm@9` via Corepack/NPM antes de rodar Prisma/Next. Certifique-se de copiar esse arquivo para o build publicado; sem ele, a task cai com `pnpm: command not found`. - Env obrigatórias (URLs válidas): - `DATABASE_URL=file:/app/data/db.sqlite` - `NEXT_PUBLIC_CONVEX_URL=http://sistema_convex_backend:3210` @@ -121,4 +122,3 @@ docker service scale sistema_web=1 - Como o stack monta `/home/renan/apps/sistema.current`, um novo release exige apenas atualizar o symlink e forçar a task. O `stack.yml` só precisa ser redeployado quando você altera labels/envs/serviços. - Se a UI parecer não mudar, valide o mount/args via inspect, confira logs da task atual e force hard‑reload no navegador. - diff --git a/src/app/api/admin/companies/[id]/route.ts b/src/app/api/admin/companies/[id]/route.ts index 5303996..6ddcd04 100644 --- a/src/app/api/admin/companies/[id]/route.ts +++ b/src/app/api/admin/companies/[id]/route.ts @@ -77,23 +77,20 @@ export async function DELETE(_: Request, { params }: { params: Promise<{ id: str } try { - // Pré‑checagem para evitar 500 por FK: conta vínculos antes - const [usersCount, ticketsCount] = await Promise.all([ - prisma.user.count({ where: { companyId: company.id } }), - prisma.ticket.count({ where: { companyId: company.id } }), - ]) - if (usersCount > 0 || ticketsCount > 0) { - return NextResponse.json( - { - error: "Não é possível remover esta empresa pois existem registros vinculados.", - details: { users: usersCount, tickets: ticketsCount }, - }, - { status: 409 } - ) - } + const result = await prisma.$transaction(async (tx) => { + const users = await tx.user.updateMany({ + where: { companyId: company.id, tenantId: company.tenantId }, + data: { companyId: null }, + }) + const tickets = await tx.ticket.updateMany({ + where: { companyId: company.id, tenantId: company.tenantId }, + data: { companyId: null }, + }) + await tx.company.delete({ where: { id: company.id } }) + return { detachedUsers: users.count, detachedTickets: tickets.count } + }) - await prisma.company.delete({ where: { id: company.id } }) - return NextResponse.json({ ok: true }) + return NextResponse.json({ ok: true, ...result }) } catch (error) { if (error instanceof PrismaClientKnownRequestError && error.code === "P2003") { return NextResponse.json( diff --git a/src/components/admin/companies/admin-companies-manager.tsx b/src/components/admin/companies/admin-companies-manager.tsx index 3d13739..87311f1 100644 --- a/src/components/admin/companies/admin-companies-manager.tsx +++ b/src/components/admin/companies/admin-companies-manager.tsx @@ -184,11 +184,25 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: method: "DELETE", credentials: "include", }) - if (!response.ok) { - const data = await response.json().catch(() => ({})) - throw new Error(data.error ?? "Falha ao excluir empresa") + const data = (await response.json().catch(() => ({}))) as { + error?: string + detachedUsers?: number + detachedTickets?: number } - toast.success("Empresa removida") + if (!response.ok) { + throw new Error(data?.error ?? "Falha ao excluir empresa") + } + const detachedUsers = data?.detachedUsers ?? 0 + const detachedTickets = data?.detachedTickets ?? 0 + const details: string[] = [] + if (detachedUsers > 0) { + details.push(`${detachedUsers} usuário${detachedUsers > 1 ? "s" : ""} desvinculado${detachedUsers > 1 ? "s" : ""}`) + } + if (detachedTickets > 0) { + details.push(`${detachedTickets} ticket${detachedTickets > 1 ? "s" : ""} atualizado${detachedTickets > 1 ? "s" : ""}`) + } + const successMessage = details.length > 0 ? `Empresa removida (${details.join(", ")})` : "Empresa removida" + toast.success(successMessage) if (editingId === deleteId) { resetForm() setEditingId(null)