feat(reports): add date range filters and extend machine reports
This commit is contained in:
parent
82875a2252
commit
5b22065609
11 changed files with 742 additions and 290 deletions
|
|
@ -109,6 +109,53 @@ function withUpperBound<T>(range: T, field: string, value: number): T {
|
|||
return range;
|
||||
}
|
||||
|
||||
function parseIsoDateToMs(value?: string | null): number | null {
|
||||
if (!value) return null;
|
||||
const [yearStr, monthStr, dayStr] = value.split("-");
|
||||
const year = Number(yearStr);
|
||||
const month = Number(monthStr);
|
||||
const day = Number(dayStr);
|
||||
if (!year || !month || !day) return null;
|
||||
const date = new Date(Date.UTC(year, month - 1, day, 0, 0, 0, 0));
|
||||
return date.getTime();
|
||||
}
|
||||
|
||||
function resolveRangeWindow(
|
||||
range: string | undefined,
|
||||
dateFrom: string | undefined,
|
||||
dateTo: string | undefined,
|
||||
defaultDays: number,
|
||||
): { startMs: number; endMs: number; days: number } {
|
||||
const fromMs = parseIsoDateToMs(dateFrom);
|
||||
const toMs = parseIsoDateToMs(dateTo);
|
||||
|
||||
if (fromMs !== null || toMs !== null) {
|
||||
const startMs = fromMs ?? toMs ?? Date.now();
|
||||
const endMsBase = toMs ?? fromMs ?? startMs;
|
||||
const endMs = endMsBase + ONE_DAY_MS;
|
||||
const days = Math.max(1, Math.round((endMs - startMs) / ONE_DAY_MS));
|
||||
return { startMs, endMs, days };
|
||||
}
|
||||
|
||||
const normalizedRange = range ?? "90d";
|
||||
let days = defaultDays;
|
||||
if (normalizedRange === "7d") days = 7;
|
||||
else if (normalizedRange === "30d") days = 30;
|
||||
else if (normalizedRange === "365d" || normalizedRange === "12m") days = 365;
|
||||
else if (normalizedRange === "all") {
|
||||
const now = new Date();
|
||||
now.setUTCHours(0, 0, 0, 0);
|
||||
const endMs = now.getTime() + ONE_DAY_MS;
|
||||
return { startMs: 0, endMs, days: 0 };
|
||||
}
|
||||
|
||||
const end = new Date();
|
||||
end.setUTCHours(0, 0, 0, 0);
|
||||
const endMs = end.getTime() + ONE_DAY_MS;
|
||||
const startMs = endMs - days * ONE_DAY_MS;
|
||||
return { startMs, endMs, days };
|
||||
}
|
||||
|
||||
function resolveScopedCompanyId(
|
||||
viewer: Awaited<ReturnType<typeof requireStaff>>,
|
||||
companyId?: Id<"companies">,
|
||||
|
|
@ -469,15 +516,24 @@ function formatDateKey(timestamp: number) {
|
|||
|
||||
export async function slaOverviewHandler(
|
||||
ctx: QueryCtx,
|
||||
{ tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
|
||||
{
|
||||
tenantId,
|
||||
viewerId,
|
||||
range,
|
||||
companyId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}: {
|
||||
tenantId: string
|
||||
viewerId: Id<"users">
|
||||
range?: string
|
||||
companyId?: Id<"companies">
|
||||
dateFrom?: string
|
||||
dateTo?: string
|
||||
}
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
// 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 { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
|
||||
const inRange = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId);
|
||||
const queues = await fetchQueues(ctx, tenantId);
|
||||
const categoriesMap = await fetchCategoryMap(ctx, tenantId);
|
||||
|
|
@ -616,14 +672,17 @@ export const triggerScheduledExports = action({
|
|||
|
||||
export async function csatOverviewHandler(
|
||||
ctx: QueryCtx,
|
||||
{ tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
|
||||
{
|
||||
tenantId,
|
||||
viewerId,
|
||||
range,
|
||||
companyId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
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 { startMs, endMs } = resolveRangeWindow(range, dateFrom, dateTo, 90);
|
||||
const tickets = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId);
|
||||
const surveys = (await collectCsatSurveys(ctx, tickets)).filter((s) => s.receivedAt >= startMs && s.receivedAt < endMs);
|
||||
|
||||
|
|
@ -700,27 +759,37 @@ export async function csatOverviewHandler(
|
|||
assigneeId: item.assigneeId,
|
||||
assigneeName: item.assigneeName,
|
||||
})),
|
||||
rangeDays: days,
|
||||
rangeDays: Math.max(1, Math.round((endMs - startMs) / ONE_DAY_MS)),
|
||||
positiveRate,
|
||||
byAgent,
|
||||
};
|
||||
}
|
||||
|
||||
export const csatOverview = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
range: v.optional(v.string()),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: csatOverviewHandler,
|
||||
});
|
||||
|
||||
export async function openedResolvedByDayHandler(
|
||||
ctx: QueryCtx,
|
||||
{ tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
|
||||
{
|
||||
tenantId,
|
||||
viewerId,
|
||||
range,
|
||||
companyId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
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 { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
|
||||
|
||||
const openedTickets = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId);
|
||||
const resolvedTickets = await fetchScopedTicketsByResolvedRange(ctx, tenantId, viewer, startMs, endMs, companyId);
|
||||
|
|
@ -761,21 +830,30 @@ export async function openedResolvedByDayHandler(
|
|||
}
|
||||
|
||||
export const openedResolvedByDay = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
range: v.optional(v.string()),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: openedResolvedByDayHandler,
|
||||
})
|
||||
|
||||
export async function backlogOverviewHandler(
|
||||
ctx: QueryCtx,
|
||||
{ tenantId, viewerId, range, companyId }: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
|
||||
{
|
||||
tenantId,
|
||||
viewerId,
|
||||
range,
|
||||
companyId,
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId);
|
||||
// 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 { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90);
|
||||
const inRange = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId);
|
||||
|
||||
const statusCounts = inRange.reduce<Record<TicketStatusNormalized, number>>((acc, ticket) => {
|
||||
|
|
@ -821,7 +899,14 @@ export async function backlogOverviewHandler(
|
|||
}
|
||||
|
||||
export const backlogOverview = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()), companyId: v.optional(v.id("companies")) },
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
range: v.optional(v.string()),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: backlogOverviewHandler,
|
||||
});
|
||||
|
||||
|
|
@ -1023,14 +1108,12 @@ export async function ticketCategoryInsightsHandler(
|
|||
viewerId,
|
||||
range,
|
||||
companyId,
|
||||
}: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies"> }
|
||||
dateFrom,
|
||||
dateTo,
|
||||
}: { tenantId: string; viewerId: Id<"users">; range?: string; companyId?: Id<"companies">; dateFrom?: string; dateTo?: string }
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId)
|
||||
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 { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
|
||||
|
||||
const inRange = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId)
|
||||
const categories = await ctx.db
|
||||
|
|
@ -1141,6 +1224,8 @@ export const categoryInsights = query({
|
|||
viewerId: v.id("users"),
|
||||
range: v.optional(v.string()),
|
||||
companyId: v.optional(v.id("companies")),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: ticketCategoryInsightsHandler,
|
||||
})
|
||||
|
|
@ -1347,14 +1432,17 @@ export async function ticketsByMachineAndCategoryHandler(
|
|||
}
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId)
|
||||
const days = range === "7d" ? 7 : range === "30d" ? 30 : 90
|
||||
const { startMs, endMs, days } = resolveRangeWindow(range, undefined, undefined, 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 tickets = await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId)
|
||||
const tickets =
|
||||
days === 0
|
||||
? await fetchScopedTickets(ctx, tenantId, viewer).then((all) =>
|
||||
all.filter((ticket) => {
|
||||
if (companyId && ticket.companyId && ticket.companyId !== companyId) return false
|
||||
return true
|
||||
}),
|
||||
)
|
||||
: await fetchScopedTicketsByCreatedRange(ctx, tenantId, viewer, startMs, endMs, companyId)
|
||||
const categoriesMap = await fetchCategoryMap(ctx, tenantId)
|
||||
|
||||
const companyIds = new Set<Id<"companies">>()
|
||||
|
|
@ -1458,7 +1546,7 @@ export async function ticketsByMachineAndCategoryHandler(
|
|||
})
|
||||
|
||||
return {
|
||||
rangeDays: days,
|
||||
rangeDays: days === 0 ? -1 : days,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
|
@ -1471,6 +1559,8 @@ export const ticketsByMachineAndCategory = query({
|
|||
companyId: v.optional(v.id("companies")),
|
||||
machineId: v.optional(v.id("machines")),
|
||||
userId: v.optional(v.id("users")),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: ticketsByMachineAndCategoryHandler,
|
||||
})
|
||||
|
|
@ -1506,11 +1596,7 @@ export async function hoursByMachineHandler(
|
|||
const viewer = await requireStaff(ctx, viewerId, tenantId)
|
||||
const tickets = await fetchScopedTickets(ctx, tenantId, viewer)
|
||||
|
||||
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 { startMs, endMs, days } = resolveRangeWindow(range, undefined, undefined, 90)
|
||||
|
||||
const machinesById = new Map<string, Doc<"machines"> | null>()
|
||||
const companiesById = new Map<string, Doc<"companies"> | null>()
|
||||
|
|
@ -1518,7 +1604,7 @@ export async function hoursByMachineHandler(
|
|||
const map = new Map<string, MachineHoursEntry>()
|
||||
|
||||
for (const t of tickets) {
|
||||
if (t.updatedAt < startMs || t.updatedAt >= endMs) continue
|
||||
if (days !== 0 && (t.updatedAt < startMs || t.updatedAt >= endMs)) continue
|
||||
if (companyId && t.companyId && t.companyId !== companyId) continue
|
||||
if (machineId && t.machineId !== machineId) continue
|
||||
if (userId && t.requesterId !== userId) continue
|
||||
|
|
@ -1588,7 +1674,7 @@ export async function hoursByMachineHandler(
|
|||
})
|
||||
|
||||
return {
|
||||
rangeDays: days,
|
||||
rangeDays: days === 0 ? -1 : days,
|
||||
items,
|
||||
}
|
||||
}
|
||||
|
|
@ -1601,22 +1687,20 @@ export const hoursByMachine = query({
|
|||
companyId: v.optional(v.id("companies")),
|
||||
machineId: v.optional(v.id("machines")),
|
||||
userId: v.optional(v.id("users")),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: hoursByMachineHandler,
|
||||
})
|
||||
|
||||
export async function hoursByClientHandler(
|
||||
ctx: QueryCtx,
|
||||
{ tenantId, viewerId, range }: { tenantId: string; viewerId: Id<"users">; range?: string }
|
||||
{ tenantId, viewerId, range, dateFrom, dateTo }: { tenantId: string; viewerId: Id<"users">; range?: string; dateFrom?: string; dateTo?: string }
|
||||
) {
|
||||
const viewer = await requireStaff(ctx, viewerId, tenantId)
|
||||
const tickets = await fetchScopedTickets(ctx, tenantId, viewer)
|
||||
|
||||
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 { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
|
||||
|
||||
type Acc = {
|
||||
companyId: Id<"companies">
|
||||
|
|
@ -1671,22 +1755,24 @@ export async function hoursByClientHandler(
|
|||
}
|
||||
|
||||
export const hoursByClient = query({
|
||||
args: { tenantId: v.string(), viewerId: v.id("users"), range: v.optional(v.string()) },
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
viewerId: v.id("users"),
|
||||
range: v.optional(v.string()),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: hoursByClientHandler,
|
||||
})
|
||||
|
||||
// Internal variant used by scheduled jobs: skips viewer scoping and aggregates for the whole tenant
|
||||
export async function hoursByClientInternalHandler(
|
||||
ctx: QueryCtx,
|
||||
{ tenantId, range }: { tenantId: string; range?: string }
|
||||
{ tenantId, range, dateFrom, dateTo }: { tenantId: string; range?: string; dateFrom?: string; dateTo?: string }
|
||||
) {
|
||||
const tickets = await fetchTickets(ctx, tenantId)
|
||||
|
||||
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 { startMs, endMs, days } = resolveRangeWindow(range, dateFrom, dateTo, 90)
|
||||
|
||||
type Acc = {
|
||||
companyId: Id<"companies">
|
||||
|
|
@ -1741,7 +1827,12 @@ export async function hoursByClientInternalHandler(
|
|||
}
|
||||
|
||||
export const hoursByClientInternal = query({
|
||||
args: { tenantId: v.string(), range: v.optional(v.string()) },
|
||||
args: {
|
||||
tenantId: v.string(),
|
||||
range: v.optional(v.string()),
|
||||
dateFrom: v.optional(v.string()),
|
||||
dateTo: v.optional(v.string()),
|
||||
},
|
||||
handler: hoursByClientInternalHandler,
|
||||
})
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue