"use client" import { useCallback, useMemo, useState, useTransition } from "react" import { format } from "date-fns" import { ptBR } from "date-fns/locale" import { Controller, FormProvider, useFieldArray, useForm } from "react-hook-form" import { zodResolver } from "@hookform/resolvers/zod" import { IconBuildingSkyscraper, IconChevronRight, IconFilter, IconMapPin, IconNotebook, IconPencil, IconPlus, IconSearch, IconTrash, IconUsers, } from "@tabler/icons-react" import { toast } from "sonner" import { COMPANY_CONTACT_ROLES, COMPANY_CONTRACT_SCOPES, COMPANY_CONTRACT_TYPES, COMPANY_LOCATION_TYPES, companyFormSchema, type CompanyContact, type CompanyContract, type CompanyFormValues, type CompanyLocation, } from "@/lib/schemas/company" import type { NormalizedCompany } from "@/server/company-service" import { cn } from "@/lib/utils" import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs" import { Button } from "@/components/ui/button" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Checkbox } from "@/components/ui/checkbox" import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle } from "@/components/ui/dialog" import { Input } from "@/components/ui/input" import { Label } from "@/components/ui/label" import { MultiValueInput } from "@/components/ui/multi-value-input" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select" import { Sheet, SheetContent, SheetFooter, SheetHeader, SheetTitle } from "@/components/ui/sheet" import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow, } from "@/components/ui/table" import { Textarea } from "@/components/ui/textarea" import { Badge } from "@/components/ui/badge" import { Avatar, AvatarFallback } from "@/components/ui/avatar" export type AdminAccount = { id: string email: string name: string role: "MANAGER" | "COLLABORATOR" companyId: string | null companyName: string | null tenantId: string createdAt: string updatedAt: string authUserId: string | null lastSeenAt: string | null } type Props = { initialAccounts: AdminAccount[] companies: NormalizedCompany[] } type SectionEditorState = | null | { section: "contacts" | "locations" | "contracts" company: NormalizedCompany } const ROLE_LABEL: Record = { MANAGER: "Gestor", COLLABORATOR: "Colaborador", } const NO_CONTACT_VALUE = "__none__" function createId(prefix: string) { if (typeof crypto !== "undefined" && typeof crypto.randomUUID === "function") { return `${prefix}-${crypto.randomUUID()}` } return `${prefix}-${Math.random().toString(36).slice(2, 10)}` } function formatDate(value: string | null) { if (!value) return "Nunca" const parsed = new Date(value) return format(parsed, "dd/MM/yy HH:mm", { locale: ptBR }) } function FieldError({ message }: { message?: string }) { if (!message) return null return

{message}

} export function AdminUsersWorkspace({ initialAccounts, companies }: Props) { const [tab, setTab] = useState<"accounts" | "structure">("accounts") return ( setTab(value as typeof tab)}> Usuários com acesso Estrutura das empresas ) } function AccountsTable({ initialAccounts }: { initialAccounts: AdminAccount[] }) { const [accounts, setAccounts] = useState(initialAccounts) const [search, setSearch] = useState("") const [roleFilter, setRoleFilter] = useState<"all" | AdminAccount["role"]>("all") const [companyFilter, setCompanyFilter] = useState("all") const [rowSelection, setRowSelection] = useState>({}) const [isPending, startTransition] = useTransition() const [deleteDialogIds, setDeleteDialogIds] = useState([]) const filteredAccounts = useMemo(() => { const term = search.trim().toLowerCase() return accounts.filter((account) => { if (roleFilter !== "all" && account.role !== roleFilter) return false if (companyFilter !== "all" && account.companyId !== companyFilter) return false if (!term) return true return ( account.name.toLowerCase().includes(term) || account.email.toLowerCase().includes(term) || (account.companyName ?? "").toLowerCase().includes(term) ) }) }, [accounts, roleFilter, companyFilter, search]) const selectedIds = useMemo( () => Object.entries(rowSelection).filter(([, selected]) => selected).map(([id]) => id), [rowSelection] ) const companies = useMemo(() => { const map = new Map() accounts.forEach((account) => { if (account.companyId && account.companyName) { map.set(account.companyId, account.companyName) } }) return Array.from(map.entries()).map(([id, name]) => ({ id, name })) }, [accounts]) const openDeleteDialog = useCallback((ids: string[]) => { setDeleteDialogIds(ids) }, []) const closeDeleteDialog = useCallback(() => setDeleteDialogIds([]), []) const handleDelete = useCallback( (ids: string[]) => { if (ids.length === 0) return startTransition(async () => { try { const response = await fetch("/api/admin/users", { method: "DELETE", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ ids }), }) if (!response.ok) { const payload = await response.json().catch(() => null) throw new Error(payload?.error ?? "Não foi possível excluir os usuários selecionados.") } const json = (await response.json()) as { deletedIds: string[] } setAccounts((prev) => prev.filter((account) => !json.deletedIds.includes(account.id))) setRowSelection({}) setDeleteDialogIds([]) toast.success( json.deletedIds.length === 1 ? "Usuário removido com sucesso." : `${json.deletedIds.length} usuários removidos com sucesso.`, ) } catch (error) { const message = error instanceof Error ? error.message : "Não foi possível excluir os usuários selecionados." toast.error(message) } }) }, [startTransition], ) return ( Usuários do cliente Gestores e colaboradores com acesso ao portal de chamados.
setSearch(event.target.value)} placeholder="Buscar por nome, e-mail ou empresa..." className="pl-9" />
Usuário Empresa Papel Último acesso Selecionar {filteredAccounts.length === 0 ? ( Nenhum usuário encontrado. ) : ( filteredAccounts.map((account) => { const initials = account.name .split(" ") .filter(Boolean) .slice(0, 2) .map((part) => part.charAt(0).toUpperCase()) .join("") return (
{initials || account.email.charAt(0).toUpperCase()}

{account.name}

{account.email}

{account.companyName ?? Sem empresa} {ROLE_LABEL[account.role]} {formatDate(account.lastSeenAt)} setRowSelection((prev) => ({ ...prev, [account.id]: Boolean(checked) })) } />
) }) )}
0} onOpenChange={(open) => (!open ? closeDeleteDialog() : null)}> Remover usuários Os usuários selecionados perderão o acesso ao portal e seus convites ativos serão revogados.
) } function CompanyStructurePanel({ initialCompanies }: { initialCompanies: NormalizedCompany[] }) { const [companies, setCompanies] = useState(initialCompanies) const [search, setSearch] = useState("") const [selectedId, setSelectedId] = useState(initialCompanies[0]?.id ?? null) const [editor, setEditor] = useState(null) const filtered = useMemo(() => { const term = search.trim().toLowerCase() return companies.filter((company) => { if (!term) return true return ( company.name.toLowerCase().includes(term) || company.slug.toLowerCase().includes(term) || (company.domain?.toLowerCase().includes(term) ?? false) ) }) }, [companies, search]) const selectedCompany = useMemo( () => companies.find((company) => company.id === selectedId) ?? null, [companies, selectedId] ) const openEditor = useCallback( (section: "contacts" | "locations" | "contracts") => { if (!selectedCompany) return setEditor({ section, company: selectedCompany }) }, [selectedCompany], ) const closeEditor = useCallback(() => setEditor(null), []) const applyUpdate = useCallback((updated: NormalizedCompany) => { setCompanies((prev) => prev.map((company) => (company.id === updated.id ? updated : company))) }, []) return (
Empresas Selecione uma empresa para visualizar contatos, localizações e contratos.
setSearch(event.target.value)} />
{filtered.length === 0 ? (
Nenhuma empresa encontrada.
) : ( filtered.map((company) => { const isActive = company.id === selectedId return ( ) }) )}
{selectedCompany ? ( <>
{selectedCompany.name} {selectedCompany.domain ?? "Sem domínio"} ·{" "} {selectedCompany.isAvulso ? "Cliente avulso" : "Cliente com contrato"}
({ id: contact.id, title: contact.fullName, subtitle: contact.email, description: contact.role.replace("_", " "), }))} /> ({ id: location.id, title: location.name, subtitle: location.type, description: location.address?.city ? `${location.address.city}/${location.address.state}` : location.notes ?? "", }))} /> ({ id: contract.id, title: `${contract.contractType.toUpperCase()}${contract.planSku ? ` · ${contract.planSku}` : ""}`, subtitle: contract.scope.length ? contract.scope.join(", ") : "Escopo não informado", description: contract.notes ?? "", }))} /> ) : (

Selecione uma empresa para visualizar os detalhes.

)}
) } function SectionSummary({ title, items, emptyMessage, }: { title: string items: { id: string; title: string; subtitle?: string | null; description?: string | null }[] emptyMessage: string }) { return (

{title}

{items.length === 0 ? (
{emptyMessage}
) : (
{items.map((item) => (

{item.title}

{item.subtitle ?

{item.subtitle}

: null} {item.description ? (

{item.description}

) : null}
))}
)}
) } type CompanySectionSheetProps = { editor: SectionEditorState onClose(): void onUpdated(company: NormalizedCompany): void } function CompanySectionSheet({ editor, onClose, onUpdated }: CompanySectionSheetProps) { const [isSubmitting, startSubmit] = useTransition() const open = Boolean(editor) const handleSave = (payload: Partial, companyId: string) => { startSubmit(async () => { try { const response = await fetch(`/api/admin/companies/${companyId}`, { method: "PATCH", headers: { "Content-Type": "application/json" }, body: JSON.stringify(payload), }) if (!response.ok) { const json = await response.json().catch(() => null) throw new Error(json?.error ?? "Falha ao atualizar a empresa.") } const json = (await response.json()) as { company: NormalizedCompany } onUpdated(json.company) toast.success("Dados atualizados com sucesso.") onClose() } catch (error) { const message = error instanceof Error ? error.message : "Não foi possível salvar as alterações." toast.error(message) } }) } let content: React.ReactNode = null if (editor) { if (editor.section === "contacts") { content = ( handleSave({ contacts }, editor.company.id)} /> ) } else if (editor.section === "locations") { content = ( handleSave({ locations }, editor.company.id)} /> ) } else if (editor.section === "contracts") { content = ( handleSave({ contracts }, editor.company.id)} /> ) } } return ( (!value ? onClose() : null)}> {editor?.section === "contacts" ? "Contatos da empresa" : null} {editor?.section === "locations" ? "Localizações e unidades" : null} {editor?.section === "contracts" ? "Contratos ativos" : null}
{content}
{isSubmitting ? ( Salvando... ) : null}
) } function ContactsEditor({ company, onCancel, onSubmit, }: { company: NormalizedCompany onCancel(): void onSubmit(contacts: CompanyContact[]): void }) { const form = useForm<{ contacts: CompanyContact[] }>({ resolver: zodResolver(companyFormSchema.pick({ contacts: true })), defaultValues: { contacts: company.contacts.length ? company.contacts : [ { id: createId("contact"), fullName: "", email: "", role: COMPANY_CONTACT_ROLES[0]?.value ?? "usuario_chave", phone: null, whatsapp: null, preference: [], title: null, canAuthorizeTickets: false, canApproveCosts: false, lgpdConsent: true, notes: null, }, ], }, }) const fieldArray = useFieldArray({ control: form.control, name: "contacts" }) const submit = form.handleSubmit((values) => { onSubmit(values.contacts) }) return (

Contatos estratégicos

Cadastre responsáveis por aprovação, faturamento e comunicação.

{fieldArray.fields.map((field, index) => { const errors = form.formState.errors.contacts?.[index] return ( Contato #{index + 1}
( )} />
( )} />
form.setValue(`contacts.${index}.canAuthorizeTickets` as const, Boolean(checked)) } /> Pode autorizar tickets
form.setValue(`contacts.${index}.canApproveCosts` as const, Boolean(checked)) } /> Pode aprovar custos