ui(admin/alerts): envolver página com AppShell + SiteHeader e mover lógica para AdminAlertsManager (client); docs: agents.md reforça uso do wrapper em páginas administrativas

This commit is contained in:
Esdras Renan 2025-10-07 16:25:37 -03:00
parent 48f8952079
commit 13eb53c3cf
4 changed files with 149 additions and 119 deletions

View file

@ -0,0 +1,127 @@
"use client"
import { useMemo, useState } from "react"
import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api"
import type { Id, Doc } from "@/convex/_generated/dataModel"
import { useAuth } from "@/lib/auth-client"
import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { Card, CardAction, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton"
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import { Button } from "@/components/ui/button"
export function AdminAlertsManager() {
const [companyId, setCompanyId] = useState<string>("all")
const [range, setRange] = useState<string>("30d")
const { session, convexUserId } = useAuth()
const tenantId = session?.user.tenantId ?? DEFAULT_TENANT_ID
const now = new Date()
const days = range === "7d" ? 7 : range === "30d" ? 30 : range === "90d" ? 90 : null
const end = now.getTime()
const start = days ? end - days * 24 * 60 * 60 * 1000 : undefined
const alertsRaw = useQuery(
api.alerts.list,
convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) as Doc<"alerts">[] | undefined
const alerts = useMemo(() => {
let list = alertsRaw ?? []
if (companyId !== "all") list = list.filter((a) => String(a.companyId) === companyId)
if (typeof start === "number") list = list.filter((a) => a.createdAt >= start)
if (typeof end === "number") list = list.filter((a) => a.createdAt < end)
return list.sort((a, b) => b.createdAt - a.createdAt)
}, [alertsRaw, companyId, start, end])
const companies = useQuery(
api.companies.list,
convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) as Array<{ id: Id<"companies">; name: string }> | undefined
return (
<Card className="border-slate-200">
<CardHeader>
<CardTitle className="text-lg font-semibold text-neutral-900">Alertas enviados</CardTitle>
<CardDescription className="text-neutral-600">
Histórico dos e-mails de alerta de uso de horas disparados automaticamente.
</CardDescription>
<CardAction>
<div className="flex flex-col items-stretch gap-2 sm:flex-row sm:items-center sm:gap-2">
<Select value={companyId} onValueChange={setCompanyId}>
<SelectTrigger className="w-full min-w-56 sm:w-64">
<SelectValue placeholder="Todas as empresas" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="all">Todas as empresas</SelectItem>
{(companies ?? []).map((c) => (
<SelectItem key={c.id} value={c.id}>{c.name}</SelectItem>
))}
</SelectContent>
</Select>
<Select value={range} onValueChange={setRange}>
<SelectTrigger className="w-40">
<SelectValue placeholder="Período" />
</SelectTrigger>
<SelectContent className="rounded-xl">
<SelectItem value="7d">Últimos 7 dias</SelectItem>
<SelectItem value="30d">Últimos 30 dias</SelectItem>
<SelectItem value="90d">Últimos 90 dias</SelectItem>
<SelectItem value="all">Todos</SelectItem>
</SelectContent>
</Select>
<Button asChild size="sm" variant="outline">
<a href="/api/admin/alerts/hours-usage?range=30d&threshold=90">Disparar manualmente</a>
</Button>
</div>
</CardAction>
</CardHeader>
<CardContent>
{!alerts ? (
<div className="space-y-2">
{Array.from({ length: 5 }).map((_, i) => (
<Skeleton key={i} className="h-10 w-full rounded-lg" />
))}
</div>
) : alerts.length === 0 ? (
<p className="rounded-lg border border-dashed border-slate-200 p-6 text-sm text-neutral-500">
Nenhum alerta enviado ainda.
</p>
) : (
<div className="overflow-x-auto">
<table className="min-w-full border-separate border-spacing-y-2">
<thead>
<tr className="text-left text-xs font-semibold uppercase tracking-wide text-neutral-500">
<th className="px-2 py-1">Quando</th>
<th className="px-2 py-1">Empresa</th>
<th className="px-2 py-1">Uso</th>
<th className="px-2 py-1">Limite</th>
<th className="px-2 py-1">Período</th>
<th className="px-2 py-1">Destinatários</th>
<th className="px-2 py-1">Entregues</th>
</tr>
</thead>
<tbody>
{alerts.map((a) => (
<tr key={a._id} className="rounded-xl border border-slate-200 bg-white">
<td className="px-2 py-2 text-sm text-neutral-700">
{new Date(a.createdAt).toLocaleString("pt-BR")}
</td>
<td className="px-2 py-2 text-sm font-medium text-neutral-900">{a.companyName}</td>
<td className="px-2 py-2 text-sm text-neutral-700">{a.usagePct.toFixed(1)}%</td>
<td className="px-2 py-2 text-sm text-neutral-700">{a.threshold}%</td>
<td className="px-2 py-2 text-sm text-neutral-700">{a.range}</td>
<td className="px-2 py-2 text-sm text-neutral-700">{a.recipients.join(", ")}</td>
<td className="px-2 py-2 text-sm text-neutral-700">{a.deliveredCount}</td>
</tr>
))}
</tbody>
</table>
</div>
)}
</CardContent>
</Card>
)
}