feat: export reports as xlsx and add machine inventory

This commit is contained in:
Esdras Renan 2025-10-27 18:00:28 -03:00
parent 29b865885c
commit 714b199879
34 changed files with 2304 additions and 245 deletions

View file

@ -88,6 +88,18 @@ type MachineAlertEntry = {
severity: string
createdAt: number
}
type MachineTicketSummary = {
id: string
reference: number
subject: string
status: string
priority: string
updatedAt: number
createdAt: number
machine: { id: string | null; hostname: string | null } | null
assignee: { name: string | null; email: string | null } | null
}
type DetailLineProps = {
label: string
@ -774,6 +786,13 @@ const statusLabels: Record<string, string> = {
unknown: "Desconhecida",
}
const TICKET_STATUS_LABELS: Record<string, string> = {
PENDING: "Pendente",
AWAITING_ATTENDANCE: "Em andamento",
PAUSED: "Pausado",
RESOLVED: "Resolvido",
}
const statusClasses: Record<string, string> = {
online: "border-emerald-500/20 bg-emerald-500/15 text-emerald-600",
offline: "border-rose-500/20 bg-rose-500/15 text-rose-600",
@ -1109,7 +1128,16 @@ export function AdminMachinesOverview({ tenantId, initialCompanyFilterSlug = "al
.sort((a, b) => a.name.localeCompare(b.name, "pt-BR"))
}, [companies, machines])
const filteredMachines = useMemo(() => {
const exportHref = useMemo(() => {
const params = new URLSearchParams()
if (companyFilterSlug !== "all") {
params.set("companyId", companyFilterSlug)
}
const qs = params.toString()
return `/api/reports/machines-inventory.xlsx${qs ? `?${qs}` : ""}`
}, [companyFilterSlug])
const filteredMachines = useMemo(() => {
const text = q.trim().toLowerCase()
return machines.filter((m) => {
if (onlyAlerts && !(Array.isArray(m.postureAlerts) && m.postureAlerts.length > 0)) return false
@ -1202,6 +1230,11 @@ const filteredMachines = useMemo(() => {
<span>Somente com alertas</span>
</label>
<Button variant="outline" onClick={() => { setQ(""); setStatusFilter("all"); setCompanyFilterSlug("all"); setCompanySearch(""); setOnlyAlerts(false) }}>Limpar</Button>
<Button asChild size="sm" variant="outline">
<a href={exportHref} download>
Exportar XLSX
</a>
</Button>
</div>
{isLoading ? (
<LoadingState />
@ -1305,6 +1338,11 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
machine ? { machineId: machine.id as Id<"machines">, limit: 50 } : ("skip" as const)
) as MachineAlertEntry[] | undefined
const machineAlertsHistory = alertsHistory ?? []
const openTickets = useQuery(
machine ? api.machines.listOpenTickets : "skip",
machine ? { machineId: machine.id as Id<"machines">, limit: 8 } : ("skip" as const)
) as MachineTicketSummary[] | undefined
const machineTickets = openTickets ?? []
const metadata = machine?.inventory ?? null
const metrics = machine?.metrics ?? null
const metricsCapturedAt = useMemo(() => getMetricsTimestamp(metrics), [metrics])
@ -2175,6 +2213,43 @@ export function MachineDetails({ machine }: MachineDetailsProps) {
<InfoChip key={chip.key} label={chip.label} value={chip.value} icon={chip.icon} tone={chip.tone} />
))}
</div>
<div className="rounded-2xl border border-indigo-100 bg-indigo-50/40 px-4 py-4">
<div className="flex flex-wrap items-center justify-between gap-2">
<h4 className="text-sm font-semibold text-indigo-900">Tickets abertos por esta máquina</h4>
<Badge variant="outline" className="border-indigo-200 bg-white text-[11px] font-semibold uppercase tracking-wide text-indigo-700">
{machineTickets.length}
</Badge>
</div>
{machineTickets.length === 0 ? (
<p className="mt-3 text-xs text-indigo-700">Nenhum chamado em aberto registrado diretamente por esta máquina.</p>
) : (
<ul className="mt-3 space-y-2">
{machineTickets.map((ticket) => (
<li
key={ticket.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-indigo-200 bg-white px-3 py-2 text-sm shadow-sm"
>
<div className="min-w-0 flex-1">
<p className="truncate font-medium text-neutral-900">
#{ticket.reference} · {ticket.subject}
</p>
<p className="text-xs text-neutral-500">
Atualizado {formatRelativeTime(new Date(ticket.updatedAt))}
</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="border-slate-200 text-[11px] uppercase text-neutral-600">
{ticket.priority}
</Badge>
<Badge className="bg-indigo-600 text-[11px] uppercase tracking-wide text-white">
{TICKET_STATUS_LABELS[ticket.status] ?? ticket.status}
</Badge>
</div>
</li>
))}
</ul>
)}
</div>
<div className="flex flex-wrap gap-2">
{machine.authEmail ? (
<Button size="sm" variant="outline" onClick={copyEmail} className="gap-2 border-dashed">