Add company management editing and deletion
This commit is contained in:
parent
b60f27b2dc
commit
17f9f00343
2 changed files with 137 additions and 7 deletions
|
|
@ -1,5 +1,6 @@
|
||||||
import { NextResponse } from "next/server"
|
import { NextResponse } from "next/server"
|
||||||
|
|
||||||
|
import { Prisma } from "@prisma/client"
|
||||||
import { prisma } from "@/lib/prisma"
|
import { prisma } from "@/lib/prisma"
|
||||||
import { assertAdminSession } from "@/lib/auth-server"
|
import { assertAdminSession } from "@/lib/auth-server"
|
||||||
|
|
||||||
|
|
@ -46,3 +47,36 @@ export async function PATCH(request: Request, { params }: { params: Promise<{ id
|
||||||
return NextResponse.json({ error: "Falha ao atualizar empresa" }, { status: 500 })
|
return NextResponse.json({ error: "Falha ao atualizar empresa" }, { status: 500 })
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function DELETE(_: Request, { params }: { params: Promise<{ id: string }> }) {
|
||||||
|
const session = await assertAdminSession()
|
||||||
|
if (!session) return NextResponse.json({ error: "Não autorizado" }, { status: 401 })
|
||||||
|
const { id } = await params
|
||||||
|
|
||||||
|
const company = await prisma.company.findUnique({
|
||||||
|
where: { id },
|
||||||
|
select: { id: true, tenantId: true, name: true },
|
||||||
|
})
|
||||||
|
|
||||||
|
if (!company) {
|
||||||
|
return NextResponse.json({ error: "Empresa não encontrada" }, { status: 404 })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (company.tenantId !== (session.user.tenantId ?? company.tenantId)) {
|
||||||
|
return NextResponse.json({ error: "Acesso negado" }, { status: 403 })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
await prisma.company.delete({ where: { id: company.id } })
|
||||||
|
return NextResponse.json({ ok: true })
|
||||||
|
} catch (error) {
|
||||||
|
if (error instanceof Prisma.PrismaClientKnownRequestError && error.code === "P2003") {
|
||||||
|
return NextResponse.json(
|
||||||
|
{ error: "Não é possível remover esta empresa pois existem registros vinculados." },
|
||||||
|
{ status: 409 }
|
||||||
|
)
|
||||||
|
}
|
||||||
|
console.error("Failed to delete company", error)
|
||||||
|
return NextResponse.json({ error: "Falha ao excluir empresa" }, { status: 500 })
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
|
||||||
|
|
@ -1,6 +1,6 @@
|
||||||
"use client"
|
"use client"
|
||||||
|
|
||||||
import { useCallback, useEffect, useState, useTransition } from "react"
|
import { useCallback, useEffect, useMemo, useState, useTransition } from "react"
|
||||||
import { toast } from "sonner"
|
import { toast } from "sonner"
|
||||||
|
|
||||||
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
|
||||||
|
|
@ -8,6 +8,7 @@ import { Button } from "@/components/ui/button"
|
||||||
import { Input } from "@/components/ui/input"
|
import { Input } from "@/components/ui/input"
|
||||||
import { Label } from "@/components/ui/label"
|
import { Label } from "@/components/ui/label"
|
||||||
import { Checkbox } from "@/components/ui/checkbox"
|
import { Checkbox } from "@/components/ui/checkbox"
|
||||||
|
import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog"
|
||||||
import {
|
import {
|
||||||
Table,
|
Table,
|
||||||
TableBody,
|
TableBody,
|
||||||
|
|
@ -37,6 +38,8 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
const [form, setForm] = useState<Partial<Company>>({})
|
const [form, setForm] = useState<Partial<Company>>({})
|
||||||
const [editingId, setEditingId] = useState<string | null>(null)
|
const [editingId, setEditingId] = useState<string | null>(null)
|
||||||
const [lastAlerts, setLastAlerts] = useState<Record<string, { createdAt: number; usagePct: number; threshold: number } | null>>({})
|
const [lastAlerts, setLastAlerts] = useState<Record<string, { createdAt: number; usagePct: number; threshold: number } | null>>({})
|
||||||
|
const [deleteId, setDeleteId] = useState<string | null>(null)
|
||||||
|
const [isDeleting, setIsDeleting] = useState(false)
|
||||||
|
|
||||||
const resetForm = () => setForm({})
|
const resetForm = () => setForm({})
|
||||||
|
|
||||||
|
|
@ -49,7 +52,10 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
|
|
||||||
function handleEdit(c: Company) {
|
function handleEdit(c: Company) {
|
||||||
setEditingId(c.id)
|
setEditingId(c.id)
|
||||||
setForm({ ...c })
|
setForm({
|
||||||
|
...c,
|
||||||
|
contractedHoursPerMonth: c.contractedHoursPerMonth ?? undefined,
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
const loadLastAlerts = useCallback(async (list: Company[] = companies) => {
|
const loadLastAlerts = useCallback(async (list: Company[] = companies) => {
|
||||||
|
|
@ -68,6 +74,11 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
|
|
||||||
async function handleSubmit(e: React.FormEvent) {
|
async function handleSubmit(e: React.FormEvent) {
|
||||||
e.preventDefault()
|
e.preventDefault()
|
||||||
|
const contractedHours =
|
||||||
|
typeof form.contractedHoursPerMonth === "number" && Number.isFinite(form.contractedHoursPerMonth)
|
||||||
|
? form.contractedHoursPerMonth
|
||||||
|
: null
|
||||||
|
|
||||||
const payload = {
|
const payload = {
|
||||||
name: form.name?.trim(),
|
name: form.name?.trim(),
|
||||||
slug: form.slug?.trim(),
|
slug: form.slug?.trim(),
|
||||||
|
|
@ -77,6 +88,7 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
phone: form.phone?.trim() || null,
|
phone: form.phone?.trim() || null,
|
||||||
description: form.description?.trim() || null,
|
description: form.description?.trim() || null,
|
||||||
address: form.address?.trim() || null,
|
address: form.address?.trim() || null,
|
||||||
|
contractedHoursPerMonth: contractedHours,
|
||||||
}
|
}
|
||||||
if (!payload.name || !payload.slug) {
|
if (!payload.name || !payload.slug) {
|
||||||
toast.error("Informe nome e slug válidos")
|
toast.error("Informe nome e slug válidos")
|
||||||
|
|
@ -123,18 +135,53 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
})
|
})
|
||||||
if (!r.ok) throw new Error("toggle_failed")
|
if (!r.ok) throw new Error("toggle_failed")
|
||||||
await refresh()
|
await refresh()
|
||||||
|
toast.success(`Cliente ${!c.isAvulso ? "marcado como avulso" : "marcado como recorrente"}`)
|
||||||
} catch {
|
} catch {
|
||||||
toast.error("Não foi possível atualizar o cliente avulso")
|
toast.error("Não foi possível atualizar o cliente avulso")
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function handleDeleteConfirmed() {
|
||||||
|
if (!deleteId) return
|
||||||
|
setIsDeleting(true)
|
||||||
|
try {
|
||||||
|
const response = await fetch(`/api/admin/companies/${deleteId}`, {
|
||||||
|
method: "DELETE",
|
||||||
|
credentials: "include",
|
||||||
|
})
|
||||||
|
if (!response.ok) {
|
||||||
|
const data = await response.json().catch(() => ({}))
|
||||||
|
throw new Error(data.error ?? "Falha ao excluir empresa")
|
||||||
|
}
|
||||||
|
toast.success("Empresa removida")
|
||||||
|
if (editingId === deleteId) {
|
||||||
|
resetForm()
|
||||||
|
setEditingId(null)
|
||||||
|
}
|
||||||
|
await refresh()
|
||||||
|
} catch (error) {
|
||||||
|
const message = error instanceof Error ? error.message : "Não foi possível remover a empresa"
|
||||||
|
toast.error(message)
|
||||||
|
} finally {
|
||||||
|
setIsDeleting(false)
|
||||||
|
setDeleteId(null)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const editingCompanyName = useMemo(() => companies.find((company) => company.id === editingId)?.name ?? null, [companies, editingId])
|
||||||
|
const deleteTarget = useMemo(() => companies.find((company) => company.id === deleteId) ?? null, [companies, deleteId])
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-6">
|
<div className="space-y-6">
|
||||||
<Card className="border-slate-200">
|
<Card className="border-slate-200">
|
||||||
<CardHeader>
|
<CardHeader>
|
||||||
<CardTitle>Nova empresa</CardTitle>
|
<CardTitle>{editingId ? `Editar empresa${editingCompanyName ? ` · ${editingCompanyName}` : ""}` : "Nova empresa"}</CardTitle>
|
||||||
<CardDescription>Cadastre um cliente/empresa e defina se é avulso.</CardDescription>
|
<CardDescription>
|
||||||
|
{editingId
|
||||||
|
? "Atualize os dados cadastrais e as informações de faturamento do cliente selecionado."
|
||||||
|
: "Cadastre um cliente/empresa e defina se é avulso."}
|
||||||
|
</CardDescription>
|
||||||
</CardHeader>
|
</CardHeader>
|
||||||
<CardContent>
|
<CardContent>
|
||||||
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4 md:grid-cols-2">
|
||||||
|
|
@ -146,6 +193,10 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
<Label>Slug</Label>
|
<Label>Slug</Label>
|
||||||
<Input value={form.slug ?? ""} onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value }))} />
|
<Input value={form.slug ?? ""} onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value }))} />
|
||||||
</div>
|
</div>
|
||||||
|
<div className="grid gap-2 md:col-span-2">
|
||||||
|
<Label>Descrição</Label>
|
||||||
|
<Input value={form.description ?? ""} onChange={(e) => setForm((p) => ({ ...p, description: e.target.value }))} placeholder="Resumo, segmento ou observações internas" />
|
||||||
|
</div>
|
||||||
<div className="grid gap-2">
|
<div className="grid gap-2">
|
||||||
<Label>CNPJ</Label>
|
<Label>CNPJ</Label>
|
||||||
<Input value={form.cnpj ?? ""} onChange={(e) => setForm((p) => ({ ...p, cnpj: e.target.value }))} />
|
<Input value={form.cnpj ?? ""} onChange={(e) => setForm((p) => ({ ...p, cnpj: e.target.value }))} />
|
||||||
|
|
@ -230,9 +281,19 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
: "—"}
|
: "—"}
|
||||||
</TableCell>
|
</TableCell>
|
||||||
<TableCell>
|
<TableCell>
|
||||||
<Button size="sm" variant="outline" onClick={() => handleEdit(c)}>
|
<div className="flex flex-wrap gap-2">
|
||||||
Editar
|
<Button size="sm" variant="outline" onClick={() => handleEdit(c)}>
|
||||||
</Button>
|
Editar
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
size="sm"
|
||||||
|
variant="ghost"
|
||||||
|
className="text-red-600 transition-colors hover:bg-red-500/10"
|
||||||
|
onClick={() => setDeleteId(c.id)}
|
||||||
|
>
|
||||||
|
Remover
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
</TableCell>
|
</TableCell>
|
||||||
</TableRow>
|
</TableRow>
|
||||||
))}
|
))}
|
||||||
|
|
@ -240,6 +301,41 @@ export function AdminCompaniesManager({ initialCompanies }: { initialCompanies:
|
||||||
</Table>
|
</Table>
|
||||||
</CardContent>
|
</CardContent>
|
||||||
</Card>
|
</Card>
|
||||||
|
|
||||||
|
<Dialog
|
||||||
|
open={deleteId !== null}
|
||||||
|
onOpenChange={(open) => {
|
||||||
|
if (!open) {
|
||||||
|
setDeleteId(null)
|
||||||
|
}
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
<DialogContent>
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle>Excluir empresa</DialogTitle>
|
||||||
|
<DialogDescription>
|
||||||
|
Esta operação remove o cadastro do cliente e impede novos vínculos automáticos.
|
||||||
|
</DialogDescription>
|
||||||
|
</DialogHeader>
|
||||||
|
<div className="space-y-2 text-sm text-neutral-600">
|
||||||
|
<p>
|
||||||
|
Confirme a exclusão de{" "}
|
||||||
|
<span className="font-semibold text-neutral-900">{deleteTarget?.name ?? "empresa selecionada"}</span>.
|
||||||
|
</p>
|
||||||
|
<p className="text-xs text-neutral-500">
|
||||||
|
Registros históricos que apontem para a empresa poderão impedir a exclusão.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<DialogFooter>
|
||||||
|
<Button variant="outline" onClick={() => setDeleteId(null)} disabled={isDeleting}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button variant="destructive" onClick={() => void handleDeleteConfirmed()} disabled={isDeleting}>
|
||||||
|
{isDeleting ? "Removendo..." : "Remover empresa"}
|
||||||
|
</Button>
|
||||||
|
</DialogFooter>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue