298 lines
9.2 KiB
JavaScript
298 lines
9.2 KiB
JavaScript
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;
|
|
}
|
|
}
|
|
} |