733 lines
25 KiB
JavaScript
733 lines
25 KiB
JavaScript
// ENVIRONMENT
|
|
require('dotenv').config();
|
|
|
|
// DATABASE
|
|
const { PrismaClient: CMSClient } = require("../../prisma/clients/cms");
|
|
|
|
const prisma = new CMSClient();
|
|
|
|
// SERVICES
|
|
const { sendNotification } = require("../services/firebase.services.js");
|
|
const logger = require("../services/logger.services");
|
|
const { localTime } = require("../services/time.services.js");
|
|
const fileServices = require("../services/file.services.js");
|
|
const { default: prefixes } = require("../static/prefix.js");
|
|
|
|
// CONSTANTS
|
|
const { badRequestResponse, successResponse, notFoundResponse } = require("../res/responses.js");
|
|
|
|
// HELPER FUNCTIONS
|
|
const deleteCampaignImage = async (imageUrl) => {
|
|
try {
|
|
if (!imageUrl) return;
|
|
const urlParts = imageUrl.split('/');
|
|
const fileName = urlParts[urlParts.length - 1];
|
|
|
|
await fileServices.delete(prefixes.bucketName, "notifications", fileName);
|
|
logger.info(`Deleted campaign image: ${fileName}`);
|
|
} catch (err) {
|
|
logger.error(`Error deleting campaign image: ${err}`);
|
|
}
|
|
};
|
|
|
|
// CONTROLLER
|
|
exports.sendNotification = async (req, res) => {
|
|
try {
|
|
const { userID, title, body, data = {} } = req.body;
|
|
const imageFile = req.files?.find(file => file.fieldname === 'image');
|
|
let imageUrl = null;
|
|
if (imageFile) {
|
|
const [uploadedUrl] = await fileServices.upload(
|
|
prefixes.bucketName,
|
|
"notifications",
|
|
imageFile.mimetype,
|
|
[imageFile]
|
|
);
|
|
imageUrl = uploadedUrl;
|
|
}
|
|
|
|
const userToken = await prisma.usersToken.findFirst({
|
|
where: {
|
|
UserID_UT: userID
|
|
}
|
|
});
|
|
|
|
let parsedData = data;
|
|
if (typeof data === 'string') {
|
|
try {
|
|
parsedData = JSON.parse(data);
|
|
} catch (e) {
|
|
parsedData = {};
|
|
}
|
|
}
|
|
|
|
await sendNotification(userToken.Token_UT, title, body, parsedData, imageUrl);
|
|
|
|
return successResponse(res, "Notification sent successfully!", null);
|
|
|
|
} catch (err) {
|
|
return badRequestResponse(res, "Error sending notification", err);
|
|
}
|
|
}
|
|
|
|
exports.setupCampaign = async (req, res) => {
|
|
try {
|
|
const { title, content, date, data = null } = req.body;
|
|
const imageFile = req.files?.find(file => file.fieldname === 'image');
|
|
|
|
let imageUrl = null;
|
|
let fileName = null;
|
|
if (imageFile) {
|
|
const [uploadedUrl, uploadedFileName] = await fileServices.upload(
|
|
prefixes.bucketName,
|
|
"notifications",
|
|
imageFile.mimetype,
|
|
[imageFile]
|
|
);
|
|
imageUrl = uploadedUrl;
|
|
fileName = uploadedFileName;
|
|
}
|
|
|
|
let parsedData = data;
|
|
if (typeof data === 'string') {
|
|
try {
|
|
parsedData = JSON.parse(data);
|
|
} catch (e) {
|
|
parsedData = null;
|
|
}
|
|
}
|
|
|
|
await prisma.appCampaign.create({
|
|
data: {
|
|
Title_ACP: title,
|
|
Content_ACP: content,
|
|
Date_ACP: date,
|
|
ImageUrl_ACP: imageUrl,
|
|
Data_ACP: parsedData ? JSON.stringify(parsedData) : null,
|
|
CreatedAt_ACP: localTime(new Date()),
|
|
UpdatedAt_ACP: localTime(new Date()),
|
|
}
|
|
});
|
|
|
|
return successResponse(res, "Campaign setup successfully!", null);
|
|
|
|
} catch (err) {
|
|
console.log(err);
|
|
return badRequestResponse(res, "Error setting up campaign", err);
|
|
}
|
|
}
|
|
|
|
exports.getAllCampaigns = async (req, res) => {
|
|
try {
|
|
const { status } = req.query;
|
|
|
|
const where = {};
|
|
if (status) {
|
|
const statuses = Array.isArray(status)
|
|
? status
|
|
: String(status)
|
|
.split(",")
|
|
.map((s) => s.trim())
|
|
.filter(Boolean);
|
|
|
|
where.Status_ACP = statuses.length > 1 ? { in: statuses } : statuses[0];
|
|
}
|
|
|
|
const campaigns = await prisma.appCampaign.findMany({
|
|
where,
|
|
orderBy: {
|
|
Date_ACP: "desc",
|
|
},
|
|
});
|
|
|
|
return successResponse(res, "Campaigns retrieved successfully!", campaigns);
|
|
} catch (err) {
|
|
return badRequestResponse(res, "Error retrieving campaigns", err);
|
|
}
|
|
}
|
|
|
|
exports.updateCampaign = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
const { title, content, date, status, data } = req.body;
|
|
const imageFile = req.files?.find(file => file.fieldname === 'image');
|
|
|
|
const exists = await prisma.appCampaign.findFirst({
|
|
where: { UUID_ACP: id }
|
|
});
|
|
|
|
if (!exists) {
|
|
return notFoundResponse(res, "Campaign tidak ditemukan", null);
|
|
}
|
|
|
|
const dataToUpdate = {};
|
|
if (title !== undefined) dataToUpdate.Title_ACP = title;
|
|
if (content !== undefined) dataToUpdate.Content_ACP = content;
|
|
if (date !== undefined) dataToUpdate.Date_ACP = date;
|
|
if (status !== undefined) dataToUpdate.Status_ACP = status;
|
|
|
|
if (imageFile) {
|
|
if (exists.ImageUrl_ACP) {
|
|
await deleteCampaignImage(exists.ImageUrl_ACP);
|
|
}
|
|
const [uploadedUrl] = await fileServices.upload(
|
|
prefixes.bucketName,
|
|
"notifications",
|
|
imageFile.mimetype,
|
|
[imageFile]
|
|
);
|
|
dataToUpdate.ImageUrl_ACP = uploadedUrl;
|
|
}
|
|
|
|
if (data !== undefined) {
|
|
let parsedData = data;
|
|
if (typeof data === 'string') {
|
|
try {
|
|
parsedData = JSON.parse(data);
|
|
} catch (e) {
|
|
parsedData = null;
|
|
}
|
|
}
|
|
dataToUpdate.Data_ACP = parsedData ? JSON.stringify(parsedData) : null;
|
|
}
|
|
|
|
dataToUpdate.UpdatedAt_ACP = localTime(new Date());
|
|
|
|
if (Object.keys(dataToUpdate).length === 1) {
|
|
return badRequestResponse(res, "Tidak ada field yang diupdate", null);
|
|
}
|
|
|
|
const updated = await prisma.appCampaign.update({
|
|
where: { UUID_ACP: id },
|
|
data: dataToUpdate
|
|
});
|
|
|
|
logger.info(`Campaign ${id} diupdate.`);
|
|
return successResponse(res, "Campaign berhasil diupdate!", updated);
|
|
} catch (err) {
|
|
return badRequestResponse(res, "Terjadi kesalahan saat update campaign", err);
|
|
}
|
|
}
|
|
|
|
exports.deleteCampaign = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const campaign = await prisma.appCampaign.findFirst({
|
|
where: { UUID_ACP: id }
|
|
});
|
|
|
|
if (!campaign) {
|
|
return notFoundResponse(res, "Campaign not found", null);
|
|
}
|
|
|
|
if (campaign.ImageUrl_ACP) {
|
|
await deleteCampaignImage(campaign.ImageUrl_ACP);
|
|
}
|
|
|
|
await prisma.appCampaign.update({
|
|
where: { UUID_ACP: id },
|
|
data: { Status_ACP: "cancelled", UpdatedAt_ACP: localTime(new Date()) }
|
|
});
|
|
return successResponse(res, "Campaign cancelled successfully!", null);
|
|
} catch (err) {
|
|
return badRequestResponse(res, "Error cancelling campaign", err);
|
|
}
|
|
}
|
|
|
|
exports.sendCampaign = async (campaignID) => {
|
|
try {
|
|
const campaign = await prisma.appCampaign.findFirst({
|
|
where: {
|
|
UUID_ACP: campaignID,
|
|
Status_ACP: "pending"
|
|
}
|
|
});
|
|
|
|
if (!campaign) {
|
|
logger.info(`No pending campaign found for id ${campaignID}`);
|
|
return null;
|
|
}
|
|
|
|
const { Title_ACP, Content_ACP, ImageUrl_ACP, Data_ACP } = campaign;
|
|
|
|
let customData = { campaignId: campaignID };
|
|
if (Data_ACP) {
|
|
try {
|
|
const parsedData = JSON.parse(Data_ACP);
|
|
customData = { ...customData, ...parsedData };
|
|
} catch (e) {
|
|
logger.error(`Failed to parse Data_ACP for campaign ${campaignID}: ${e}`);
|
|
}
|
|
}
|
|
|
|
const result = await require("../services/firebase.services").sendCampaignWithTracking(
|
|
campaignID,
|
|
Title_ACP,
|
|
Content_ACP,
|
|
customData,
|
|
ImageUrl_ACP
|
|
);
|
|
|
|
if (result.success) {
|
|
await prisma.appCampaign.update({
|
|
where: {
|
|
UUID_ACP: campaignID
|
|
},
|
|
data: {
|
|
Status_ACP: "completed",
|
|
TargetUsers_ACP: result.targetUsers,
|
|
SentCount_ACP: result.targetUsers,
|
|
SuccessCount_ACP: result.successCount,
|
|
FailureCount_ACP: result.failureCount,
|
|
DeliveryRate_ACP: parseFloat(result.deliveryRate),
|
|
SentAt_ACP: result.sentAt,
|
|
CompletedAt_ACP: localTime(new Date()),
|
|
UpdatedAt_ACP: localTime(new Date())
|
|
}
|
|
});
|
|
|
|
logger.info(`Campaign ${campaignID} sent successfully! Delivered: ${result.successCount}/${result.targetUsers}`);
|
|
} else {
|
|
throw new Error(result.message || "Failed to send campaign");
|
|
}
|
|
|
|
} catch (err) {
|
|
const failedCampaign = await prisma.appCampaign.findFirst({
|
|
where: { UUID_ACP: campaignID }
|
|
});
|
|
|
|
await prisma.appCampaign.update({
|
|
where: {
|
|
UUID_ACP: campaignID
|
|
},
|
|
data: {
|
|
Status_ACP: "failed",
|
|
ErrorMessage_ACP: err.message || err.toString()
|
|
}
|
|
});
|
|
|
|
if (failedCampaign?.ImageUrl_ACP) {
|
|
await deleteCampaignImage(failedCampaign.ImageUrl_ACP);
|
|
}
|
|
logger.error(`Error sending campaign ${campaignID}: ${err}`);
|
|
}
|
|
}
|
|
|
|
exports.checkCampaign = async (datetime) => {
|
|
try {
|
|
const target = new Date(datetime);
|
|
const startOfMinute = new Date(target);
|
|
startOfMinute.setSeconds(0, 0);
|
|
const endOfMinute = new Date(startOfMinute);
|
|
endOfMinute.setMinutes(endOfMinute.getMinutes() + 1);
|
|
|
|
const campaign = await prisma.appCampaign.findFirst({
|
|
where: {
|
|
Date_ACP: {
|
|
gte: startOfMinute,
|
|
lt: endOfMinute
|
|
}
|
|
}
|
|
});
|
|
|
|
return campaign ? campaign.UUID_ACP : null;
|
|
|
|
} catch (err) {
|
|
return null;
|
|
}
|
|
}
|
|
|
|
exports.getCampaignAnalytics = async (req, res) => {
|
|
try {
|
|
const now = new Date();
|
|
const last30Days = new Date(now.getTime() - 30 * 24 * 60 * 60 * 1000);
|
|
const last7Days = new Date(now.getTime() - 7 * 24 * 60 * 60 * 1000);
|
|
|
|
const [
|
|
totalCampaigns,
|
|
statusDistribution,
|
|
allCampaigns,
|
|
recentCampaigns,
|
|
upcomingCampaigns,
|
|
totalDeliveries,
|
|
deliveryStatusDistribution
|
|
] = await Promise.all([
|
|
prisma.appCampaign.count(),
|
|
prisma.appCampaign.groupBy({
|
|
by: ['Status_ACP'],
|
|
_count: {
|
|
Status_ACP: true
|
|
}
|
|
}),
|
|
prisma.appCampaign.findMany({
|
|
select: {
|
|
UUID_ACP: true,
|
|
Title_ACP: true,
|
|
Status_ACP: true,
|
|
Date_ACP: true,
|
|
CreatedAt_ACP: true,
|
|
UpdatedAt_ACP: true,
|
|
TargetUsers_ACP: true,
|
|
SentCount_ACP: true,
|
|
SuccessCount_ACP: true,
|
|
FailureCount_ACP: true,
|
|
DeliveryRate_ACP: true
|
|
},
|
|
orderBy: {
|
|
Date_ACP: 'desc'
|
|
}
|
|
}),
|
|
|
|
prisma.appCampaign.findMany({
|
|
where: {
|
|
Date_ACP: {
|
|
gte: last30Days,
|
|
lte: now
|
|
}
|
|
},
|
|
select: {
|
|
Date_ACP: true,
|
|
Status_ACP: true,
|
|
Title_ACP: true,
|
|
SuccessCount_ACP: true,
|
|
FailureCount_ACP: true,
|
|
TargetUsers_ACP: true
|
|
},
|
|
orderBy: {
|
|
Date_ACP: 'asc'
|
|
}
|
|
}),
|
|
|
|
prisma.appCampaign.findMany({
|
|
where: {
|
|
Date_ACP: {
|
|
gte: now
|
|
},
|
|
Status_ACP: 'pending'
|
|
},
|
|
select: {
|
|
UUID_ACP: true,
|
|
Title_ACP: true,
|
|
Date_ACP: true
|
|
},
|
|
orderBy: {
|
|
Date_ACP: 'asc'
|
|
},
|
|
take: 10
|
|
}),
|
|
|
|
prisma.campaignDelivery.count(),
|
|
|
|
prisma.campaignDelivery.groupBy({
|
|
by: ['Status_CD'],
|
|
_count: {
|
|
Status_CD: true
|
|
}
|
|
})
|
|
]);
|
|
|
|
const statusStats = statusDistribution.reduce((acc, item) => {
|
|
acc[item.Status_ACP] = item._count.Status_ACP;
|
|
return acc;
|
|
}, {});
|
|
|
|
|
|
const completedCount = statusStats.completed || 0;
|
|
const failedCount = statusStats.failed || 0;
|
|
const totalExecuted = completedCount + failedCount;
|
|
const successRate = totalExecuted > 0
|
|
? ((completedCount / totalExecuted) * 100).toFixed(2)
|
|
: 0;
|
|
|
|
const campaignTimeline = {};
|
|
recentCampaigns.forEach(campaign => {
|
|
const date = new Date(campaign.Date_ACP).toISOString().split('T')[0];
|
|
if (!campaignTimeline[date]) {
|
|
campaignTimeline[date] = {
|
|
total: 0,
|
|
completed: 0,
|
|
failed: 0,
|
|
pending: 0,
|
|
cancelled: 0
|
|
};
|
|
}
|
|
campaignTimeline[date].total++;
|
|
campaignTimeline[date][campaign.Status_ACP]++;
|
|
});
|
|
|
|
const creationTrend = {};
|
|
allCampaigns.forEach(campaign => {
|
|
const date = new Date(campaign.CreatedAt_ACP).toISOString().split('T')[0];
|
|
if (!creationTrend[date]) {
|
|
creationTrend[date] = 0;
|
|
}
|
|
creationTrend[date]++;
|
|
});
|
|
|
|
const executedCampaigns = allCampaigns.filter(c =>
|
|
c.Status_ACP === 'completed' || c.Status_ACP === 'failed'
|
|
);
|
|
|
|
let avgResponseTime = 0;
|
|
if (executedCampaigns.length > 0) {
|
|
const totalTime = executedCampaigns.reduce((sum, campaign) => {
|
|
const responseTime = new Date(campaign.UpdatedAt_ACP) - new Date(campaign.Date_ACP);
|
|
return sum + responseTime;
|
|
}, 0);
|
|
avgResponseTime = Math.round(totalTime / executedCampaigns.length / 1000 / 60); // in minutes
|
|
}
|
|
|
|
const statusOverTime = {};
|
|
const last7DaysCampaigns = allCampaigns.filter(c =>
|
|
new Date(c.Date_ACP) >= last7Days
|
|
);
|
|
|
|
last7DaysCampaigns.forEach(campaign => {
|
|
const date = new Date(campaign.Date_ACP).toISOString().split('T')[0];
|
|
if (!statusOverTime[date]) {
|
|
statusOverTime[date] = {
|
|
completed: 0,
|
|
failed: 0,
|
|
pending: 0,
|
|
cancelled: 0
|
|
};
|
|
}
|
|
statusOverTime[date][campaign.Status_ACP]++;
|
|
});
|
|
|
|
const totalTargetUsers = allCampaigns.reduce((sum, c) => sum + (c.TargetUsers_ACP || 0), 0);
|
|
const totalSent = allCampaigns.reduce((sum, c) => sum + (c.SentCount_ACP || 0), 0);
|
|
const totalSuccess = allCampaigns.reduce((sum, c) => sum + (c.SuccessCount_ACP || 0), 0);
|
|
const totalFailure = allCampaigns.reduce((sum, c) => sum + (c.FailureCount_ACP || 0), 0);
|
|
const overallDeliveryRate = totalSent > 0 ? ((totalSuccess / totalSent) * 100).toFixed(2) : 0;
|
|
|
|
const deliveryStats = deliveryStatusDistribution.reduce((acc, item) => {
|
|
acc[item.Status_CD] = item._count.Status_CD;
|
|
return acc;
|
|
}, {});
|
|
|
|
const deliveryRateTrend = {};
|
|
recentCampaigns.forEach(campaign => {
|
|
if (campaign.TargetUsers_ACP > 0) {
|
|
const date = new Date(campaign.Date_ACP).toISOString().split('T')[0];
|
|
const rate = campaign.SuccessCount_ACP && campaign.TargetUsers_ACP
|
|
? ((campaign.SuccessCount_ACP / campaign.TargetUsers_ACP) * 100).toFixed(2)
|
|
: 0;
|
|
deliveryRateTrend[date] = rate;
|
|
}
|
|
});
|
|
|
|
const campaignPerformance = allCampaigns
|
|
.filter(c => c.Status_ACP === 'completed' && c.TargetUsers_ACP > 0)
|
|
.map(c => ({
|
|
id: c.UUID_ACP,
|
|
title: c.Title_ACP,
|
|
targetUsers: c.TargetUsers_ACP,
|
|
successCount: c.SuccessCount_ACP,
|
|
failureCount: c.FailureCount_ACP,
|
|
deliveryRate: c.DeliveryRate_ACP,
|
|
date: c.Date_ACP
|
|
}))
|
|
.sort((a, b) => b.deliveryRate - a.deliveryRate)
|
|
.slice(0, 10);
|
|
|
|
return successResponse(res, "Campaign analytics retrieved successfully!", {
|
|
summary: {
|
|
campaigns: {
|
|
total: totalCampaigns,
|
|
completed: statusStats.completed || 0,
|
|
failed: statusStats.failed || 0,
|
|
pending: statusStats.pending || 0,
|
|
cancelled: statusStats.cancelled || 0,
|
|
successRate: `${successRate}%`,
|
|
avgResponseTime: `${avgResponseTime} minutes`,
|
|
upcomingCount: upcomingCampaigns.length
|
|
},
|
|
delivery: {
|
|
totalTargetUsers: totalTargetUsers,
|
|
totalSent: totalSent,
|
|
totalDelivered: totalSuccess,
|
|
totalFailed: totalFailure,
|
|
overallDeliveryRate: `${overallDeliveryRate}%`,
|
|
totalDeliveryRecords: totalDeliveries
|
|
}
|
|
},
|
|
|
|
charts: {
|
|
statusDistribution: {
|
|
labels: Object.keys(statusStats),
|
|
data: Object.values(statusStats)
|
|
},
|
|
|
|
deliveryStatusDistribution: {
|
|
labels: Object.keys(deliveryStats),
|
|
data: Object.values(deliveryStats)
|
|
},
|
|
|
|
campaignTimeline: campaignTimeline,
|
|
|
|
creationTrend: {
|
|
labels: Object.keys(creationTrend).sort(),
|
|
data: Object.keys(creationTrend).sort().map(date => creationTrend[date])
|
|
},
|
|
|
|
statusOverTime: statusOverTime,
|
|
|
|
deliveryRateTrend: {
|
|
labels: Object.keys(deliveryRateTrend).sort(),
|
|
data: Object.keys(deliveryRateTrend).sort().map(date => parseFloat(deliveryRateTrend[date]))
|
|
}
|
|
},
|
|
|
|
topPerforming: campaignPerformance,
|
|
|
|
upcoming: upcomingCampaigns,
|
|
|
|
recentActivity: recentCampaigns.slice(0, 10)
|
|
});
|
|
|
|
} catch (err) {
|
|
return badRequestResponse(res, "Error retrieving campaign analytics", err);
|
|
}
|
|
}
|
|
|
|
exports.getCampaignReport = async (req, res) => {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
const [campaign, deliveryRecords, deliveryStatusBreakdown] = await Promise.all([
|
|
prisma.appCampaign.findFirst({
|
|
where: { UUID_ACP: id }
|
|
}),
|
|
prisma.campaignDelivery.findMany({
|
|
where: { Campaign_CD: id },
|
|
select: {
|
|
UUID_CD: true,
|
|
UserID_CD: true,
|
|
Status_CD: true,
|
|
SentAt_CD: true,
|
|
DeliveredAt_CD: true,
|
|
FailedAt_CD: true,
|
|
ErrorMessage_CD: true,
|
|
CreatedAt_CD: true
|
|
},
|
|
orderBy: {
|
|
CreatedAt_CD: 'desc'
|
|
}
|
|
}),
|
|
prisma.campaignDelivery.groupBy({
|
|
by: ['Status_CD'],
|
|
where: { Campaign_CD: id },
|
|
_count: {
|
|
Status_CD: true
|
|
}
|
|
})
|
|
]);
|
|
|
|
if (!campaign) {
|
|
return notFoundResponse(res, "Campaign not found", null);
|
|
}
|
|
|
|
const createdAt = new Date(campaign.CreatedAt_ACP);
|
|
const scheduledAt = new Date(campaign.Date_ACP);
|
|
const updatedAt = new Date(campaign.UpdatedAt_ACP);
|
|
const now = new Date();
|
|
|
|
const leadTime = Math.round((scheduledAt - createdAt) / 1000 / 60 / 60); // hours
|
|
const executionTime = campaign.Status_ACP !== 'pending'
|
|
? Math.round((updatedAt - scheduledAt) / 1000 / 60) // minutes
|
|
: null;
|
|
|
|
const isScheduled = scheduledAt > now;
|
|
const isOverdue = scheduledAt < now && campaign.Status_ACP === 'pending';
|
|
const timeUntilExecution = isScheduled
|
|
? Math.round((scheduledAt - now) / 1000 / 60 / 60) // hours
|
|
: null;
|
|
|
|
const deliveryStats = deliveryStatusBreakdown.reduce((acc, item) => {
|
|
acc[item.Status_CD] = item._count.Status_CD;
|
|
return acc;
|
|
}, {});
|
|
|
|
const failedDeliveries = deliveryRecords.filter(d => d.Status_CD === 'failed');
|
|
const errorBreakdown = {};
|
|
failedDeliveries.forEach(delivery => {
|
|
const errorKey = delivery.ErrorMessage_CD || 'Unknown error';
|
|
if (!errorBreakdown[errorKey]) {
|
|
errorBreakdown[errorKey] = 0;
|
|
}
|
|
errorBreakdown[errorKey]++;
|
|
});
|
|
|
|
const deliveredRecords = deliveryRecords.filter(d => d.DeliveredAt_CD);
|
|
let avgDeliveryTime = 0;
|
|
if (deliveredRecords.length > 0 && campaign.SentAt_ACP) {
|
|
const sentTime = new Date(campaign.SentAt_ACP);
|
|
const totalDeliveryTime = deliveredRecords.reduce((sum, record) => {
|
|
const deliveryTime = new Date(record.DeliveredAt_CD) - sentTime;
|
|
return sum + deliveryTime;
|
|
}, 0);
|
|
avgDeliveryTime = Math.round(totalDeliveryTime / deliveredRecords.length / 1000); // in seconds
|
|
}
|
|
|
|
return successResponse(res, "Campaign report retrieved successfully!", {
|
|
campaign: {
|
|
id: campaign.UUID_ACP,
|
|
title: campaign.Title_ACP,
|
|
content: campaign.Content_ACP,
|
|
status: campaign.Status_ACP,
|
|
scheduledDate: campaign.Date_ACP,
|
|
createdAt: campaign.CreatedAt_ACP,
|
|
updatedAt: campaign.UpdatedAt_ACP,
|
|
errorMessage: campaign.ErrorMessage_ACP
|
|
},
|
|
|
|
metrics: {
|
|
leadTime: `${leadTime} hours`,
|
|
executionTime: executionTime ? `${executionTime} minutes` : 'Not executed yet',
|
|
isScheduled: isScheduled,
|
|
isOverdue: isOverdue,
|
|
timeUntilExecution: timeUntilExecution ? `${timeUntilExecution} hours` : null,
|
|
status: campaign.Status_ACP,
|
|
avgDeliveryTime: avgDeliveryTime ? `${avgDeliveryTime} seconds` : 'N/A'
|
|
},
|
|
|
|
delivery: {
|
|
targetUsers: campaign.TargetUsers_ACP || 0,
|
|
sentCount: campaign.SentCount_ACP || 0,
|
|
successCount: campaign.SuccessCount_ACP || 0,
|
|
failureCount: campaign.FailureCount_ACP || 0,
|
|
deliveryRate: campaign.DeliveryRate_ACP ? `${campaign.DeliveryRate_ACP}%` : '0%',
|
|
sentAt: campaign.SentAt_ACP,
|
|
completedAt: campaign.CompletedAt_ACP,
|
|
|
|
statusBreakdown: {
|
|
pending: deliveryStats.pending || 0,
|
|
sent: deliveryStats.sent || 0,
|
|
delivered: deliveryStats.delivered || 0,
|
|
failed: deliveryStats.failed || 0
|
|
},
|
|
|
|
errorBreakdown: errorBreakdown
|
|
},
|
|
|
|
timeline: {
|
|
created: createdAt.toISOString(),
|
|
scheduled: scheduledAt.toISOString(),
|
|
sent: campaign.SentAt_ACP ? new Date(campaign.SentAt_ACP).toISOString() : null,
|
|
completed: campaign.CompletedAt_ACP ? new Date(campaign.CompletedAt_ACP).toISOString() : null,
|
|
executed: campaign.Status_ACP !== 'pending' ? updatedAt.toISOString() : null
|
|
},
|
|
|
|
deliveryRecords: {
|
|
total: deliveryRecords.length,
|
|
records: deliveryRecords.slice(0, 100)
|
|
}
|
|
});
|
|
|
|
} catch (err) {
|
|
return badRequestResponse(res, "Error retrieving campaign report", err);
|
|
}
|
|
}
|