refine queue metrics and devices ui

This commit is contained in:
Esdras Renan 2025-11-04 19:53:54 -03:00
parent 1e45324460
commit c2acd65764
11 changed files with 181 additions and 116 deletions

View file

@ -1,14 +1,5 @@
import { cronJobs } from "convex/server" import { cronJobs } from "convex/server"
import { api } from "./_generated/api"
const crons = cronJobs() const crons = cronJobs()
// Check hourly and the action will gate by America/Sao_Paulo hour
crons.interval(
"hours-usage-alerts-hourly",
{ hours: 1 },
api.alerts_actions.sendHoursUsageAlerts,
{}
)
export default crons export default crons

View file

@ -113,20 +113,39 @@ export const summary = query({
const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect(); const queues = await ctx.db.query("queues").withIndex("by_tenant", (q) => q.eq("tenantId", tenantId)).collect();
const result = await Promise.all( const result = await Promise.all(
queues.map(async (qItem) => { queues.map(async (qItem) => {
const pending = await ctx.db const tickets = await ctx.db
.query("tickets") .query("tickets")
.withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", qItem._id)) .withIndex("by_tenant_queue", (q) => q.eq("tenantId", tenantId).eq("queueId", qItem._id))
.collect(); .collect();
const waiting = pending.filter((t) => { let pending = 0;
const status = normalizeStatus(t.status); let inProgress = 0;
return status === "PENDING" || status === "PAUSED"; let paused = 0;
}).length; let breached = 0;
const open = pending.filter((t) => { const now = Date.now();
const status = normalizeStatus(t.status); for (const ticket of tickets) {
return status !== "RESOLVED"; const status = normalizeStatus(ticket.status);
}).length; if (status === "PENDING") {
const breached = 0; pending += 1;
return { id: qItem._id, name: renameQueueString(qItem.name), pending: open, waiting, breached }; } else if (status === "AWAITING_ATTENDANCE") {
inProgress += 1;
} else if (status === "PAUSED") {
paused += 1;
}
if (status !== "RESOLVED") {
const dueAt = typeof ticket.dueAt === "number" ? ticket.dueAt : null;
if (dueAt && dueAt < now) {
breached += 1;
}
}
}
return {
id: qItem._id,
name: renameQueueString(qItem.name),
pending,
inProgress,
paused,
breached,
};
}) })
); );
return result; return result;

35
docs/requests-status.md Normal file
View file

@ -0,0 +1,35 @@
# Solicitações mapeadas
## Tickets e fluxo
- [feito] Revisão dos status para Pendente / Em andamento / Pausado / Resolvido em listagens e componente de fila.
- [feito] Botões de play interno/externo com rastreio de horas.
- [feito] Pausa com motivo obrigatório registrado na timeline.
- [feito] Encerramento manual com diálogo atualizado e templates.
- [pendente] Encerramento automático após 3 tentativas de contato (não implementado).
## Comentários e notificações
- [feito] Comentários internos como padrão para equipe; públicos visíveis ao cliente.
- [feito] Exportação do histórico completo do ticket em PDF.
- [feito] E-mails automáticos para comentários públicos e encerramento.
- [não aplicável] Resumo com IA (feature solicitada, ainda não iniciada).
## Relatórios e contratos
- [feito] Relatório de horas internas x externas por cliente com exportação XLSX.
- [feito] Campo "Cliente avulso" e horas contratadas no cadastro de empresas, com alertas configuráveis.
- [removido] Alertas automáticos por e-mail de uso de horas (cron desativado a pedido).
## Inventário e dispositivos
- [feito] Unificação de máquinas em “Dispositivos”, suporte a desktops e celulares.
- [feito] Campos personalizados e templates de exportação no inventário.
- [aguardando uso] Tag de serviço pode ser adicionada como campo personalizado conforme necessidade.
## Perfis e acesso
- [feito] Perfis Admin/Agente/Gestor/Usuário com restrições de visualização por empresa.
- [feito] Motivo da troca de responsável registrado como comentário interno.
- [feito] Templates de comentário e encerramento gerenciáveis na área de configurações.
- [feito] Ajuste de horas manual com motivo e log.
## Itens adicionais
- [pendente] Dashboard personalizado para gestores (fora do escopo atual).
- [pendente] Integração com IA/assistente para resumo automático.
- [pendente] Gatilho automático para encerramento por falta de contato (ver item em Tickets e fluxo).

View file

@ -3523,7 +3523,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
<button <button
type="button" type="button"
onClick={copyEmail} onClick={copyEmail}
className="inline-flex items-center rounded p-1 text-neutral-500 transition hover:bg-slate-100 hover:text-neutral-700 focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-300" className="inline-flex items-center rounded-md p-1 text-neutral-500 transition hover:bg-[#00d6eb]/15 hover:text-[#0a4760] focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[#00d6eb]/40 focus-visible:ring-offset-2"
title="Copiar e-mail do dispositivo" title="Copiar e-mail do dispositivo"
aria-label="Copiar e-mail do dispositivo" aria-label="Copiar e-mail do dispositivo"
> >
@ -3541,13 +3541,18 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</div> </div>
{/* Campos personalizados (posicionado logo após métricas) */} {/* Campos personalizados (posicionado logo após métricas) */}
<div className="space-y-3"> <div className="space-y-3">
<div className="flex items-center justify-between"> <div className="flex flex-wrap items-start justify-between gap-3">
<div className="flex flex-col gap-1">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<h4 className="text-sm font-semibold text-neutral-900">Campos personalizados</h4> <h4 className="text-sm font-semibold text-neutral-900">Campos personalizados</h4>
<Badge variant="outline" className="rounded-full px-2.5 py-0.5 text-[11px] font-semibold"> <Badge variant="outline" className="rounded-full px-2.5 py-0.5 text-[11px] font-semibold">
{(device.customFields ?? []).length} {(device.customFields ?? []).length}
</Badge> </Badge>
</div> </div>
{(!device.customFields || device.customFields.length === 0) ? (
<p className="text-xs text-neutral-500">Nenhum campo personalizado definido para este dispositivo.</p>
) : null}
</div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Button size="sm" variant="outline" className="gap-2" onClick={() => setCustomFieldsEditorOpen(true)}> <Button size="sm" variant="outline" className="gap-2" onClick={() => setCustomFieldsEditorOpen(true)}>
<Pencil className="size-4" /> <Pencil className="size-4" />
@ -3564,9 +3569,7 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</div> </div>
))} ))}
</div> </div>
) : ( ) : null}
<p className="text-xs text-neutral-500">Nenhum campo personalizado definido para este dispositivo.</p>
)}
</div> </div>
<div className="rounded-2xl border border-[color:var(--accent)] bg-[color:var(--accent)]/80 px-4 py-4"> <div className="rounded-2xl border border-[color:var(--accent)] bg-[color:var(--accent)]/80 px-4 py-4">
@ -3587,18 +3590,10 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
</p> </p>
)} )}
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center">
<div className="flex h-10 min-w-[56px] items-center justify-center rounded-xl border border-[color:var(--accent)] bg-white px-3 text-[color:var(--accent-foreground)] shadow-sm"> <div className="flex h-10 min-w-[56px] items-center justify-center rounded-xl border border-[color:var(--accent)] bg-white px-3 text-[color:var(--accent-foreground)] shadow-sm">
<span className="text-lg font-semibold leading-none tabular-nums">{totalOpenTickets}</span> <span className="text-lg font-semibold leading-none tabular-nums">{totalOpenTickets}</span>
</div> </div>
{deviceTicketsHref ? (
<Link
href={deviceTicketsHref}
className="text-xs font-semibold text-accent-foreground underline-offset-4 transition hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-foreground)] focus-visible:ring-offset-2"
>
Ver todos
</Link>
) : null}
</div> </div>
</div> </div>
{totalOpenTickets > 0 ? ( {totalOpenTickets > 0 ? (
@ -3630,6 +3625,16 @@ export function DeviceDetails({ device }: DeviceDetailsProps) {
})} })}
</div> </div>
) : null} ) : null}
{deviceTicketsHref ? (
<div className="mt-4">
<Link
href={deviceTicketsHref}
className="text-xs font-semibold text-[color:var(--accent-foreground)] underline-offset-4 transition hover:underline focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-[color:var(--accent-foreground)] focus-visible:ring-offset-2"
>
Ver todos
</Link>
</div>
) : null}
</div> </div>
<div className="flex flex-wrap gap-2"> <div className="flex flex-wrap gap-2">

