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:
parent
addd4ce6e8
commit
3bafcc5a0a
45 changed files with 1401 additions and 256 deletions
|
|
@ -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]) => ({
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue