"use client"
import { useCallback, useEffect, useMemo, useRef, useState, useTransition } from "react"
import dynamic from "next/dynamic"
import { Controller, FormProvider, useFieldArray, useForm, type UseFormReturn } from "react-hook-form"
import { zodResolver } from "@/lib/zod-resolver"
import {
IconAlertTriangle,
IconBuildingSkyscraper,
IconCheck,
IconClipboard,
IconClock,
IconCopy,
IconFilter,
IconList,
IconMapPin,
IconDeviceDesktop,
IconPencil,
IconPlus,
IconRefresh,
IconSearch,
IconTrash,
IconUsers,
} from "@tabler/icons-react"
import { toast } from "sonner"
import {
COMPANY_CONTACT_PREFERENCES,
COMPANY_CONTACT_ROLES,
COMPANY_CONTRACT_SCOPES,
COMPANY_CONTRACT_TYPES,
COMPANY_LOCATION_TYPES,
COMPANY_STATE_REGISTRATION_TYPES,
companyFormSchema,
type CompanyBusinessHours,
type CompanyContract,
type CompanyFormValues,
} from "@/lib/schemas/company"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import type { NormalizedCompany } from "@/server/company-service"
import { cn } from "@/lib/utils"
import { Badge } from "@/components/ui/badge"
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 { ScrollArea } from "@/components/ui/scroll-area"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import {
Table,
TableBody,
TableCell,
TableHead,
TableHeader,
TableRow,
} from "@/components/ui/table"
import {
Pagination,
PaginationContent,
PaginationItem,
PaginationLink,
PaginationNext,
PaginationPrevious,
} from "@/components/ui/pagination"
import { Textarea } from "@/components/ui/textarea"
import { TimePicker } from "@/components/ui/time-picker"
import { ToggleGroup, ToggleGroupItem } from "@/components/ui/toggle-group"
import { Accordion, AccordionContent, AccordionItem, AccordionTrigger } from "@/components/ui/accordion"
import { Skeleton } from "@/components/ui/skeleton"
import { useQuery, useMutation } from "convex/react"
import { useAuth } from "@/lib/auth-client"
import type { Id } from "@/convex/_generated/dataModel"
import { api } from "@/convex/_generated/api"
import { MultiValueInput } from "@/components/ui/multi-value-input"
import { Spinner } from "@/components/ui/spinner"
import { DatePicker } from "@/components/ui/date-picker"
type LastAlertInfo = { createdAt: number; usagePct: number; threshold: number } | null
type Props = {
initialCompanies: NormalizedCompany[]
tenantId?: string | null
autoOpenCreate?: boolean
}
type ViewMode = "table" | "board"
export type CompanyEditorState =
| { mode: "create" }
| { mode: "edit"; company: NormalizedCompany }
const AdminDevicesOverview = dynamic(
() =>
import("@/components/admin/devices/admin-devices-overview").then(
(mod) => mod.AdminDevicesOverview
),
{
ssr: false,
loading: () => (
Carregando dispositivos...
),
}
)
const BOARD_COLUMNS = [
{ id: "monthly", title: "Mensalistas", description: "Contratos recorrentes ou planos mensais." },
{ id: "time_bank", title: "Banco de horas", description: "Clientes com consumo controlado por horas." },
{ id: "project", title: "Projetos", description: "Clientes com projetos fechados." },
{ id: "per_ticket", title: "Por chamado", description: "Pagamento por ticket/chamado." },
{ id: "avulso", title: "Avulsos", description: "Sem contrato ou marcação como avulso." },
{ id: "other", title: "Outros", description: "Contratos customizados ou sem categoria definida." },
] as const
const DAY_OPTIONS = [
{ value: "mon", label: "Seg" },
{ value: "tue", label: "Ter" },
{ value: "wed", label: "Qua" },
{ value: "thu", label: "Qui" },
{ value: "fri", label: "Sex" },
{ value: "sat", label: "Sáb" },
{ value: "sun", label: "Dom" },
] as const
const EMPTY_SELECT_VALUE = "__empty__"
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 toFormValues(company: NormalizedCompany): CompanyFormValues {
const {
id: _id,
provisioningCode: _code,
createdAt: _createdAt,
updatedAt: _updatedAt,
...rest
} = company
void _id
void _code
void _createdAt
void _updatedAt
return rest
}
function emptyCompany(tenantId: string): CompanyFormValues {
return {
tenantId,
name: "",
slug: "",
legalName: null,
tradeName: null,
cnpj: null,
stateRegistration: null,
stateRegistrationType: undefined,
primaryCnae: null,
description: null,
domain: null,
phone: null,
address: null,
contractedHoursPerMonth: null,
businessHours: {
mode: "business",
timezone: "America/Sao_Paulo",
periods: [
{
days: ["mon", "tue", "wed", "thu", "fri"],
start: "09:00",
end: "18:00",
},
],
},
communicationChannels: {
supportEmails: [],
billingEmails: [],
whatsappNumbers: [],
phones: [],
portals: [],
},
supportEmail: null,
billingEmail: null,
contactPreferences: {
defaultChannel: null,
escalationNotes: null,
},
clientDomains: [],
fiscalAddress: null,
hasBranches: false,
regulatedEnvironments: [],
privacyPolicy: {
accepted: false,
reference: null,
},
contacts: [],
locations: [],
contracts: [],
sla: {
calendar: "business",
validChannels: [],
holidays: [],
severities: [
{ level: "P1", responseMinutes: 30, resolutionMinutes: 240 },
{ level: "P2", responseMinutes: 60, resolutionMinutes: 480 },
{ level: "P3", responseMinutes: 120, resolutionMinutes: 1440 },
{ level: "P4", responseMinutes: 240, resolutionMinutes: 2880 },
],
serviceWindow: {
timezone: "America/Sao_Paulo",
periods: [
{
days: ["mon", "tue", "wed", "thu", "fri"],
start: "09:00",
end: "18:00",
},
],
},
},
tags: [],
customFields: [],
notes: null,
isAvulso: false,
}
}
function sanitisePayload(values: CompanyFormValues) {
// Remove helper-only fields that should not be persisted when blank
const normalizedPrivacy = values.privacyPolicy
? {
...values.privacyPolicy,
reference:
typeof values.privacyPolicy.reference === "string" && values.privacyPolicy.reference.trim().length === 0
? null
: values.privacyPolicy.reference ?? null,
}
: undefined
return {
...values,
privacyPolicy: normalizedPrivacy,
contactPreferences:
values.contactPreferences && values.contactPreferences.defaultChannel
? values.contactPreferences
: values.contactPreferences?.escalationNotes
? values.contactPreferences
: undefined,
}
}
function inferBoardBucket(company: NormalizedCompany) {
if (company.isAvulso) return "avulso"
const contractTypes = new Set(company.contracts.map((contract) => contract.contractType))
for (const column of BOARD_COLUMNS) {
if (column.id === "avulso" || column.id === "other") continue
if (contractTypes.has(column.id as CompanyContract["contractType"])) return column.id
}
if (contractTypes.size === 0) return "other"
return contractTypes.values().next().value ?? "other"
}
function formatCurrency(value: number | null | undefined) {
if (value === null || value === undefined) return "—"
return new Intl.NumberFormat("pt-BR", { style: "currency", currency: "BRL" }).format(value)
}
function formatDate(value: string | null | undefined) {
if (!value) return "—"
const date = new Date(value)
if (Number.isNaN(date.getTime())) return value
return date.toLocaleDateString("pt-BR")
}
function FieldError({ error }: { error?: string }) {
if (!error) return null
return {error}
}
export function AdminCompaniesManager({ initialCompanies, tenantId, autoOpenCreate = false }: Props) {
const [companies, setCompanies] = useState(() => initialCompanies)
const [view, setView] = useState("table")
const [search, setSearch] = useState("")
const [contractFilter, setContractFilter] = useState("all")
const [regulatedFilter, setRegulatedFilter] = useState("all")
const [isRefreshing, startRefresh] = useTransition()
const [editor, setEditor] = useState(null)
const [isDeleting, setIsDeleting] = useState(null)
const [alertsBySlug, setAlertsBySlug] = useState>({})
const effectiveTenantId = tenantId ?? companies[0]?.tenantId ?? DEFAULT_TENANT_ID
// Dispositivos por empresa para contagem rápida
const machines = useQuery(api.devices.listByTenant, {
tenantId: effectiveTenantId,
includeMetadata: false,
}) as unknown[] | undefined
function extractCompanySlug(entry: unknown): string | undefined {
if (!entry || typeof entry !== "object") {
return undefined
}
if ("companySlug" in entry) {
const value = (entry as { companySlug?: unknown }).companySlug
return typeof value === "string" && value.length > 0 ? value : undefined
}
return undefined
}
const machineCountsBySlug = useMemo(() => {
const map: Record = {}
;(machines ?? []).forEach((entry) => {
const slug = extractCompanySlug(entry)
if (!slug) return
map[slug] = (map[slug] ?? 0) + 1
})
return map
}, [machines])
const filtered = useMemo(() => {
const term = search.trim().toLowerCase()
return companies.filter((company) => {
if (term) {
const matchesTerm =
company.name.toLowerCase().includes(term) ||
company.slug.toLowerCase().includes(term) ||
(company.domain?.toLowerCase().includes(term) ?? false) ||
company.contacts.some((contact) => contact.fullName.toLowerCase().includes(term))
if (!matchesTerm) return false
}
if (contractFilter !== "all") {
const types = new Set(company.contracts.map((contract) => contract.contractType))
if (!types.has(contractFilter as CompanyContract["contractType"])) return false
}
if (regulatedFilter !== "all") {
if (!company.regulatedEnvironments.includes(regulatedFilter)) return false
}
return true
})
}, [companies, contractFilter, regulatedFilter, search])
const contractOptions = useMemo(() => {
const entries = new Set()
companies.forEach((company) => {
company.contracts.forEach((contract) => entries.add(contract.contractType))
})
return Array.from(entries.values()).sort()
}, [companies])
const regulatedOptions = useMemo(() => {
const entries = new Set()
companies.forEach((company) =>
company.regulatedEnvironments.forEach((item) => entries.add(item)),
)
return Array.from(entries.values()).sort()
}, [companies])
useEffect(() => {
const slugs = companies.map((company) => company.slug).filter(Boolean)
if (slugs.length === 0) {
setAlertsBySlug({})
return
}
let active = true
void fetch(`/api/admin/companies/last-alerts?${new URLSearchParams({ slugs: slugs.join(",") })}`, {
credentials: "include",
})
.then(async (response) => {
if (!response.ok) return
const json = (await response.json()) as { items?: Record }
if (active && json?.items) {
setAlertsBySlug(json.items)
}
})
.catch((error) => {
console.warn("Failed to load last alerts", error)
})
return () => {
active = false
}
}, [companies])
const refresh = useCallback(() => {
startRefresh(async () => {
try {
const response = await fetch("/api/admin/companies", { credentials: "include" })
if (!response.ok) throw new Error("Falha ao atualizar a listagem de empresas.")
const json = (await response.json()) as { companies?: NormalizedCompany[] }
const nextCompanies = json.companies ?? []
setCompanies(nextCompanies)
toast.success("Empresas atualizadas.")
} catch (error) {
const message =
error instanceof Error ? error.message : "Não foi possível atualizar as empresas."
toast.error(message)
}
})
}, [])
const openCreate = useCallback(() => {
setEditor({ mode: "create" })
}, [])
const openEdit = useCallback((company: NormalizedCompany) => {
setEditor({ mode: "edit", company })
}, [])
const closeEditor = useCallback(() => {
setEditor(null)
}, [])
const confirmDelete = useCallback((company: NormalizedCompany) => {
setIsDeleting(company)
}, [])
const cancelDelete = useCallback(() => setIsDeleting(null), [])
useEffect(() => {
if (autoOpenCreate) {
openCreate()
}
}, [autoOpenCreate, openCreate])
useEffect(() => {
if (typeof window === "undefined") return
const handler = () => {
openCreate()
}
window.addEventListener("quick-open-company", handler)
return () => window.removeEventListener("quick-open-company", handler)
}, [openCreate])
const handleDelete = useCallback(async () => {
if (!isDeleting) return
try {
const response = await fetch(`/api/admin/companies/${isDeleting.id}`, {
method: "DELETE",
credentials: "include",
})
if (!response.ok) {
const payload = await response.json().catch(() => null)
throw new Error(payload?.error ?? "Não foi possível remover a empresa.")
}
setCompanies((prev) => prev.filter((company) => company.id !== isDeleting.id))
toast.success(`Empresa “${isDeleting.name}” removida.`)
setIsDeleting(null)
} catch (error) {
const message =
error instanceof Error ? error.message : "Não foi possível remover a empresa."
toast.error(message)
}
}, [isDeleting])
const boardGroups = useMemo(() => {
const map = new Map()
filtered.forEach((company) => {
const bucket = inferBoardBucket(company)
const bucketId = BOARD_COLUMNS.some((column) => column.id === bucket) ? bucket : "other"
const list = map.get(bucketId) ?? []
list.push(company)
map.set(bucketId, list)
})
return map
}, [filtered])
const renderBoardCard = (company: NormalizedCompany) => {
const alert = alertsBySlug[company.slug]
const firstContract = company.contracts[0]
return (
{company.name}
{company.slug}
{company.isAvulso ? Avulso : null}
{company.regulatedEnvironments.map((env) => (
{env.toUpperCase()}
))}
Contratos
{firstContract ? (
{firstContract.contractType}
{firstContract.scope.length
? firstContract.scope.join(", ")
: "Escopo indefinido"}
) : (
Nenhum contrato cadastrado.
)}
Último alerta
{alert ? (
{alert.usagePct}% usado · limiar {alert.threshold}% ·{" "}
{new Date(alert.createdAt).toLocaleDateString("pt-BR")}
) : (
Nenhum alerta registrado
)}
Canais principais
{company.supportEmail ?
Suporte: {company.supportEmail}
: null}
{company.billingEmail ?
Financeiro: {company.billingEmail}
: null}
{company.phone ?
Telefone: {company.phone}
: null}
)
}
return (
<>
Empresas atendidas
Cadastre, edite e visualize contratos, contatos e SLAs das empresas.
setSearch(event.target.value)}
className="pl-9"
/>
{
if (!next) return
setView(next as ViewMode)
}}
variant="outline"
className="rounded-md border border-border/60 bg-muted/30"
>
Lista
Quadro
{view === "table" ? (
) : (
{BOARD_COLUMNS.map((column) => {
const list = boardGroups.get(column.id) ?? []
return (
{column.title}
{column.description}
{list.length}
{list.length === 0 ? (
) : (
list.map((company) => renderBoardCard(company))
)}
)
})}
)}
{
setCompanies((prev) => [...prev, company].sort((a, b) => a.name.localeCompare(b.name)))
}}
onUpdated={(company) => {
setCompanies((prev) =>
prev.map((item) => (item.id === company.id ? company : item)).sort((a, b) => a.name.localeCompare(b.name)),
)
}}
/>
>
)
}
function EmptyColumn() {
return (
Sem empresas nesta categoria.
)
}
type TableViewProps = {
companies: NormalizedCompany[]
machineCountsBySlug: Record
onEdit(company: NormalizedCompany): void
onDelete(company: NormalizedCompany): void
}
function TableView({ companies, machineCountsBySlug, onEdit, onDelete }: TableViewProps) {
const [pageSize, setPageSize] = useState(10)
const [pageIndex, setPageIndex] = useState(0)
const total = companies.length
const pageCount = Math.max(1, Math.ceil(total / pageSize))
const start = total === 0 ? 0 : pageIndex * pageSize + 1
const end = total === 0 ? 0 : Math.min(total, pageIndex * pageSize + pageSize)
const current = useMemo(
() => companies.slice(pageIndex * pageSize, pageIndex * pageSize + pageSize),
[companies, pageIndex, pageSize]
)
useEffect(() => {
if (pageIndex > pageCount - 1) setPageIndex(Math.max(0, pageCount - 1))
}, [pageCount, pageIndex])
return (
Empresa
Contratos ativos
Contatos
Dispositivos
Ações
{companies.length === 0 ? (
Nenhuma empresa encontrada com os filtros atuais.
) : (
current.map((company) => {
const contracts = company.contracts
const contacts = company.contacts.slice(0, 3)
const machineCount = machineCountsBySlug[company.slug] ?? 0
return (
{company.name}
{company.isAvulso ?
Avulso : null}
{company.domain ? {company.domain} : null}
{company.phone ? (
{company.phone}
) : null}
{company.tags.map((tag) => (
{tag}
))}
{contracts.length === 0 ? (
Nenhum contrato registrado.
) : (
{contracts.map((contract) => (
-
{contract.contractType}
{contract.scope.length ? contract.scope.join(", ") : "Escopo base"}
Vigência: {formatDate(contract.startDate)} – {formatDate(contract.endDate)}
Valor: {formatCurrency(contract.price ?? null)}
))}
)}
{contacts.length === 0 ? (
Nenhum contato cadastrado.
) : (
)}
{machineCount}
)
})
)}
{total === 0 ? "Nenhum registro" : `Mostrando ${start}-${end} de ${total}`}
Itens por página
setPageIndex((p) => Math.max(0, p - 1))} />
{
event.preventDefault()
}}
>
{pageIndex + 1}
= pageCount - 1} onClick={() => setPageIndex((p) => Math.min(pageCount - 1, p + 1))} />
)
}
type CompanySheetProps = {
tenantId: string
editor: CompanyEditorState | null
onClose(): void
onCreated(company: NormalizedCompany): void
onUpdated(company: NormalizedCompany): void
}
export function CompanySheet({ tenantId, editor, onClose, onCreated, onUpdated }: CompanySheetProps) {
const [isSubmitting, startSubmit] = useTransition()
const open = Boolean(editor)
const form = useForm({
resolver: zodResolver(companyFormSchema),
defaultValues: emptyCompany(tenantId),
mode: "onBlur",
})
const contactsArray = useFieldArray({
control: form.control,
name: "contacts",
})
const locationsArray = useFieldArray({
control: form.control,
name: "locations",
})
const contractsArray = useFieldArray({
control: form.control,
name: "contracts",
})
const customFieldsArray = useFieldArray({
control: form.control,
name: "customFields",
})
useEffect(() => {
if (!editor) return
if (editor.mode === "create") {
form.reset(emptyCompany(tenantId))
return
}
const values = toFormValues(editor.company)
if (!values.businessHours) {
values.businessHours = emptyCompany(tenantId).businessHours
}
if (!values.sla) {
values.sla = emptyCompany(tenantId).sla
}
form.reset(values)
}, [editor, form, tenantId])
const close = () => {
form.reset(emptyCompany(tenantId))
onClose()
}
const handleSubmit = (values: CompanyFormValues) => {
startSubmit(async () => {
const payload = sanitisePayload(values)
try {
if (editor?.mode === "edit") {
const response = await fetch(`/api/admin/companies/${editor.company.id}`, {
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(`Empresa “${json.company.name}” atualizada com sucesso.`)
} else {
const response = await fetch("/api/admin/companies", {
method: "POST",
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 criar a empresa.")
}
const json = (await response.json()) as { company: NormalizedCompany }
onCreated(json.company)
toast.success(`Empresa “${json.company.name}” criada com sucesso.`)
}
close()
} catch (error) {
const message =
error instanceof Error ? error.message : "Não foi possível salvar a empresa."
toast.error(message)
}
})
}
return (
)
}
type BusinessHoursEditorProps = {
form: UseFormReturn
}
function BusinessHoursEditor({ form }: BusinessHoursEditorProps) {
const mode = form.watch("businessHours.mode")
const periods = form.watch("businessHours.periods") ?? []
if (mode === "twentyfour") {
return (
Atendimento contínuo 24x7. Nenhum período customizado é necessário.
)
}
const toggleDay = (index: number, day: (typeof DAY_OPTIONS)[number]["value"]) => {
const current = periods ?? []
const target = current[index]
if (!target) return
const nextDays = new Set(target.days)
if (nextDays.has(day)) {
nextDays.delete(day)
} else {
nextDays.add(day)
}
const next = [...current]
next[index] = { ...target, days: Array.from(nextDays.values()) as typeof target.days }
form.setValue("businessHours.periods", next)
}
const updateTime = (index: number, field: "start" | "end", value: string) => {
const next = [...periods]
if (!next[index]) return
next[index] = { ...next[index], [field]: value }
form.setValue("businessHours.periods", next)
}
const removePeriod = (index: number) => {
const next = periods.filter((_, idx) => idx !== index)
form.setValue("businessHours.periods", next)
}
const addPeriod = () => {
const next = [
...periods,
{ days: ["mon", "tue", "wed", "thu", "fri"], start: "09:00", end: "18:00" } as CompanyBusinessHours["periods"][number],
]
form.setValue("businessHours.periods", next)
}
return (
{periods.length === 0 ? (
Nenhum período personalizado. Defina ao menos um intervalo quando o modo for “personalizado”.
) : (
{periods.map((period, index) => (
Período #{index + 1}
{DAY_OPTIONS.map((day) => {
const active = period.days.includes(day.value)
return (
)
})}
updateTime(index, "start", v)} />
updateTime(index, "end", v)} />
))}
)}
)
}
type CompanyRequestTypesControlsProps = { tenantId?: string | null; companyId: string | null }
function CompanyRequestTypesControls({ tenantId, companyId }: CompanyRequestTypesControlsProps) {
const { convexUserId } = useAuth()
const canLoad = Boolean(tenantId && convexUserId)
const ensureDefaults = useMutation(api.tickets.ensureTicketFormDefaults)
const hasEnsuredRef = useRef(false)
const settings = useQuery(
api.ticketFormSettings.list,
canLoad ? { tenantId: tenantId as string, viewerId: convexUserId as Id<"users"> } : "skip"
) as Array<{ template: string; scope: string; companyId?: string | null; enabled: boolean; updatedAt: number }> | undefined
const templates = useQuery(
api.ticketFormTemplates.listActive,
canLoad ? { tenantId: tenantId as string, viewerId: convexUserId as Id<"users"> } : "skip"
) as Array<{ key: string; label: string }> | undefined
const upsert = useMutation(api.ticketFormSettings.upsert)
useEffect(() => {
if (!tenantId || !convexUserId || hasEnsuredRef.current) return
hasEnsuredRef.current = true
ensureDefaults({ tenantId, actorId: convexUserId as Id<"users"> }).catch((error) => {
console.error("Falha ao garantir formulários padrão", error)
hasEnsuredRef.current = false
})
}, [ensureDefaults, tenantId, convexUserId])
const resolveEnabled = (template: string) => {
const scoped = (settings ?? []).filter((s) => s.template === template)
const base = true
if (!companyId) return base
const latest = scoped
.filter((s) => s.scope === "company" && String(s.companyId ?? "") === String(companyId))
.sort((a, b) => b.updatedAt - a.updatedAt)[0]
return typeof latest?.enabled === "boolean" ? latest.enabled : base
}
const handleToggle = async (template: string, enabled: boolean) => {
if (!tenantId || !convexUserId || !companyId) return
try {
await upsert({
tenantId,
actorId: convexUserId as Id<"users">,
template,
scope: "company",
companyId: companyId as unknown as Id<"companies">,
enabled,
})
toast.success("Configuração salva.")
} catch (error) {
console.error("Falha ao salvar configuração de formulário", error)
toast.error("Não foi possível salvar a configuração.")
}
}
return (
Defina quais tipos de solicitação estão disponíveis para colaboradores/gestores desta empresa. Administradores e agentes sempre veem todas as opções.
{!templates ? (
) : templates.length === 0 ? (
Nenhum formulário disponível.
) : (
{templates.map((template) => {
const enabled = resolveEnabled(template.key)
return (
)
})}
)}
)
}
type CompanyExportTemplateSelectorProps = { tenantId?: string | null; companyId: string | null }
function CompanyExportTemplateSelector({ tenantId, companyId }: CompanyExportTemplateSelectorProps) {
const { convexUserId } = useAuth()
const canLoad = Boolean(tenantId && convexUserId)
const templates = useQuery(
api.deviceExportTemplates.list,
canLoad
? {
tenantId: tenantId as string,
viewerId: convexUserId as Id<"users">,
companyId: companyId ? (companyId as unknown as Id<"companies">) : undefined,
includeInactive: true,
}
: "skip"
) as Array<{ id: string; name: string; companyId: string | null; isDefault: boolean; description?: string }> | undefined
const setDefaultTemplate = useMutation(api.deviceExportTemplates.setDefault)
const clearDefaultTemplate = useMutation(api.deviceExportTemplates.clearCompanyDefault)
const companyTemplates = useMemo(() => {
if (!templates || !companyId) return []
return templates.filter((tpl) => String(tpl.companyId ?? "") === String(companyId))
}, [templates, companyId])
const companyDefault = useMemo(() => companyTemplates.find((tpl) => tpl.isDefault) ?? null, [companyTemplates])
const handleChange = async (value: string) => {
if (!tenantId || !convexUserId || !companyId) return
try {
if (value === "inherit") {
await clearDefaultTemplate({
tenantId,
actorId: convexUserId as Id<"users">,
companyId: companyId as unknown as Id<"companies">,
})
toast.success("Template desta empresa voltou a herdar o padrão global.")
} else {
await setDefaultTemplate({
tenantId,
actorId: convexUserId as Id<"users">,
templateId: value as Id<"deviceExportTemplates">,
})
toast.success("Template aplicado para esta empresa.")
}
} catch (error) {
console.error("Falha ao definir template de exportação", error)
toast.error("Não foi possível atualizar o template.")
}
}
const selectValue = companyDefault ? companyDefault.id : "inherit"
return (
Defina o template padrão das exportações de inventário para esta empresa. Ao herdar, o template global será utilizado.
{!companyId ? (
Salve a empresa antes de configurar o template.
) : !templates ? (
) : companyTemplates.length === 0 ? (
Nenhum template específico para esta empresa. Crie um template em Dispositivos > Exportações e associe a esta empresa para habilitar aqui.
) : (
)}
)
}