feat: CSV exports, PDF improvements, play internal/external with hour split, roles cleanup, admin companies with 'Cliente avulso', ticket list spacing/alignment fixes, status translations and mappings

This commit is contained in:
Esdras Renan 2025-10-07 13:42:45 -03:00
parent addd4ce6e8
commit 3bafcc5a0a
45 changed files with 1401 additions and 256 deletions

View file

@ -5,7 +5,7 @@ import type { Doc, Id } from "./_generated/dataModel";
import { requireStaff } from "./rbac";
type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED" | "CLOSED";
type TicketStatusNormalized = "PENDING" | "AWAITING_ATTENDANCE" | "PAUSED" | "RESOLVED";
const STATUS_NORMALIZE_MAP: Record<string, TicketStatusNormalized> = {
NEW: "PENDING",
@ -15,7 +15,7 @@ const STATUS_NORMALIZE_MAP: Record<string, TicketStatusNormalized> = {
ON_HOLD: "PAUSED",
PAUSED: "PAUSED",
RESOLVED: "RESOLVED",
CLOSED: "CLOSED",
CLOSED: "RESOLVED",
};
function normalizeStatus(status: string | null | undefined): TicketStatusNormalized {
@ -128,7 +128,7 @@ function formatDateKey(timestamp: number) {
}
export const slaOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
handler: async (ctx, { tenantId, viewerId }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
@ -138,7 +138,7 @@ export const slaOverview = query({
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";
return status === "RESOLVED";
});
const overdueTickets = openTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);
@ -179,11 +179,17 @@ export const slaOverview = query({
});
export const csatOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
handler: async (ctx, { tenantId, viewerId, range }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const surveys = await collectCsatSurveys(ctx, tickets);
const surveysAll = await collectCsatSurveys(ctx, tickets);
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const end = new Date();
end.setUTCHours(0, 0, 0, 0);
const endMs = end.getTime() + ONE_DAY_MS;
const startMs = endMs - days * ONE_DAY_MS;
const surveys = surveysAll.filter((s) => s.receivedAt >= startMs && s.receivedAt < endMs);
const averageScore = average(surveys.map((item) => item.score));
const distribution = [1, 2, 3, 4, 5].map((score) => ({
@ -205,28 +211,37 @@ export const csatOverview = query({
score: item.score,
receivedAt: item.receivedAt,
})),
rangeDays: days,
};
},
});
export const backlogOverview = query({
args: { tenantId: v.string(), viewerId: v.id("users") },
handler: async (ctx, { tenantId, viewerId }) => {
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
handler: async (ctx, { tenantId, viewerId, range }) => {
const viewer = await requireStaff(ctx, viewerId, tenantId);
const tickets = await fetchScopedTickets(ctx, tenantId, viewer);
const statusCounts = tickets.reduce<Record<TicketStatusNormalized, number>>((acc, ticket) => {
// Optional range filter (createdAt) for reporting purposes
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90;
const end = new Date();
end.setUTCHours(0, 0, 0, 0);
const endMs = end.getTime() + ONE_DAY_MS;
const startMs = endMs - days * ONE_DAY_MS;
const inRange = tickets.filter((t) => t.createdAt >= startMs && t.createdAt < endMs);
const statusCounts = inRange.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) => {
const priorityCounts = inRange.reduce<Record<string, number>>((acc, ticket) => {
acc[ticket.priority] = (acc[ticket.priority] ?? 0) + 1;
return acc;
}, {});
const openTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
const queueMap = new Map<string, { name: string; count: number }>();
for (const ticket of openTickets) {
@ -245,6 +260,7 @@ export const backlogOverview = query({
}
return {
rangeDays: days,
statusCounts,
priorityCounts,
queueCounts: Array.from(queueMap.entries()).map(([id, data]) => ({