import dayjs from "dayjs"; import { getCallsCollection } from "../calls.getters"; import { IN_CALL_STATUSES } from "../calls.constants"; import { getClientConfig } from "../../clientsConfig"; import utc from "dayjs/plugin/utc"; import timezonePlugin from "dayjs/plugin/timezone"; import isoWeek from "dayjs/plugin/isoWeek"; import quarterOfYear from "dayjs/plugin/quarterOfYear"; import { DashboardAggregationResult, DashboardDailyTrendMetric, DashboardReportQuery, DashboardReportResponse, DashboardSummaryMetrics, DashboardVolumeGranularity, } from "./calls.dashboard.types"; export type { DashboardVolumeGranularity, DashboardReportQuery, DashboardReportResponse, } from "./calls.dashboard.types"; const DEFAULT_KPI_DATA = { completedCount: 0, inCallCount: 0, answeredDuration: 0, }; dayjs.extend(utc); dayjs.extend(timezonePlugin); dayjs.extend(isoWeek); dayjs.extend(quarterOfYear); const BUCKET_UNIT: Record = { hour: "hour", day: "day", week: "isoWeek", month: "month", quarter: "quarter", year: "year", }; function formatBucketKey( bucketStart: dayjs.Dayjs, granularity: DashboardVolumeGranularity, ): string { switch (granularity) { case "hour": return bucketStart.format("YYYY-MM-DDTHH:00"); case "day": case "week": return bucketStart.format("YYYY-MM-DD"); case "month": return bucketStart.format("YYYY-MM"); case "quarter": return `${bucketStart.format("YYYY")}-Q${bucketStart.quarter()}`; case "year": return bucketStart.format("YYYY"); } } function generateBucketKeys( startDateObj: Date, endDateObj: Date, granularity: DashboardVolumeGranularity, timezone: string, ): string[] { const unitName = BUCKET_UNIT[granularity]; const startOfUnit = unitName as dayjs.OpUnitType; const stepUnit = ( unitName === "isoWeek" ? "week" : unitName ) as dayjs.ManipulateType; const end = dayjs(endDateObj).tz(timezone); let cursor = dayjs(startDateObj).tz(timezone).startOf(startOfUnit); const keys: string[] = []; while (cursor.isBefore(end) || cursor.isSame(end, startOfUnit)) { keys.push(formatBucketKey(cursor, granularity)); cursor = cursor.add(1, stepUnit); } return keys; } function fillVolumeGaps( bucketKeys: string[], volumeDataRaw: { date: string; completed: number }[], ): DashboardDailyTrendMetric[] { const completedByKey = new Map( volumeDataRaw.map((d) => [d.date, d.completed]), ); return bucketKeys.map((date) => ({ date, completed: completedByKey.get(date) ?? 0, })); } function resolveVolumeGranularity( startDateObj: Date, endDateObj: Date, ): DashboardVolumeGranularity { const spanDays = dayjs(endDateObj).diff(dayjs(startDateObj), "day"); if (spanDays <= 2) return "hour"; if (spanDays <= 60) return "day"; if (spanDays <= 365) return "week"; if (spanDays <= 730) return "month"; if (spanDays <= 1460) return "quarter"; return "year"; } const VOLUME_DATE_FORMATS: Partial> = { hour: "%Y-%m-%dT%H:00", day: "%Y-%m-%d", month: "%Y-%m", year: "%Y", }; function buildVolumeBucketIdExpression( timezone: string, granularity: DashboardVolumeGranularity, ) { if (granularity === "week") { return { $dateToString: { format: "%Y-%m-%d", date: { $dateTrunc: { date: "$createdAt", unit: "week", timezone, startOfWeek: "monday", }, }, timezone, }, }; } if (granularity === "quarter") { return { $concat: [ { $toString: { $year: { date: "$createdAt", timezone } } }, "-Q", { $toString: { $add: [ { $trunc: { $divide: [ { $subtract: [ { $month: { date: "$createdAt", timezone } }, 1, ], }, 3, ], }, }, 1, ], }, }, ], }; } return { $dateToString: { format: VOLUME_DATE_FORMATS[granularity], date: "$createdAt", timezone, }, }; } function buildKpisPipeline() { return [ { $group: { _id: null, completedCount: { $sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] }, }, inCallCount: { $sum: { $cond: [{ $in: ["$status", [...IN_CALL_STATUSES]] }, 1, 0], }, }, answeredDuration: { $sum: { $cond: [{ $eq: ["$status", "completed"] }, "$callLength", 0], }, }, }, }, ]; } function buildVolumeDataPipeline( timezone: string, granularity: DashboardVolumeGranularity, ) { return [ { $group: { _id: buildVolumeBucketIdExpression(timezone, granularity), completed: { $sum: { $cond: [{ $eq: ["$status", "completed"] }, 1, 0] }, }, }, }, { $sort: { _id: 1 } }, ]; } export async function getDashboardStats( params: DashboardReportQuery, ): Promise { const { clientId, startDate, endDate, granularity: requestedGranularity, } = params; const clientConfig = await getClientConfig(clientId); const timezone = clientConfig?.timezone ?? "UTC"; const startDateObj = dayjs(startDate).tz(timezone).startOf("day").toDate(); const endDateObj = dayjs(endDate).tz(timezone).endOf("day").toDate(); const granularity = requestedGranularity ?? resolveVolumeGranularity(startDateObj, endDateObj); const pipeline = [ { $match: { clientId, createdAt: { $gte: startDateObj, $lte: endDateObj }, }, }, { $facet: { kpis: buildKpisPipeline(), volumeData: buildVolumeDataPipeline(timezone, granularity), }, }, ]; const callsCollection = getCallsCollection(); const [aggregatedResult] = await callsCollection .aggregate(pipeline) .toArray(); const kpiData = aggregatedResult?.kpis?.[0] ?? DEFAULT_KPI_DATA; const volumeDataRaw = aggregatedResult?.volumeData ?? []; const completedCount = kpiData.completedCount; const inCallCount = kpiData.inCallCount; const totalCalls = completedCount + inCallCount; const answeredDuration = kpiData.answeredDuration ?? 0; const kpis: DashboardSummaryMetrics = { totalCalls, avgDurationSeconds: completedCount > 0 ? Math.round(answeredDuration / completedCount) : 0, timeSavedMinutes: Math.round(answeredDuration / 60), completedCount, inCallCount, }; const bucketKeys = generateBucketKeys( startDateObj, endDateObj, granularity, timezone, ); const volumeData: DashboardDailyTrendMetric[] = fillVolumeGaps( bucketKeys, volumeDataRaw.map((d) => ({ date: d._id, completed: d.completed })), ); const response: DashboardReportResponse = { kpis, charts: { volumeData, volumeGranularity: granularity, }, }; return response; }