// ENVIRONMENT require('dotenv').config(); // DATABASE const { PrismaClient : CMSClient, ContentType, CorpType } = require("../../prisma/clients/cms"); const prisma = new CMSClient(); // CONSTANTS const { badRequestResponse, successResponse, notFoundResponse } = require("../res/responses.js"); // SERVICES const fileServices = require('../services/file.services.js'); const { getAllBuckets } = require('../services/minio.services.js'); const logger = require('../services/logger.services.js'); const { localTime } = require("../services/time.services.js"); const { default: prefixes } = require('../static/prefix.js'); exports.createContent = async (req, res) => { try { const { title, description, type, corp } = req.body; if (!Object.values(CorpType).includes(corp)) { return badRequestResponse(res, "Invalid corp value", `Allowed values: ${Object.values(CorpType).join(", ")}`); } const imageFile = req.files; if (!imageFile) { return badRequestResponse(res, "Image file is required", "Missing image file in request"); } if (!title || !description || !type || !corp) { return badRequestResponse(res, "Title, description, type, and corp are required", null); } if (!Object.values(ContentType).includes(type)) { return badRequestResponse(res, "Invalid type value", `Allowed values: ${Object.values(ContentType).join(", ")}`); } const files = await fileServices.upload(prefixes.bucketName, type, (imageFile[0]).mimetype, imageFile); await prisma.appContent.create({ data: { Title_APC: title, Content_APC: description, Type_APC: type, CorpType_APC: corp, Url_APC: files[0], TargetUrl_APC: req.body.targetUrl || null, Filename_APC: files[1], CreatedAt_APC: localTime(new Date()), UpdatedAt_APC: localTime(new Date()) } }); return successResponse(res, `${type} item created successfully`, null); } catch (err) { logger.info(err); return badRequestResponse(res, "Internal Server Error", err); } }; exports.getContents = async (req, res) => { try { const { type, corp } = req.params; if (!Object.values(ContentType).includes(type)) { return badRequestResponse(res, "Invalid type value", `Allowed values: ${Object.values(ContentType).join(", ")}`); } if (corp != "all" && !Object.values(CorpType).includes(corp)) { return badRequestResponse(res, "Invalid corp value", `Allowed values: ${Object.values(CorpType).join(", ")}`); } var data = []; if (corp == "all") { data = await prisma.appContent.findMany({ where: { Type_APC: type, }, orderBy: { CreatedAt_APC: 'desc' } }); } else { data = await prisma.appContent.findMany({ where: { Type_APC: type, CorpType_APC: corp, }, orderBy: { CreatedAt_APC: 'desc' } }); } return successResponse(res, `${type} with corp ${corp} items retrieved successfully`, data); } catch (err) { logger.info(err); return badRequestResponse(res, "Internal Server Error", err); } }; exports.updateContent = async (req, res) => { try { const { id } = req.params; const { title, description, type, corp } = req.body; const imageFile = req.files; if (!imageFile) { return badRequestResponse(res, "Image file is required", "Missing image file in request"); } if (!title || !description || !type || !corp) { return badRequestResponse(res, "Title, description, type, and corp are required", null); } if (!Object.values(ContentType).includes(type)) { return badRequestResponse(res, "Invalid type value", `Allowed values: ${Object.values(ContentType).join(", ")}`); } if (corp != "all" && !Object.values(CorpType).includes(corp)) { return badRequestResponse(res, "Invalid corp value", `Allowed values: ${Object.values(CorpType).join(", ")}`); } const existingItem = await prisma.appContent.findUnique({ where: { UUID_APC: id } }); await fileServices.delete(prefixes.bucketName, existingItem.Type_APC, existingItem.Filename_APC); const files = await fileServices.upload(prefixes.bucketName, type, (imageFile[0]).mimetype, imageFile); await prisma.appContent.update({ where: { UUID_APC: id }, data: { Title_APC: title || existingItem.Title_APC, Content_APC: description || existingItem.Content_APC, Type_APC: type || existingItem.Type_APC, CorpType_APC: corp || existingItem.CorpType_APC, Url_APC: files[0] || existingItem.Url_APC, TargetUrl_APC: req.body.targetUrl || existingItem.TargetUrl_APC, Filename_APC: files[1] || existingItem.Filename_APC, UpdatedAt_APC: localTime(new Date()) } }); return successResponse(res, `${type} with corp ${corp} item updated successfully`, null); } catch (err) { logger.info(err); return badRequestResponse(res, "Internal Server Error", err); } }; exports.deleteContent = async (req, res) => { try { const { id } = req.params; const existingItem = await prisma.appContent.findFirst({ where: { UUID_APC: id } }); if (!existingItem) { return badRequestResponse(res, `${existingItem.Type_APC} item not found`, "Invalid ID"); } await fileServices.delete(prefixes.bucketName, existingItem.Type_APC, existingItem.Filename_APC); await prisma.appContent.delete({ where: { UUID_APC: id } }); return successResponse(res, `${existingItem.Type_APC} item deleted successfully`, null); } catch (err) { return badRequestResponse(res, "Internal Server Error", err); } }; exports.getStats = 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 [ splashCount, promoCount, articleCount, bannerCount, floatingWidgetCount, apiKeysCount, usersCount, buckets, allContents, campaignStats, userActivities, recentContents, recentUsers ] = await Promise.all([ prisma.appContent.count({ where: { Type_APC: ContentType.splash } }), prisma.appContent.count({ where: { Type_APC: ContentType.promo } }), prisma.appContent.count({ where: { Type_APC: ContentType.article } }), prisma.appContent.count({ where: { Type_APC: ContentType.banner } }), prisma.appContent.count({ where: { Type_APC: ContentType.floatingWidget } }), prisma.appCredential.count(), prisma.usersToken.count(), getAllBuckets(), prisma.appContent.findMany({ select: { Type_APC: true, CorpType_APC: true, CreatedAt_APC: true, } }), prisma.appCampaign.groupBy({ by: ['Status_ACP'], _count: { Status_ACP: true } }), prisma.usersActivity.groupBy({ by: ['ActivityType_UA'], _count: { ActivityType_UA: true } }), prisma.appContent.findMany({ where: { CreatedAt_APC: { gte: last30Days } }, select: { CreatedAt_APC: true, Type_APC: true, }, orderBy: { CreatedAt_APC: 'asc' } }), prisma.usersToken.findMany({ where: { CreatedAt_UT: { gte: last7Days } }, select: { CreatedAt_UT: true }, orderBy: { CreatedAt_UT: 'asc' } }) ]); const bucketCount = Array.isArray(buckets) ? buckets.length : 0; const contentByCorpType = {}; allContents.forEach(content => { if (!contentByCorpType[content.CorpType_APC]) { contentByCorpType[content.CorpType_APC] = 0; } contentByCorpType[content.CorpType_APC]++; }); const contentMatrix = {}; allContents.forEach(content => { const key = `${content.Type_APC}_${content.CorpType_APC}`; if (!contentMatrix[key]) { contentMatrix[key] = { type: content.Type_APC, corp: content.CorpType_APC, count: 0 }; } contentMatrix[key].count++; }); const contentTimeline = {}; recentContents.forEach(content => { const date = new Date(content.CreatedAt_APC).toISOString().split('T')[0]; if (!contentTimeline[date]) { contentTimeline[date] = {}; } if (!contentTimeline[date][content.Type_APC]) { contentTimeline[date][content.Type_APC] = 0; } contentTimeline[date][content.Type_APC]++; }); const userRegistrationTrend = {}; recentUsers.forEach(user => { const date = new Date(user.CreatedAt_UT).toISOString().split('T')[0]; if (!userRegistrationTrend[date]) { userRegistrationTrend[date] = 0; } userRegistrationTrend[date]++; }); const campaignStatusDistribution = campaignStats.reduce((acc, item) => { acc[item.Status_ACP] = item._count.Status_ACP; return acc; }, {}); const activityTypeDistribution = userActivities.reduce((acc, item) => { acc[item.ActivityType_UA] = item._count.ActivityType_UA; return acc; }, {}); return successResponse(res, "CMS stats retrieved successfully", { summary: { content: { splash: splashCount, promo: promoCount, article: articleCount, banner: bannerCount, floatingWidget: floatingWidgetCount, total: splashCount + promoCount + articleCount + bannerCount + floatingWidgetCount }, infra: { buckets: bucketCount, apiKeys: apiKeysCount, users: usersCount, }, campaigns: await prisma.appCampaign.count(), activities: await prisma.usersActivity.count() }, charts: { contentByType: { labels: ['Splash', 'Promo', 'Article', 'Banner', 'Floating Widget'], data: [splashCount, promoCount, articleCount, bannerCount, floatingWidgetCount] }, contentByCorpType: { labels: Object.keys(contentByCorpType), data: Object.values(contentByCorpType) }, contentMatrix: Object.values(contentMatrix), contentCreationTimeline: contentTimeline, campaignStatus: { labels: Object.keys(campaignStatusDistribution), data: Object.values(campaignStatusDistribution) }, activityTypes: { labels: Object.keys(activityTypeDistribution), data: Object.values(activityTypeDistribution) }, userRegistrationTrend: { labels: Object.keys(userRegistrationTrend).sort(), data: Object.keys(userRegistrationTrend).sort().map(date => userRegistrationTrend[date]) } } }); } catch (err) { return badRequestResponse(res, "Internal Server Error", err); } };