feat: CSV exports, PDF improvements, play internal/external with hour split, roles cleanup, admin companies with 'Cliente avulso', ticket list spacing/alignment fixes, status translations and mappings

This commit is contained in:
Esdras Renan 2025-10-07 13:42:45 -03:00
parent addd4ce6e8
commit 3bafcc5a0a
45 changed files with 1401 additions and 256 deletions

View file

@ -0,0 +1,213 @@
"use client"
import { useMemo, useState, useTransition } from "react"
import { toast } from "sonner"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Button } from "@/components/ui/button"
import { Input } from "@/components/ui/input"
import { Label } from "@/components/ui/label"
import { Checkbox } from "@/components/ui/checkbox"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
type Company = {
id: string
tenantId: string
name: string
slug: string
isAvulso: boolean
cnpj: string | null
domain: string | null
phone: string | null
description: string | null
address: string | null
}
export function AdminCompaniesManager({ initialCompanies }: { initialCompanies: Company[] }) {
const [companies, setCompanies] = useState<Company[]>(initialCompanies)
const [isPending, startTransition] = useTransition()
const [form, setForm] = useState<Partial<Company>>({})
const [editingId, setEditingId] = useState<string | null>(null)
const resetForm = () => setForm({})
async function refresh() {
const r = await fetch("/api/admin/companies", { credentials: "include" })
const json = (await r.json()) as { companies: Company[] }
setCompanies(json.companies)
}
function handleEdit(c: Company) {
setEditingId(c.id)
setForm({ ...c })
}
async function handleSubmit(e: React.FormEvent) {
e.preventDefault()
const payload = {
name: form.name?.trim(),
slug: form.slug?.trim(),
isAvulso: Boolean(form.isAvulso ?? false),
cnpj: form.cnpj?.trim() || null,
domain: form.domain?.trim() || null,
phone: form.phone?.trim() || null,
description: form.description?.trim() || null,
address: form.address?.trim() || null,
}
if (!payload.name || !payload.slug) {
toast.error("Informe nome e slug válidos")
return
}
startTransition(async () => {
toast.loading(editingId ? "Atualizando empresa..." : "Criando empresa...", { id: "companies" })
try {
if (editingId) {
const r = await fetch(`/api/admin/companies/${editingId}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
credentials: "include",
})
if (!r.ok) throw new Error("update_failed")
} else {
const r = await fetch(`/api/admin/companies`, {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify(payload),
credentials: "include",
})
if (!r.ok) throw new Error("create_failed")
}
await refresh()
resetForm()
setEditingId(null)
toast.success(editingId ? "Empresa atualizada" : "Empresa criada", { id: "companies" })
} catch {
toast.error("Não foi possível salvar", { id: "companies" })
}
})
}
async function toggleAvulso(c: Company) {
startTransition(async () => {
try {
const r = await fetch(`/api/admin/companies/${c.id}`, {
method: "PATCH",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ isAvulso: !c.isAvulso }),
credentials: "include",
})
if (!r.ok) throw new Error("toggle_failed")
await refresh()
} catch {
toast.error("Não foi possível atualizar o cliente avulso")
}
})
}
return (
<div className="space-y-6">
<Card className="border-slate-200">
<CardHeader>
<CardTitle>Nova empresa</CardTitle>
<CardDescription>Cadastre um cliente/empresa e defina se é avulso.</CardDescription>
</CardHeader>
<CardContent>
<form onSubmit={handleSubmit} className="grid grid-cols-1 gap-4 md:grid-cols-2">
<div className="grid gap-2">
<Label>Nome</Label>
<Input value={form.name ?? ""} onChange={(e) => setForm((p) => ({ ...p, name: e.target.value }))} />
</div>
<div className="grid gap-2">
<Label>Slug</Label>
<Input value={form.slug ?? ""} onChange={(e) => setForm((p) => ({ ...p, slug: e.target.value }))} />
</div>
<div className="grid gap-2">
<Label>CNPJ</Label>
<Input value={form.cnpj ?? ""} onChange={(e) => setForm((p) => ({ ...p, cnpj: e.target.value }))} />
</div>
<div className="grid gap-2">
<Label>Domínio</Label>
<Input value={form.domain ?? ""} onChange={(e) => setForm((p) => ({ ...p, domain: e.target.value }))} />
</div>
<div className="grid gap-2">
<Label>Telefone</Label>
<Input value={form.phone ?? ""} onChange={(e) => setForm((p) => ({ ...p, phone: e.target.value }))} />
</div>
<div className="grid gap-2 md:col-span-2">
<Label>Endereço</Label>
<Input value={form.address ?? ""} onChange={(e) => setForm((p) => ({ ...p, address: e.target.value }))} />
</div>
<div className="flex items-center gap-2 md:col-span-2">
<Checkbox
checked={Boolean(form.isAvulso ?? false)}
onCheckedChange={(v) => setForm((p) => ({ ...p, isAvulso: Boolean(v) }))}
id="is-avulso"
/>
<Label htmlFor="is-avulso">Cliente avulso?</Label>
</div>
<div className="md:col-span-2">
<Button type="submit" disabled={isPending}>{editingId ? "Salvar alterações" : "Cadastrar empresa"}</Button>
{editingId ? (
<Button type="button" variant="ghost" className="ml-2" onClick={() => { resetForm(); setEditingId(null) }}>
Cancelar
</Button>
) : null}
</div>
</form>
</CardContent>
</Card>
<Card className="border-slate-200">
<CardHeader>
<CardTitle>Empresas cadastradas</CardTitle>
<CardDescription>Gerencie empresas e o status de cliente avulso.</CardDescription>
</CardHeader>
<CardContent>
<Table>
<TableHeader>
<TableRow>
<TableHead>Nome</TableHead>
<TableHead>Slug</TableHead>
<TableHead>Avulso</TableHead>
<TableHead>Domínio</TableHead>
<TableHead>Telefone</TableHead>
<TableHead>CNPJ</TableHead>
<TableHead>Ações</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{companies.map((c) => (
<TableRow key={c.id}>
<TableCell className="font-medium">{c.name}</TableCell>
<TableCell>{c.slug}</TableCell>
<TableCell>
<Button size="sm" variant="outline" onClick={() => void toggleAvulso(c)}>
{c.isAvulso ? "Sim" : "Não"}
</Button>
</TableCell>
<TableCell>{c.domain ?? "—"}</TableCell>
<TableCell>{c.phone ?? "—"}</TableCell>
<TableCell>{c.cnpj ?? "—"}</TableCell>
<TableCell>
<Button size="sm" variant="outline" onClick={() => handleEdit(c)}>
Editar
</Button>
</TableCell>
</TableRow>
))}
</TableBody>
</Table>
</CardContent>
</Card>
</div>
)
}