import { DataManager } from "../base/DataManager.js"; export class AircallDataManager extends DataManager { constructor(mongodb, redis, timeManager) { const options = { collection: "aircall_daily_data", redisTTL: 300 // 5 minutes cache }; super(mongodb, redis, timeManager, options); this.options = options; } ensureDate(d) { if (d instanceof Date) return d; if (typeof d === 'string') return new Date(d); if (typeof d === 'number') return new Date(d); console.error('Invalid date value:', d); return new Date(); // fallback to current date } async storeHistoricalPeriod(start, end, calls) { if (!this.mongodb) return; try { if (!Array.isArray(calls)) { console.error("Invalid calls data:", calls); return; } // Group calls by true day boundaries using TimeManager const dailyCallsMap = new Map(); calls.forEach((call) => { try { const timestamp = call.started_at * 1000; // Convert to milliseconds const callDate = this.ensureDate(timestamp); const dayBounds = this.timeManager.getDayBounds(callDate); const dayKey = dayBounds.start.toISOString(); if (!dailyCallsMap.has(dayKey)) { dailyCallsMap.set(dayKey, { date: dayBounds.start, calls: [], }); } dailyCallsMap.get(dayKey).calls.push(call); } catch (err) { console.error('Error processing call:', err, call); } }); // Iterate over each day in the period using day boundaries const dates = []; let currentDate = this.ensureDate(start); const endDate = this.ensureDate(end); while (currentDate < endDate) { const dayBounds = this.timeManager.getDayBounds(currentDate); dates.push(dayBounds.start); currentDate.setUTCDate(currentDate.getUTCDate() + 1); } for (const date of dates) { try { const dateKey = date.toISOString(); const dayData = dailyCallsMap.get(dateKey); const dayCalls = dayData ? dayData.calls : []; // Process calls for this day using the same processing logic const metrics = this.processCallData(dayCalls); // Insert a daily_data record for this day metrics.daily_data = [ { date: date.toISOString().split("T")[0], inbound: metrics.by_direction.inbound, outbound: metrics.by_direction.outbound, }, ]; // Store this day's processed data as historical await this.storeHistoricalDay(date, metrics); } catch (err) { console.error('Error processing date:', err, date); } } } catch (error) { console.error("Error storing historical period:", error, error.stack); throw error; } } processCallData(calls) { // If calls is already processed (has total, by_direction, etc.), return it if (calls && calls.total !== undefined) { console.log('Data already processed:', { total: calls.total, by_direction: calls.by_direction }); // Return a clean copy of the processed data return { total: calls.total, by_direction: calls.by_direction, by_status: calls.by_status, by_missed_reason: calls.by_missed_reason, by_hour: calls.by_hour, by_users: calls.by_users, daily_data: calls.daily_data, duration_distribution: calls.duration_distribution, average_duration: calls.average_duration }; } console.log('Processing raw calls:', { count: calls.length, sample: calls.length > 0 ? { id: calls[0].id, direction: calls[0].direction, status: calls[0].status } : null }); // Process raw calls const metrics = { total: calls.length, by_direction: { inbound: 0, outbound: 0 }, by_status: { answered: 0, missed: 0 }, by_missed_reason: {}, by_hour: Array(24).fill(0), by_users: {}, daily_data: [], duration_distribution: [ { range: "0-1m", count: 0 }, { range: "1-5m", count: 0 }, { range: "5-15m", count: 0 }, { range: "15-30m", count: 0 }, { range: "30m+", count: 0 }, ], average_duration: 0, total_duration: 0, }; // Group calls by date for daily data const dailyCallsMap = new Map(); calls.forEach((call) => { try { // Direction metrics metrics.by_direction[call.direction]++; // Get call date and hour using TimeManager const timestamp = call.started_at * 1000; // Convert to milliseconds const callDate = this.ensureDate(timestamp); const dayBounds = this.timeManager.getDayBounds(callDate); const dayKey = dayBounds.start.toISOString().split("T")[0]; const hour = callDate.getHours(); metrics.by_hour[hour]++; // Status and duration metrics if (call.answered_at) { metrics.by_status.answered++; const duration = call.ended_at - call.answered_at; metrics.total_duration += duration; // Duration distribution if (duration <= 60) { metrics.duration_distribution[0].count++; } else if (duration <= 300) { metrics.duration_distribution[1].count++; } else if (duration <= 900) { metrics.duration_distribution[2].count++; } else if (duration <= 1800) { metrics.duration_distribution[3].count++; } else { metrics.duration_distribution[4].count++; } // Track user performance if (call.user) { const userId = call.user.id; if (!metrics.by_users[userId]) { metrics.by_users[userId] = { id: userId, name: call.user.name, total: 0, answered: 0, missed: 0, total_duration: 0, average_duration: 0, }; } metrics.by_users[userId].total++; metrics.by_users[userId].answered++; metrics.by_users[userId].total_duration += duration; } } else { metrics.by_status.missed++; if (call.missed_call_reason) { metrics.by_missed_reason[call.missed_call_reason] = (metrics.by_missed_reason[call.missed_call_reason] || 0) + 1; } // Track missed calls by user if (call.user) { const userId = call.user.id; if (!metrics.by_users[userId]) { metrics.by_users[userId] = { id: userId, name: call.user.name, total: 0, answered: 0, missed: 0, total_duration: 0, average_duration: 0, }; } metrics.by_users[userId].total++; metrics.by_users[userId].missed++; } } // Group by date for daily data if (!dailyCallsMap.has(dayKey)) { dailyCallsMap.set(dayKey, { date: dayKey, inbound: 0, outbound: 0 }); } dailyCallsMap.get(dayKey)[call.direction]++; } catch (err) { console.error('Error processing call:', err, call); } }); // Calculate average durations for users Object.values(metrics.by_users).forEach((user) => { if (user.answered > 0) { user.average_duration = Math.round(user.total_duration / user.answered); } }); // Calculate global average duration if (metrics.by_status.answered > 0) { metrics.average_duration = Math.round( metrics.total_duration / metrics.by_status.answered ); } // Convert daily data map to sorted array metrics.daily_data = Array.from(dailyCallsMap.values()).sort((a, b) => a.date.localeCompare(b.date) ); delete metrics.total_duration; console.log('Processed metrics:', { total: metrics.total, by_direction: metrics.by_direction, by_status: metrics.by_status, daily_data_count: metrics.daily_data.length }); return metrics; } async storeHistoricalDay(date, data) { if (!this.mongodb) return; try { const collection = this.mongodb.collection(this.options.collection); const dayBounds = this.timeManager.getDayBounds(this.ensureDate(date)); // Ensure consistent data structure with metrics nested in data field const document = { date: dayBounds.start, data: { total: data.total, by_direction: data.by_direction, by_status: data.by_status, by_missed_reason: data.by_missed_reason, by_hour: data.by_hour, by_users: data.by_users, daily_data: data.daily_data, duration_distribution: data.duration_distribution, average_duration: data.average_duration }, updatedAt: new Date() }; await collection.updateOne( { date: dayBounds.start }, { $set: document }, { upsert: true } ); } catch (error) { console.error("Error storing historical day:", error); throw error; } } }