View file

@ -11,6 +11,7 @@ import {
PanelsTopLeft, PanelsTopLeft,
UserCog, UserCog,
Building2, Building2,
Skyscraper,
Waypoints, Waypoints,
Clock4, Clock4,
Timer, Timer,
@ -105,7 +106,7 @@ const navigation: NavigationGroup[] = [
{ {
title: "Empresas", title: "Empresas",
url: "/admin/companies", url: "/admin/companies",
icon: Building2, icon: Skyscraper,
requiredRole: "admin", requiredRole: "admin",
}, },
{ title: "Usuários", url: "/admin/users", icon: Users, requiredRole: "admin" }, { title: "Usuários", url: "/admin/users", icon: Users, requiredRole: "admin" },

View file

@ -1,6 +1,7 @@
"use client" "use client"
import { useEffect, useState } from "react" import { useEffect, useMemo, useState } from "react"
import Link from "next/link"
import { useQuery } from "convex/react" import { useQuery } from "convex/react"
import { api } from "@/convex/_generated/api" import { api } from "@/convex/_generated/api"
import type { Id } from "@/convex/_generated/dataModel" import type { Id } from "@/convex/_generated/dataModel"
@ -8,14 +9,7 @@ import { DEFAULT_TENANT_ID } from "@/lib/constants"
import { useAuth } from "@/lib/auth-client" import { useAuth } from "@/lib/auth-client"
import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card" import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"
import { Skeleton } from "@/components/ui/skeleton" import { Skeleton } from "@/components/ui/skeleton"
import { Badge } from "@/components/ui/badge" import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from "@/components/ui/select"
import {
Select,
SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from "@/components/ui/select"
import { import {
ChartConfig, ChartConfig,
ChartContainer, ChartContainer,
@ -25,6 +19,12 @@ import {
ChartTooltipContent, ChartTooltipContent,
} from "@/components/ui/chart" } from "@/components/ui/chart"
import { Area, AreaChart, CartesianGrid, XAxis, Bar, BarChart, Pie, PieChart } from "recharts" import { Area, AreaChart, CartesianGrid, XAxis, Bar, BarChart, Pie, PieChart } from "recharts"
import { SearchableCombobox, type SearchableComboboxOption } from "@/components/ui/searchable-combobox"
import { TicketPriorityPill } from "@/components/tickets/priority-pill"
import { TicketStatusBadge } from "@/components/tickets/status-badge"
import { formatDistanceToNow } from "date-fns"
import { ptBR } from "date-fns/locale"
import type { TicketPriority, TicketStatus } from "@/lib/schemas/ticket"
type CompanyRecord = { id: Id<"companies">; name: string } type CompanyRecord = { id: Id<"companies">; name: string }
@ -67,11 +67,20 @@ export function CompanyReport() {
isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip" isStaff && convexUserId ? { tenantId, viewerId: convexUserId as Id<"users"> } : "skip"
) as CompanyRecord[] | undefined ) as CompanyRecord[] | undefined
const companyOptions = useMemo<SearchableComboboxOption[]>(
() =>
(companies ?? []).map((company) => ({
value: company.id as string,
label: company.name,
})),
[companies]
)
useEffect(() => { useEffect(() => {
if (!selectedCompany && companies && companies.length > 0) { if (!selectedCompany && companyOptions.length > 0) {
setSelectedCompany(companies[0].id) setSelectedCompany(companyOptions[0]?.value ?? "")
} }
}, [companies, selectedCompany]) }, [companyOptions, selectedCompany])
const report = useQuery( const report = useQuery(
api.reports.companyOverview, api.reports.companyOverview,
@ -142,18 +151,19 @@ export function CompanyReport() {
<p className="text-sm text-neutral-500">Acompanhe tickets, inventário e colaboradores de um cliente específico.</p> <p className="text-sm text-neutral-500">Acompanhe tickets, inventário e colaboradores de um cliente específico.</p>
</div> </div>
<div className="flex flex-wrap items-center gap-3"> <div className="flex flex-wrap items-center gap-3">
<Select value={selectedCompany} onValueChange={setSelectedCompany} disabled={!companies?.length}> <div className="w-[260px]">
<SelectTrigger className="w-[220px] rounded-xl border-slate-200"> <SearchableCombobox
<SelectValue placeholder="Selecione a empresa" /> value={selectedCompany || null}
</SelectTrigger> onValueChange={(value) => {
<SelectContent className="rounded-xl"> if (value) setSelectedCompany(value)
{(companies ?? []).map((company) => ( }}
<SelectItem key={company.id} value={company.id as string}> options={companyOptions}
{company.name} placeholder="Selecionar empresa"
</SelectItem> searchPlaceholder="Buscar empresa..."
))} emptyText="Nenhuma empresa encontrada."
</SelectContent> disabled={!companyOptions.length}
</Select> />
</div>
<Select value={timeRange} onValueChange={(value) => setTimeRange(value as typeof timeRange)}> <Select value={timeRange} onValueChange={(value) => setTimeRange(value as typeof timeRange)}>
<SelectTrigger className="w-[160px] rounded-xl border-slate-200"> <SelectTrigger className="w-[160px] rounded-xl border-slate-200">
<SelectValue placeholder="Período" /> <SelectValue placeholder="Período" />
@ -306,10 +316,8 @@ export function CompanyReport() {
<Card className="border-slate-200"> <Card className="border-slate-200">
<CardHeader> <CardHeader>
<CardTitle className="text-lg font-semibold text-neutral-900">Tickets recentes (máximo 6)</CardTitle> <CardTitle className="text-lg font-semibold text-neutral-900">Tickets recentes</CardTitle>
<CardDescription className="text-neutral-600"> <CardDescription className="text-neutral-600">Chamados em aberto para a empresa selecionada.</CardDescription>
Chamados em aberto para a empresa filtrada.
</CardDescription>
</CardHeader> </CardHeader>
<CardContent className="space-y-3"> <CardContent className="space-y-3">
{openTickets.length === 0 ? ( {openTickets.length === 0 ? (
@ -317,11 +325,12 @@ export function CompanyReport() {
Nenhum chamado aberto no período selecionado. Nenhum chamado aberto no período selecionado.
</p> </p>
) : ( ) : (
<ul className="space-y-3"> <div className="space-y-3">
{openTickets.map((ticket) => ( {openTickets.map((ticket) => (
<li <Link
key={ticket.id} key={ticket.id}
className="flex flex-wrap items-center justify-between gap-3 rounded-xl border border-slate-200 bg-white px-3 py-2 shadow-sm" href={`/tickets/${ticket.id}`}
className="group flex flex-wrap items-center justify-between gap-3 rounded-xl border border-slate-200 bg-white px-4 py-3 text-sm shadow-sm transition hover:-translate-y-0.5 hover:border-slate-300 hover:shadow-md focus-visible:outline-none focus-visible:ring-2 focus-visible:ring-slate-300 focus-visible:ring-offset-2"
> >
<div className="min-w-0 flex-1"> <div className="min-w-0 flex-1">
<p className="truncate text-sm font-semibold text-neutral-900"> <p className="truncate text-sm font-semibold text-neutral-900">
@ -329,25 +338,25 @@ export function CompanyReport() {
</p> </p>
<p className="text-xs text-neutral-500"> <p className="text-xs text-neutral-500">
Atualizado{" "} Atualizado{" "}
{new Date(ticket.updatedAt).toLocaleString("pt-BR", { {formatDistanceToNow(new Date(ticket.updatedAt), {
day: "2-digit", addSuffix: true,
month: "short", locale: ptBR,
hour: "2-digit",
minute: "2-digit",
})} })}
</p> </p>
</div> </div>
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
<Badge variant="outline" className="border-slate-200 text-[11px] uppercase text-neutral-600"> <TicketPriorityPill
{ticket.priority} priority={ticket.priority as TicketPriority}
</Badge> className="h-8 rounded-full px-3 text-xs"
<Badge className="bg-indigo-600 text-[11px] uppercase tracking-wide text-white"> />
{STATUS_LABELS[ticket.status] ?? ticket.status} <TicketStatusBadge
</Badge> status={ticket.status as TicketStatus}
className="h-8 px-3 text-xs font-semibold"
/>
</div> </div>
</li> </Link>
))} ))}
</ul> </div>
)} )}
</CardContent> </CardContent>
</Card> </Card>

View file

@ -544,7 +544,7 @@ export function NewTicketDialog({ triggerClassName }: { triggerClassName?: strin
</DialogTrigger> </DialogTrigger>
<DialogContent className="max-w-4xl gap-0 overflow-hidden rounded-3xl border border-slate-200 bg-white p-0 shadow-2xl lg:max-w-5xl"> <DialogContent className="max-w-4xl gap-0 overflow-hidden rounded-3xl border border-slate-200 bg-white p-0 shadow-2xl lg:max-w-5xl">
<div className="max-h-[88vh] overflow-y-auto"> <div className="max-h-[88vh] overflow-y-auto">
<div className="space-y-5 px-6 py-7 sm:px-8 md:px-10"> <div className="space-y-5 px-6 pt-7 pb-12 sm:px-8 md:px-10">
<form className="space-y-6" onSubmit={form.handleSubmit(submit)}> <form className="space-y-6" onSubmit={form.handleSubmit(submit)}>
<div className="flex flex-col gap-4 border-b border-slate-200 pb-5 md:flex-row md:items-start md:justify-between"> <div className="flex flex-col gap-4 border-b border-slate-200 pb-5 md:flex-row md:items-start md:justify-between">
<DialogHeader className="gap-1.5 p-0"> <DialogHeader className="gap-1.5 p-0">

View file

@ -64,8 +64,9 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
id: "default", id: "default",
name: "Geral", name: "Geral",
pending: queueSummary.reduce((acc, item) => acc + item.pending, 0), pending: queueSummary.reduce((acc, item) => acc + item.pending, 0),
waiting: queueSummary.reduce((acc, item) => acc + item.waiting, 0), inProgress: queueSummary.reduce((acc, item) => acc + item.inProgress, 0),
breached: 0, paused: queueSummary.reduce((acc, item) => acc + item.paused, 0),
breached: queueSummary.reduce((acc, item) => acc + item.breached, 0),
}, },
nextTicket: nextTicketUi, nextTicket: nextTicketUi,
} }
@ -127,11 +128,15 @@ export function PlayNextTicketCard({ context }: PlayNextTicketCardProps) {
<span className="font-semibold text-neutral-900">{cardContext.queue.pending}</span> <span className="font-semibold text-neutral-900">{cardContext.queue.pending}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>Em espera</span> <span>Em andamento</span>
<span className="font-semibold text-neutral-900">{cardContext.queue.waiting}</span> <span className="font-semibold text-neutral-900">{cardContext.queue.inProgress}</span>
</div> </div>
<div className="flex items-center justify-between"> <div className="flex items-center justify-between">
<span>SLA violado</span> <span>Pausados</span>
<span className="font-semibold text-neutral-900">{cardContext.queue.paused}</span>
</div>
<div className="flex items-center justify-between">
<span>Fora do SLA</span>
<span className="font-semibold text-red-600">{cardContext.queue.breached}</span> <span className="font-semibold text-red-600">{cardContext.queue.breached}</span>
</div> </div>
</div> </div>

View file

@ -221,10 +221,6 @@ export function TicketCsatCard({ ticket }: TicketCsatCardProps) {
</span> </span>
{ratedAtRelative ? `${ratedAtRelative}` : null} {ratedAtRelative ? `${ratedAtRelative}` : null}
</p> </p>
) : viewerIsStaff ? (
<div className="flex items-center gap-2 rounded-lg border border-dashed border-slate-200 bg-slate-50 px-3 py-2 text-xs text-neutral-600">
Nenhuma avaliação registrada ainda.
</div>
) : null} ) : null}
{canSubmit ? ( {canSubmit ? (
<div className="space-y-2"> <div className="space-y-2">

View file

@ -38,8 +38,8 @@ export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) {
return ( return (
<div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(22rem,1fr))]"> <div className="grid gap-4 grid-cols-[repeat(auto-fit,minmax(22rem,1fr))]">
{data.map((queue) => { {data.map((queue) => {
const total = queue.pending + queue.waiting const totalOpen = queue.pending + queue.inProgress + queue.paused
const breachPercent = total === 0 ? 0 : Math.round((queue.breached / total) * 100) const breachPercent = totalOpen === 0 ? 0 : Math.round((queue.breached / totalOpen) * 100)
return ( return (
<Card <Card
key={queue.id} key={queue.id}
@ -52,7 +52,7 @@ export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) {
</CardTitle> </CardTitle>
</CardHeader> </CardHeader>
<CardContent className="space-y-3 sm:space-y-4"> <CardContent className="space-y-3 sm:space-y-4">
<div className="grid min-w-0 grid-cols-1 gap-2.5 sm:grid-cols-2"> <div className="grid min-w-0 grid-cols-1 gap-2.5 sm:grid-cols-3">
<div className="flex min-w-0 flex-col items-start justify-between gap-2 rounded-xl border border-slate-200 bg-gradient-to-br from-white via-white to-slate-100 px-3 py-2.5 text-left shadow-sm lg:px-4 lg:py-3"> <div className="flex min-w-0 flex-col items-start justify-between gap-2 rounded-xl border border-slate-200 bg-gradient-to-br from-white via-white to-slate-100 px-3 py-2.5 text-left shadow-sm lg:px-4 lg:py-3">
<p className="min-w-0 pr-0.5 text-[0.68rem] font-semibold uppercase leading-tight tracking-[0.02em] text-neutral-500 sm:text-[0.72rem] lg:text-xs"> <p className="min-w-0 pr-0.5 text-[0.68rem] font-semibold uppercase leading-tight tracking-[0.02em] text-neutral-500 sm:text-[0.72rem] lg:text-xs">
Pendentes Pendentes
@ -63,25 +63,28 @@ export function TicketQueueSummaryCards({ queues }: TicketQueueSummaryProps) {
</div> </div>
<div className="flex min-w-0 flex-col items-start justify-between gap-2 rounded-xl border border-sky-200 bg-gradient-to-br from-white via-white to-sky-50 px-3 py-2.5 text-left shadow-sm lg:px-4 lg:py-3"> <div className="flex min-w-0 flex-col items-start justify-between gap-2 rounded-xl border border-sky-200 bg-gradient-to-br from-white via-white to-sky-50 px-3 py-2.5 text-left shadow-sm lg:px-4 lg:py-3">
<p className="min-w-0 pr-0.5 text-[0.68rem] font-semibold uppercase leading-tight tracking-[0.02em] text-sky-700 sm:text-[0.72rem] lg:text-xs"> <p className="min-w-0 pr-0.5 text-[0.68rem] font-semibold uppercase leading-tight tracking-[0.02em] text-sky-700 sm:text-[0.72rem] lg:text-xs">
Aguardando resposta Em andamento
</p> </p>
<p className="text-2xl font-bold tracking-tight text-sky-700 tabular-nums sm:text-3xl"> <p className="text-2xl font-bold tracking-tight text-sky-700 tabular-nums sm:text-3xl">
{queue.waiting} {queue.inProgress}
</p> </p>
</div> </div>
<div className="flex min-w-0 flex-col items-start justify-between gap-2 rounded-xl border border-amber-200 bg-gradient-to-br from-white via-white to-amber-50 px-3 py-2.5 text-left shadow-sm sm:col-span-2 lg:px-4 lg:py-3"> <div className="flex min-w-0 flex-col items-start justify-between gap-2 rounded-xl border border-amber-200 bg-gradient-to-br from-white via-white to-amber-50 px-3 py-2.5 text-left shadow-sm lg:px-4 lg:py-3">
<p className="min-w-0 pr-0.5 text-[0.68rem] font-semibold uppercase leading-tight tracking-[0.02em] text-amber-700 sm:text-[0.72rem] lg:text-xs"> <p className="min-w-0 pr-0.5 text-[0.68rem] font-semibold uppercase leading-tight tracking-[0.02em] text-amber-700 sm:text-[0.72rem] lg:text-xs">
Violados Pausados
</p> </p>
<p className="text-2xl font-bold tracking-tight text-amber-700 tabular-nums sm:text-3xl"> <p className="text-2xl font-bold tracking-tight text-amber-700 tabular-nums sm:text-3xl">
{queue.breached} {queue.paused}
</p> </p>
</div> </div>
</div> </div>
<div className="pt-1"> <div className="pt-1">
<Progress value={breachPercent} className="h-1.5 bg-slate-100" indicatorClassName="bg-[#00e8ff]" /> <Progress value={breachPercent} className="h-1.5 bg-slate-100" indicatorClassName="bg-[#00e8ff]" />
<span className="mt-2 block text-xs text-neutral-500"> <span className="mt-2 block text-xs text-neutral-500">
{breachPercent}% com SLA violado nesta fila {breachPercent}% dos chamados da fila estão fora do SLA
</span>
<span className="mt-1 block text-xs text-neutral-400">
Em atraso: {queue.breached}
</span> </span>
</div> </div>
</CardContent> </CardContent>

View file

@ -198,7 +198,8 @@ export const ticketQueueSummarySchema = z.object({
id: z.string(), id: z.string(),
name: z.string(), name: z.string(),
pending: z.number(), pending: z.number(),
waiting: z.number(), inProgress: z.number(),
paused: z.number(),
breached: z.number(), breached: z.number(),
}) })
export type TicketQueueSummary = z.infer<typeof ticketQueueSummarySchema> export type TicketQueueSummary = z.infer<typeof ticketQueueSummarySchema>