feat: enhance tickets portal and admin flows

This commit is contained in:
Esdras Renan 2025-10-07 02:26:09 -03:00
parent 9cdd8763b4
commit c15f0a5b09
67 changed files with 1101 additions and 338 deletions

View file

@ -5,12 +5,31 @@ import type { Doc, Id } from "./_generated/dataModel";
import { requireStaff } from "./rbac";
type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED" | "CLOSED";
const STATUS_NORMALIZE_MAP: Record<string, TicketStatusNormalized> = {
NEW: "PENDING",
PENDING: "PENDING",
OPEN: "AWAITING_ATTENDANCE",
AWAITING_ATTENDANCE: "AWAITING_ATTENDANCE",
ON_HOLD: "PAUSED",
PAUSED: "PAUSED",
RESOLVED: "RESOLVED",
CLOSED: "CLOSED",
};
function normalizeStatus(status: string | null | undefined): TicketStatusNormalized {
if (!status) return "PENDING";
const normalized = STATUS_NORMALIZE_MAP[status.toUpperCase()];
return normalized ?? "PENDING";
}
function average(values: number[]) {
if (values.length === 0) return null;
return values.reduce((sum, value) => sum + value, 0) / values.length;
}
const OPEN_STATUSES = new Set(["NEW", "OPEN", "PENDING", "ON_HOLD"]);
const OPEN_STATUSES = new Set<TicketStatusNormalized>(["PENDING", "AWAITING_ATTENDANCE", "PAUSED"]);
const ONE_DAY_MS = 24 * 60 * 60 * 1000;
function percentageChange(current: number, previous: number) {
@ -116,8 +135,11 @@ export const slaOverview = query({
const queues = await fetchQueues(ctx, tenantId);
const now = Date.now();
const openTickets = tickets.filter((ticket) => OPEN_STATUSES.has(ticket.status));
const resolvedTickets = tickets.filter((ticket) => ticket.status === "RESOLVED" || ticket.status === "CLOSED");
const openTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
const resolvedTickets = tickets.filter((ticket) => {
const status = normalizeStatus(ticket.status);
return status === "RESOLVED" || status === "CLOSED";
});
const overdueTickets = openTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);
const firstResponseTimes = tickets
@ -193,17 +215,18 @@ export const backlogOverview = query({
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const statusCounts = tickets.reduce<Record<string, number>>((acc, ticket) => {
acc[ticket.status] = (acc[ticket.status] ?? 0) + 1;
const statusCounts = tickets.reduce<Record<TicketStatusNormalized, number>>((acc, ticket) => {
const status = normalizeStatus(ticket.status);
acc[status] = (acc[status] ?? 0) + 1;
return acc;
}, {});
}, {} as Record<TicketStatusNormalized, number>);
const priorityCounts = tickets.reduce<Record<string, number>>((acc, ticket) => {
acc[ticket.priority] = (acc[ticket.priority] ?? 0) + 1;
return acc;
}, {});
const openTickets = tickets.filter((ticket) => OPEN_STATUSES.has(ticket.status));
const openTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
const queueMap = new Map<string, { name: string; count: number }>();
for (const ticket of openTickets) {
@ -276,7 +299,7 @@ export const dashboardOverview = query({
const deltaMinutes =
averageWindow !== null && averagePrevious !== null ? averageWindow - averagePrevious : null;
const awaitingTickets = tickets.filter((ticket) => OPEN_STATUSES.has(ticket.status));
const awaitingTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
const atRiskTickets = awaitingTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);
const surveys = await collectCsatSurveys(ctx, tickets);