reports(SLA): aplica filtro de período (7d/30d/90d) no Convex e inclui período no filename do CSV; admin(alerts): filtros no servidor; alerts: batch de últimos alertas por slugs; filtros persistentes de empresa (localStorage) em relatórios; prisma: Company.contractedHoursPerMonth; smtp: suporte a múltiplos destinatários e timeout opcional
This commit is contained in:
parent
a23b429e4d
commit
384d4411b6
13 changed files with 133 additions and 38 deletions
|
|
@ -129,21 +129,28 @@ function formatDateKey(timestamp: number) {
|
|||
|
||||
export const slaOverview = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
|
||||
handler: async (ctx, { tenantId, viewerId, companyId }) => {
|
||||
handler: async (ctx, { tenantId, viewerId, range, companyId }) => {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
let tickets = await fetchScopedTickets(ctx, tenantId, viewer);
|
||||
if (companyId) tickets = tickets.filter((t) => t.companyId === companyId)
|
||||
// Optional range filter (createdAt) for reporting purposes, similar ao backlog/csat
|
||||
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 queues = await fetchQueues(ctx, tenantId);
|
||||
|
||||
const now = Date.now();
|
||||
const openTickets = tickets.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
|
||||
const resolvedTickets = tickets.filter((ticket) => {
|
||||
const openTickets = inRange.filter((ticket) => OPEN_STATUSES.has(normalizeStatus(ticket.status)));
|
||||
const resolvedTickets = inRange.filter((ticket) => {
|
||||
const status = normalizeStatus(ticket.status);
|
||||
return status === "RESOLVED";
|
||||
});
|
||||
const overdueTickets = openTickets.filter((ticket) => ticket.dueAt && ticket.dueAt < now);
|
||||
|
||||
const firstResponseTimes = tickets
|
||||
const firstResponseTimes = inRange
|
||||
.filter((ticket) => ticket.firstResponseAt)
|
||||
.map((ticket) => (ticket.firstResponseAt! - ticket.createdAt) / 60000);
|
||||
const resolutionTimes = resolvedTickets
|
||||
|
|
@ -161,7 +168,7 @@ export const slaOverview = query({
|
|||
|
||||
return {
|
||||
totals: {
|
||||
total: tickets.length,
|
||||
total: inRange.length,
|
||||
open: openTickets.length,
|
||||
resolved: resolvedTickets.length,
|
||||
overdue: overdueTickets.length,
|
||||
|
|
@ -175,6 +182,7 @@ export const slaOverview = query({
|
|||
resolvedCount: resolutionTimes.length,
|
||||
},
|
||||
queueBreakdown,
|
||||
rangeDays: days,
|
||||
};
|
||||
},
|
||||
});
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue