feat: export reports as xlsx and add machine inventory
This commit is contained in:
parent
29b865885c
commit
714b199879
34 changed files with 2304 additions and 245 deletions
|
|
@ -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">
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue