tango-app-api-trax 3.8.21 → 3.8.22-nike
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +2 -2
- package/src/controllers/download.controller.js +19 -19
- package/src/controllers/gallery.controller.js +91 -27
- package/src/controllers/handlebar-helper.js +1 -0
- package/src/controllers/internalTrax.controller.js +808 -87
- package/src/controllers/mobileTrax.controller.js +262 -211
- package/src/controllers/teaxFlag.controller.js +5 -5
- package/src/controllers/trax.controller.js +6 -2
- package/src/hbs/recurringFlag.hbs +18 -16
- package/src/hbs/template.hbs +34 -23
- package/src/hbs/visit-checklist.hbs +84 -40
- package/src/hbs/weeklyWrap.hbs +218 -0
- package/src/routes/internalTraxApi.router.js +2 -0
- package/src/services/recurringFlagTracker.service.js +33 -33
- package/src/utils/visitChecklistPdf.utils.js +6 -5
|
@@ -325,7 +325,7 @@ export async function PCLconfigCreation( req, res ) {
|
|
|
325
325
|
},
|
|
326
326
|
} );
|
|
327
327
|
let getSections = await CLquestions.aggregate( sectionQuery );
|
|
328
|
-
if ( getSections.length || [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', '
|
|
328
|
+
if ( getSections.length || [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', 'suspiciousfootfall', 'drinking', 'bagdetection', 'inventorycount', 'carsattended', 'numberplateinfo', 'vehicle_check_in', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'storehygienemonitoring', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
329
329
|
if ( getSections.length ) {
|
|
330
330
|
for ( let element3 of getSections ) {
|
|
331
331
|
let collectQuestions = {};
|
|
@@ -650,11 +650,11 @@ export async function PCLconfigCreation( req, res ) {
|
|
|
650
650
|
// }
|
|
651
651
|
}
|
|
652
652
|
} else {
|
|
653
|
-
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', '
|
|
653
|
+
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', 'suspiciousfootfall', 'drinking', 'bagdetection', 'inventorycount', 'carsattended', 'numberplateinfo', 'vehicle_check_in', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert' ].includes( getCLconfig.checkListType ) ) {
|
|
654
654
|
let storeNameList = allQuestion.map( ( item ) => item.store_id );
|
|
655
|
-
let storeDetails = await storeService.find( { clientId: getCLconfig.client_id, status: 'active', ...( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert' ].includes( getCLconfig.checkListType ) ) ? { storeId: { $in: storeNameList } } : {} }, { storeId: 1 } );
|
|
655
|
+
let storeDetails = await storeService.find( { clientId: getCLconfig.client_id, status: 'active', ...( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) ? { storeId: { $in: storeNameList } } : {} }, { storeId: 1 } );
|
|
656
656
|
let storeList = storeDetails.map( ( store ) => store.storeId );
|
|
657
|
-
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert' ].includes( getCLconfig.checkListType ) ) {
|
|
657
|
+
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
658
658
|
allQuestion = allQuestion.filter( ( ele ) => storeList.includes( ele?.store_id ) );
|
|
659
659
|
} else {
|
|
660
660
|
allQuestion = storeDetails.map( ( item ) => {
|
|
@@ -688,7 +688,7 @@ export async function PCLconfigCreation( req, res ) {
|
|
|
688
688
|
client_id: getCLconfig.client_id,
|
|
689
689
|
aiStoreList: allQuestion.length ? allQuestion.map( ( store ) => store.store_id ) : [],
|
|
690
690
|
};
|
|
691
|
-
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert' ].includes( getCLconfig.checkListType ) ) {
|
|
691
|
+
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
692
692
|
let processData = {
|
|
693
693
|
aiStoreList: allQuestion.length ? allQuestion.map( ( store ) => {
|
|
694
694
|
return { storeName: store.storeName, storeId: store.store_id, events: store.events };
|
|
@@ -929,7 +929,7 @@ async function insertData( requestData ) {
|
|
|
929
929
|
},
|
|
930
930
|
} );
|
|
931
931
|
let getSections = await CLquestions.aggregate( sectionQuery );
|
|
932
|
-
if ( getSections.length || [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', '
|
|
932
|
+
if ( getSections.length || [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', 'suspiciousfootfall', 'drinking', 'bagdetection', 'inventorycount', 'carsattended', 'numberplateinfo', 'vehicle_check_in', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
933
933
|
if ( getSections.length ) {
|
|
934
934
|
for ( let element3 of getSections ) {
|
|
935
935
|
let collectQuestions = {};
|
|
@@ -1224,11 +1224,11 @@ async function insertData( requestData ) {
|
|
|
1224
1224
|
// }
|
|
1225
1225
|
}
|
|
1226
1226
|
} else {
|
|
1227
|
-
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', '
|
|
1227
|
+
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'eyetest', 'remoteoptometrist', 'storehygienemonitoring', 'cleaning', 'scrum', 'suspiciousactivity', 'suspiciousfootfall', 'drinking', 'bagdetection', 'inventorycount', 'carsattended', 'numberplateinfo', 'vehicle_check_in', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
1228
1228
|
let storeNameList = allQuestion.map( ( item ) => item.store_id );
|
|
1229
|
-
let storeDetails = await storeService.find( { clientId: getCLconfig.client_id, status: 'active', ...( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert' ].includes( getCLconfig.checkListType ) ) ? { storeId: { $in: storeNameList } } : {} }, { storeId: 1 } );
|
|
1229
|
+
let storeDetails = await storeService.find( { clientId: getCLconfig.client_id, status: 'active', ...( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) ? { storeId: { $in: storeNameList } } : {} }, { storeId: 1 } );
|
|
1230
1230
|
let storeList = storeDetails.map( ( store ) => store.storeId );
|
|
1231
|
-
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert' ].includes( getCLconfig.checkListType ) ) {
|
|
1231
|
+
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
1232
1232
|
allQuestion = allQuestion.filter( ( ele ) => storeList.includes( ele?.store_id ) );
|
|
1233
1233
|
} else {
|
|
1234
1234
|
allQuestion = storeDetails.map( ( item ) => {
|
|
@@ -1262,7 +1262,7 @@ async function insertData( requestData ) {
|
|
|
1262
1262
|
client_id: getCLconfig.client_id,
|
|
1263
1263
|
aiStoreList: allQuestion.length ? allQuestion.map( ( store ) => store.store_id ) : [],
|
|
1264
1264
|
};
|
|
1265
|
-
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert' ].includes( getCLconfig.checkListType ) ) {
|
|
1265
|
+
if ( [ 'storeopenandclose', 'mobileusagedetection', 'uniformdetection', 'customerunattended', 'staffleftinthemiddle', 'scrum', 'cleaning', 'outsidebusinesshoursqueuetracking', 'halfshutter', 'tvcompliance', 'cameratampering', 'queuealert', 'staffgrouping', 'boxalert', 'employeeCount' ].includes( getCLconfig.checkListType ) ) {
|
|
1266
1266
|
let processData = {
|
|
1267
1267
|
aiStoreList: allQuestion.length ? allQuestion.map( ( store ) => {
|
|
1268
1268
|
return { storeName: store.storeName, storeId: store.store_id, events: store.events };
|
|
@@ -2119,7 +2119,6 @@ export async function getPDFCSVChecklistDetails( req, res ) {
|
|
|
2119
2119
|
}
|
|
2120
2120
|
}
|
|
2121
2121
|
|
|
2122
|
-
|
|
2123
2122
|
export async function AiPushNotificationAlert( req, res ) {
|
|
2124
2123
|
try {
|
|
2125
2124
|
// console.log( req.body );
|
|
@@ -2176,7 +2175,6 @@ export async function liveAiPushNotificationAlert( req, res ) {
|
|
|
2176
2175
|
}
|
|
2177
2176
|
}
|
|
2178
2177
|
|
|
2179
|
-
|
|
2180
2178
|
export async function taskPushNotification( req, res ) {
|
|
2181
2179
|
try {
|
|
2182
2180
|
let query = [ {
|
|
@@ -2645,6 +2643,9 @@ export async function updateRunAI( req, res ) {
|
|
|
2645
2643
|
if ( !req.body.qName ) {
|
|
2646
2644
|
return res.sendError( 'Question name is required', 400 );
|
|
2647
2645
|
}
|
|
2646
|
+
if ( !req.body.userAnswer ) {
|
|
2647
|
+
return res.sendError( 'answer is required', 400 );
|
|
2648
|
+
}
|
|
2648
2649
|
let getDetails = await processedchecklist.findOne( { _id: req.body.id } );
|
|
2649
2650
|
if ( !getDetails ) {
|
|
2650
2651
|
return res.sendError( 'No data found', 204 );
|
|
@@ -2653,7 +2654,7 @@ export async function updateRunAI( req, res ) {
|
|
|
2653
2654
|
let updateData = {};
|
|
2654
2655
|
|
|
2655
2656
|
for ( let k of Object.keys( req.body.data ) ) {
|
|
2656
|
-
let keyPath = `questionAnswers.$[section].questions.$[question].userAnswer
|
|
2657
|
+
let keyPath = `questionAnswers.$[section].questions.$[question].userAnswer.$[userAnswer].${k}`;
|
|
2657
2658
|
updateData[keyPath] = req.body.data[k];
|
|
2658
2659
|
}
|
|
2659
2660
|
|
|
@@ -2664,7 +2665,9 @@ export async function updateRunAI( req, res ) {
|
|
|
2664
2665
|
arrayFilters: [
|
|
2665
2666
|
{ 'section.section_id': new ObjectId( req.body.sectionId ) },
|
|
2666
2667
|
{ 'question.qname': req.body.qName },
|
|
2668
|
+
{ 'userAnswer.answer': req.body.userAnswer },
|
|
2667
2669
|
],
|
|
2670
|
+
strict: false,
|
|
2668
2671
|
},
|
|
2669
2672
|
);
|
|
2670
2673
|
return res.sendSuccess( 'RunAI details updated successfully' );
|
|
@@ -2699,6 +2702,113 @@ export async function countUpdateRunAI( req, res ) {
|
|
|
2699
2702
|
}
|
|
2700
2703
|
}
|
|
2701
2704
|
|
|
2705
|
+
// Called by the runAI processing team once per (subject, checklist, section, qno, date) when their cron
|
|
2706
|
+
// detects a runAI flag. Idempotent per day via lastRunAIFlaggedDate — replaying the same date is a no-op.
|
|
2707
|
+
export async function incrementRunAIRecurring( req, res ) {
|
|
2708
|
+
try {
|
|
2709
|
+
let body = { ...( req.body || {} ) };
|
|
2710
|
+
if ( !body.section_id ) return res.sendError( 'section_id is required', 400 );
|
|
2711
|
+
if ( body.qno === undefined || body.qno === null || body.qno === '' ) return res.sendError( 'qno is required', 400 );
|
|
2712
|
+
if ( !body.id ) return res.sendError( 'Id is required', 400 );
|
|
2713
|
+
if ( !body.qname ) return res.sendError( 'Question name is required', 400 );
|
|
2714
|
+
if ( !body.sectionName ) return res.sendError( 'Section name is required', 400 );
|
|
2715
|
+
|
|
2716
|
+
const checklistDetails = await processedchecklist.findOne( { _id: body.id } );
|
|
2717
|
+
if ( !checklistDetails ) {
|
|
2718
|
+
return res.sendError( 'Checklist not found', 204 );
|
|
2719
|
+
}
|
|
2720
|
+
|
|
2721
|
+
// sectionName/qname live inside questionAnswers; surface them so the first-insert tracker doc is complete.
|
|
2722
|
+
// const section = ( checklistDetails.questionAnswers || [] ).find( ( s ) => String( s?.section_id ) === String( body.section_id ) );
|
|
2723
|
+
// const question = section?.questions?.find( ( q ) => String( q?.qno ) === String( body.qno ) );
|
|
2724
|
+
|
|
2725
|
+
body = {
|
|
2726
|
+
...body,
|
|
2727
|
+
date: checklistDetails.date_string,
|
|
2728
|
+
sourceCheckList_id: checklistDetails.sourceCheckList_id,
|
|
2729
|
+
client_id: checklistDetails.client_id,
|
|
2730
|
+
coverage: checklistDetails.coverage,
|
|
2731
|
+
store_id: checklistDetails.store_id,
|
|
2732
|
+
storeName: checklistDetails.storeName,
|
|
2733
|
+
user_id: checklistDetails.userId,
|
|
2734
|
+
userEmail: checklistDetails.userEmail,
|
|
2735
|
+
userName: checklistDetails.userName,
|
|
2736
|
+
checkListName: checklistDetails.checkListName,
|
|
2737
|
+
// sectionName: section?.sectionName || '',
|
|
2738
|
+
// qname: question?.qname || '',
|
|
2739
|
+
lastSubmittedBy: checklistDetails.userName || checklistDetails.userEmail || '--',
|
|
2740
|
+
lastSubmissionDate: checklistDetails.submitTime_string,
|
|
2741
|
+
};
|
|
2742
|
+
|
|
2743
|
+
// Skip checklists that don't have recurring flag configured — same gate that recurringFlagAlert uses
|
|
2744
|
+
// when picking which checklists to email for. Avoids creating tracker docs that would never be acted on.
|
|
2745
|
+
const checklistConfig = await CLconfig.findOne( { _id: body.sourceCheckList_id }, { recurringFlag: 1, publish: 1 } );
|
|
2746
|
+
if ( !checklistConfig ) {
|
|
2747
|
+
return res.sendError( 'Checklist not found', 404 );
|
|
2748
|
+
}
|
|
2749
|
+
const hasRecurring = ( Array.isArray( checklistConfig?.recurringFlag?.users ) && checklistConfig.recurringFlag.users.length > 0 ) ||
|
|
2750
|
+
( Array.isArray( checklistConfig?.recurringFlag?.notifyType ) && checklistConfig.recurringFlag.notifyType.length > 0 );
|
|
2751
|
+
if ( !hasRecurring ) {
|
|
2752
|
+
return res.sendSuccess( { message: 'Recurring flag not configured for this checklist', noop: true } );
|
|
2753
|
+
}
|
|
2754
|
+
|
|
2755
|
+
const isUserBased = ( body.coverage === 'user' ) || ( !body.store_id && ( body.user_id || body.userEmail ) );
|
|
2756
|
+
const storeId = isUserBased ? '' : ( body.store_id || '' );
|
|
2757
|
+
const userId = isUserBased ? ( body.user_id ? String( body.user_id ) : ( body.userEmail || '' ) ) : '';
|
|
2758
|
+
if ( !isUserBased && !storeId ) return res.sendError( 'store_id is required for store-based', 400 );
|
|
2759
|
+
if ( isUserBased && !userId ) return res.sendError( 'user_id or userEmail is required for user-based', 400 );
|
|
2760
|
+
|
|
2761
|
+
const date = body.date;
|
|
2762
|
+
const filter = {
|
|
2763
|
+
client_id: body.client_id,
|
|
2764
|
+
sourceCheckList_id: body.sourceCheckList_id,
|
|
2765
|
+
section_id: body.section_id,
|
|
2766
|
+
qno: String( body.qno ),
|
|
2767
|
+
...( isUserBased ? { user_id: userId } : { store_id: storeId } ),
|
|
2768
|
+
};
|
|
2769
|
+
|
|
2770
|
+
const existing = await recurringFlagTracker.findOne( filter, { lastRunAIFlaggedDate: 1 } );
|
|
2771
|
+
if ( existing && existing.lastRunAIFlaggedDate === date ) {
|
|
2772
|
+
return res.sendSuccess( { message: 'Already counted for this date', noop: true } );
|
|
2773
|
+
}
|
|
2774
|
+
|
|
2775
|
+
const setOnInsert = {
|
|
2776
|
+
coverage: isUserBased ? 'user' : 'store',
|
|
2777
|
+
checkListName: body.checkListName || '',
|
|
2778
|
+
sectionName: body.sectionName || '',
|
|
2779
|
+
qname: body.qname || '',
|
|
2780
|
+
storeName: isUserBased ? '' : ( body.storeName || '' ),
|
|
2781
|
+
userName: body.userName || '',
|
|
2782
|
+
userEmail: body.userEmail || '',
|
|
2783
|
+
};
|
|
2784
|
+
|
|
2785
|
+
console.log( setOnInsert );
|
|
2786
|
+
|
|
2787
|
+
await recurringFlagTracker.bulkWrite( [
|
|
2788
|
+
{
|
|
2789
|
+
updateOne: {
|
|
2790
|
+
filter,
|
|
2791
|
+
update: {
|
|
2792
|
+
$setOnInsert: setOnInsert,
|
|
2793
|
+
$set: {
|
|
2794
|
+
lastRunAIFlaggedDate: date,
|
|
2795
|
+
...( body.lastSubmittedBy ? { lastSubmittedBy: body.lastSubmittedBy } : {} ),
|
|
2796
|
+
...( body.lastSubmissionDate ? { lastSubmissionDate: body.lastSubmissionDate } : {} ),
|
|
2797
|
+
},
|
|
2798
|
+
$inc: { runAICount: 1 },
|
|
2799
|
+
},
|
|
2800
|
+
upsert: true,
|
|
2801
|
+
},
|
|
2802
|
+
},
|
|
2803
|
+
] );
|
|
2804
|
+
|
|
2805
|
+
return res.sendSuccess( { message: 'runAI recurring count updated' } );
|
|
2806
|
+
} catch ( e ) {
|
|
2807
|
+
logger.error( { functionName: 'incrementRunAIRecurring', error: e } );
|
|
2808
|
+
return res.sendError( e, 500 );
|
|
2809
|
+
}
|
|
2810
|
+
}
|
|
2811
|
+
|
|
2702
2812
|
export async function getRunAIQuestions( req, res ) {
|
|
2703
2813
|
try {
|
|
2704
2814
|
let requestData = req.body;
|
|
@@ -3749,7 +3859,6 @@ export async function checklistAutoMailList( req, res ) {
|
|
|
3749
3859
|
}
|
|
3750
3860
|
}
|
|
3751
3861
|
|
|
3752
|
-
|
|
3753
3862
|
export const downloadInsertPdfOld = async ( req, res ) => {
|
|
3754
3863
|
try {
|
|
3755
3864
|
setImmediate( async () => {
|
|
@@ -4227,17 +4336,18 @@ export async function getEyetestStream( req, res ) {
|
|
|
4227
4336
|
}
|
|
4228
4337
|
}
|
|
4229
4338
|
|
|
4230
|
-
function buildRecurringFlagExcel( rows ) {
|
|
4339
|
+
function buildRecurringFlagExcel( rows, subjectLabel = 'Store' ) {
|
|
4231
4340
|
const workbook = new ExcelJS.Workbook();
|
|
4232
4341
|
const sheet = workbook.addWorksheet( 'Recurring Flags' );
|
|
4233
4342
|
sheet.columns = [
|
|
4234
|
-
{ header:
|
|
4343
|
+
{ header: `${subjectLabel} Name`, key: 'storeName', width: 25 },
|
|
4235
4344
|
{ header: 'Checklist Name', key: 'checklistName', width: 30 },
|
|
4236
4345
|
{ header: 'Section', key: 'sectionName', width: 25 },
|
|
4237
4346
|
{ header: 'Question', key: 'questionName', width: 40 },
|
|
4238
4347
|
{ header: 'Last Submitted By', key: 'lastSubmittedBy', width: 25 },
|
|
4239
4348
|
{ header: 'Last Submission Date', key: 'lastSubmissionDate', width: 22 },
|
|
4240
4349
|
{ header: 'Recurring Days', key: 'days', width: 16 },
|
|
4350
|
+
{ header: 'Run AI Flags', key: 'runAICount', width: 14 },
|
|
4241
4351
|
];
|
|
4242
4352
|
sheet.getRow( 1 ).font = { bold: true };
|
|
4243
4353
|
rows.forEach( ( r ) => sheet.addRow( r ) );
|
|
@@ -4249,13 +4359,12 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4249
4359
|
const checklistDetails = await CLconfig.find( {
|
|
4250
4360
|
publish: true,
|
|
4251
4361
|
$expr: {
|
|
4252
|
-
$
|
|
4253
|
-
{ $size: { $cond: [ { $isArray: '$recurringFlag.users' }, '$recurringFlag.users', [] ] } },
|
|
4254
|
-
0,
|
|
4362
|
+
$or: [
|
|
4363
|
+
{ $gt: [ { $size: { $cond: [ { $isArray: '$recurringFlag.users' }, '$recurringFlag.users', [] ] } }, 0 ] },
|
|
4364
|
+
{ $gt: [ { $size: { $cond: [ { $isArray: '$recurringFlag.notifyType' }, '$recurringFlag.notifyType', [] ] } }, 0 ] },
|
|
4255
4365
|
],
|
|
4256
4366
|
},
|
|
4257
|
-
}, { _id: 1, checkListName: 1, recurringFlag: 1, approver: 1, client_id: 1 } );
|
|
4258
|
-
|
|
4367
|
+
}, { _id: 1, checkListName: 1, recurringFlag: 1, notifyFlags: 1, approver: 1, client_id: 1 } );
|
|
4259
4368
|
if ( !checklistDetails.length ) {
|
|
4260
4369
|
return res.sendSuccess( 'No checklists configured for recurring flag' );
|
|
4261
4370
|
}
|
|
@@ -4268,42 +4377,103 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4268
4377
|
const threshold = cl?.recurringFlag?.threshold || 3;
|
|
4269
4378
|
const notifyType = cl?.recurringFlag?.notifyType || [];
|
|
4270
4379
|
const users = cl?.recurringFlag?.users || [];
|
|
4271
|
-
|
|
4272
4380
|
let recipients = [];
|
|
4381
|
+
if ( notifyType.includes( 'sameAsNotify' ) ) {
|
|
4382
|
+
const nfType = cl?.notifyFlags?.notifyType || [];
|
|
4383
|
+
const nfUsers = cl?.notifyFlags?.users || [];
|
|
4384
|
+
if ( nfType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
|
|
4385
|
+
recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
|
|
4386
|
+
}
|
|
4387
|
+
recipients = [ ...recipients, ...nfUsers.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4388
|
+
}
|
|
4273
4389
|
if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
|
|
4274
|
-
recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
|
|
4390
|
+
recipients = [ ...recipients, ...cl.approver.map( ( a ) => a?.value ).filter( Boolean ) ];
|
|
4275
4391
|
}
|
|
4276
4392
|
recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4277
4393
|
recipients = [ ...new Set( recipients ) ];
|
|
4278
4394
|
|
|
4279
4395
|
if ( !recipients.length ) return;
|
|
4280
4396
|
|
|
4281
|
-
//
|
|
4282
|
-
//
|
|
4397
|
+
// Resolve each recipient's allowed-store scope once per checklist:
|
|
4398
|
+
// allowed === null → full access (superadmin / non-client userType / external recipient not in users collection)
|
|
4399
|
+
// allowed === Set → restrict store-based rows to these storeIds
|
|
4400
|
+
// User-based tracker rows pass through regardless (no store binding).
|
|
4401
|
+
const recipientFilters = await Promise.all( recipients.map( async ( recipient ) => {
|
|
4402
|
+
const userDetails = await userService.findOne(
|
|
4403
|
+
{ email: recipient },
|
|
4404
|
+
{ email: 1, assignedStores: 1, userType: 1, role: 1, clientId: 1 },
|
|
4405
|
+
);
|
|
4406
|
+
// Unknown recipients (external approvers etc.) keep full access — preserves existing behavior.
|
|
4407
|
+
const allowed = userDetails ? await resolveUserAssignedStores( userDetails ) : null;
|
|
4408
|
+
return { recipient, allowed };
|
|
4409
|
+
} ) );
|
|
4410
|
+
|
|
4411
|
+
// Read tracker rows where EITHER the sop streak or the runAI count has hit threshold and the
|
|
4412
|
+
// current trigger hasn't been emailed yet (lastEmailDate vs the relevant flagged date).
|
|
4283
4413
|
const trackerRows = await recurringFlagTracker.find( {
|
|
4284
4414
|
sourceCheckList_id: cl._id,
|
|
4285
|
-
|
|
4286
|
-
|
|
4415
|
+
$or: [
|
|
4416
|
+
{
|
|
4417
|
+
consecutiveCount: { $gte: threshold },
|
|
4418
|
+
$expr: { $ne: [ { $ifNull: [ '$lastEmailDate', '' ] }, { $ifNull: [ '$lastFlaggedDate', '' ] } ] },
|
|
4419
|
+
},
|
|
4420
|
+
{
|
|
4421
|
+
runAICount: { $gte: threshold },
|
|
4422
|
+
$expr: { $ne: [ { $ifNull: [ '$lastEmailDate', '' ] }, { $ifNull: [ '$lastRunAIFlaggedDate', '' ] } ] },
|
|
4423
|
+
},
|
|
4424
|
+
],
|
|
4287
4425
|
} );
|
|
4288
4426
|
|
|
4289
4427
|
for ( const t of trackerRows ) {
|
|
4290
|
-
|
|
4428
|
+
const isUserBased = t.coverage === 'user' || ( !t.store_id && ( t.user_id || t.userEmail ) );
|
|
4429
|
+
// For user-based checklists, group/identify by userEmail; for store-based, by store_id.
|
|
4430
|
+
const subjectId = isUserBased ? ( t.userEmail || t.user_id || '' ) : ( t.store_id || '' );
|
|
4431
|
+
const subjectName = isUserBased ? ( t.userName || t.userEmail || '--' ) : ( t.storeName || '--' );
|
|
4432
|
+
// Determine which streak crossed threshold for this row — drives reset granularity below.
|
|
4433
|
+
const sopFired = ( t.consecutiveCount || 0 ) >= threshold && t.lastEmailDate !== t.lastFlaggedDate;
|
|
4434
|
+
const runAIFired = ( t.runAICount || 0 ) >= threshold && t.lastEmailDate !== t.lastRunAIFlaggedDate;
|
|
4435
|
+
|
|
4436
|
+
let rowEmitted = false;
|
|
4437
|
+
for ( const { recipient, allowed } of recipientFilters ) {
|
|
4438
|
+
// Skip store-based rows outside the recipient's reachable stores. Full-access (null) and
|
|
4439
|
+
// user-based rows always pass through.
|
|
4440
|
+
if ( allowed !== null && !isUserBased && !allowed.has( t.store_id ) ) continue;
|
|
4291
4441
|
triggers.push( {
|
|
4292
4442
|
recipient,
|
|
4293
4443
|
clientId: t.client_id,
|
|
4294
|
-
|
|
4295
|
-
|
|
4444
|
+
coverage: isUserBased ? 'user' : 'store',
|
|
4445
|
+
subjectId,
|
|
4446
|
+
subjectName,
|
|
4447
|
+
storeId: t.store_id || '',
|
|
4448
|
+
storeName: t.storeName || '',
|
|
4449
|
+
userId: t.user_id || '',
|
|
4450
|
+
userName: t.userName || '',
|
|
4451
|
+
userEmail: t.userEmail || '',
|
|
4296
4452
|
checklistId: cl._id.toString(),
|
|
4297
4453
|
checklistName: cl.checkListName?.trim() || t.checkListName || '',
|
|
4298
4454
|
sectionName: t.sectionName,
|
|
4299
4455
|
qno: t.qno,
|
|
4300
4456
|
qname: t.qname,
|
|
4301
4457
|
days: t.consecutiveCount,
|
|
4458
|
+
runAICount: t.runAICount || 0,
|
|
4459
|
+
sopFired,
|
|
4460
|
+
runAIFired,
|
|
4302
4461
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4303
4462
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4304
4463
|
} );
|
|
4464
|
+
rowEmitted = true;
|
|
4465
|
+
}
|
|
4466
|
+
// Only reset rows that actually went into at least one recipient's email — otherwise a row
|
|
4467
|
+
// visible to nobody would silently zero its streak without an email being sent.
|
|
4468
|
+
if ( rowEmitted ) {
|
|
4469
|
+
trackerIdsToReset.push( {
|
|
4470
|
+
_id: t._id,
|
|
4471
|
+
lastFlaggedDate: t.lastFlaggedDate,
|
|
4472
|
+
lastRunAIFlaggedDate: t.lastRunAIFlaggedDate,
|
|
4473
|
+
sopFired,
|
|
4474
|
+
runAIFired,
|
|
4475
|
+
} );
|
|
4305
4476
|
}
|
|
4306
|
-
trackerIdsToReset.push( { _id: t._id, lastFlaggedDate: t.lastFlaggedDate } );
|
|
4307
4477
|
}
|
|
4308
4478
|
} ) );
|
|
4309
4479
|
|
|
@@ -4311,6 +4481,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4311
4481
|
return res.sendSuccess( 'No recurring flags reached threshold' );
|
|
4312
4482
|
}
|
|
4313
4483
|
|
|
4484
|
+
|
|
4314
4485
|
// Group triggers by recipient.
|
|
4315
4486
|
const byRecipient = new Map();
|
|
4316
4487
|
for ( const t of triggers ) {
|
|
@@ -4325,21 +4496,77 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4325
4496
|
const sentSummary = [];
|
|
4326
4497
|
|
|
4327
4498
|
await Promise.all( [ ...byRecipient.entries() ].map( async ( [ recipient, items ] ) => {
|
|
4328
|
-
const
|
|
4499
|
+
const subjects = new Set( items.map( ( i ) => i.subjectId ) );
|
|
4329
4500
|
const checklists = new Set( items.map( ( i ) => i.checklistId ) );
|
|
4330
|
-
const isMultiStore =
|
|
4501
|
+
const isMultiStore = subjects.size > 1; // "isMultiStore" name retained for template back-compat
|
|
4331
4502
|
const isMultiChecklist = !isMultiStore && checklists.size > 1;
|
|
4332
|
-
//
|
|
4503
|
+
// Sub-mode of multi-store when the recipient's flagged subjects all share a single checklist.
|
|
4504
|
+
// Drives a tighter email layout (no Checklist column, no "Total Checklists" line, checklist name in intro).
|
|
4505
|
+
const isMultiStoreSingleChecklist = isMultiStore && checklists.size === 1;
|
|
4506
|
+
// Threshold for the message line — when grouping spans multiple checklists/subjects, take min threshold seen.
|
|
4333
4507
|
const thresholdShown = items.reduce( ( acc, it ) => Math.min( acc, it.days ), items[0].days );
|
|
4334
4508
|
|
|
4335
|
-
|
|
4336
|
-
|
|
4509
|
+
// If every trigger for this recipient is user-based, label as User. Mixed sets fall back to Store.
|
|
4510
|
+
const coverages = new Set( items.map( ( i ) => i.coverage ) );
|
|
4511
|
+
const isAllUser = coverages.size === 1 && coverages.has( 'user' );
|
|
4512
|
+
const subjectLabel = isAllUser ? 'User' : 'Store';
|
|
4513
|
+
const subjectLabelPlural = isAllUser ? 'Users' : 'Stores';
|
|
4514
|
+
const subjectLabelLower = isAllUser ? 'user' : 'store';
|
|
4515
|
+
const subjectLabelPluralLower = isAllUser ? 'users' : 'stores';
|
|
4516
|
+
|
|
4517
|
+
// Aggregate triggers per (subject, checklist) — each table row counts how many distinct questions
|
|
4518
|
+
// hit the recurring threshold for that pair. The streak length on each question is no longer surfaced
|
|
4519
|
+
// in the email body; it remains in the per-question Excel breakdown below.
|
|
4520
|
+
const parseSubmissionDate = ( s ) => {
|
|
4521
|
+
if ( !s ) return 0;
|
|
4522
|
+
const d = dayjs( s, 'hh:mm A, DD MMM YYYY' );
|
|
4523
|
+
return d.isValid() ? d.valueOf() : 0;
|
|
4524
|
+
};
|
|
4525
|
+
const groupMap = new Map();
|
|
4526
|
+
for ( const i of items ) {
|
|
4527
|
+
const k = `${i.subjectId}::${i.checklistId}`;
|
|
4528
|
+
if ( !groupMap.has( k ) ) {
|
|
4529
|
+
groupMap.set( k, {
|
|
4530
|
+
subjectId: i.subjectId,
|
|
4531
|
+
subjectName: i.subjectName,
|
|
4532
|
+
checklistId: i.checklistId,
|
|
4533
|
+
checklistName: i.checklistName,
|
|
4534
|
+
questionCount: 0,
|
|
4535
|
+
runAICount: 0,
|
|
4536
|
+
lastSubmittedBy: i.lastSubmittedBy,
|
|
4537
|
+
lastSubmissionDate: i.lastSubmissionDate,
|
|
4538
|
+
} );
|
|
4539
|
+
}
|
|
4540
|
+
const g = groupMap.get( k );
|
|
4541
|
+
if ( i.sopFired ) g.questionCount += 1;
|
|
4542
|
+
if ( i.runAIFired ) g.runAICount += 1;
|
|
4543
|
+
if ( parseSubmissionDate( i.lastSubmissionDate ) > parseSubmissionDate( g.lastSubmissionDate ) ) {
|
|
4544
|
+
g.lastSubmissionDate = i.lastSubmissionDate;
|
|
4545
|
+
g.lastSubmittedBy = i.lastSubmittedBy;
|
|
4546
|
+
}
|
|
4547
|
+
}
|
|
4548
|
+
const rows = [ ...groupMap.values() ].map( ( g ) => ( {
|
|
4549
|
+
subjectName: g.subjectName,
|
|
4550
|
+
storeName: g.subjectName, // legacy field name still consumed by template fallbacks
|
|
4551
|
+
checklistName: g.checklistName,
|
|
4552
|
+
lastSubmittedBy: g.lastSubmittedBy,
|
|
4553
|
+
lastSubmissionDate: g.lastSubmissionDate,
|
|
4554
|
+
days: g.questionCount, // legacy alias kept for back-compat with older template builds
|
|
4555
|
+
flagCount: g.questionCount,
|
|
4556
|
+
runAICount: g.runAICount,
|
|
4557
|
+
totalFlags: g.questionCount + g.runAICount,
|
|
4558
|
+
} ) );
|
|
4559
|
+
|
|
4560
|
+
// Excel attachment keeps per-question detail (one row per flagged question).
|
|
4561
|
+
const excelRows = items.map( ( i ) => ( {
|
|
4562
|
+
storeName: i.subjectName,
|
|
4337
4563
|
checklistName: i.checklistName,
|
|
4338
4564
|
sectionName: i.sectionName,
|
|
4339
4565
|
questionName: i.qname,
|
|
4340
4566
|
lastSubmittedBy: i.lastSubmittedBy,
|
|
4341
4567
|
lastSubmissionDate: i.lastSubmissionDate,
|
|
4342
4568
|
days: i.days,
|
|
4569
|
+
runAICount: i.runAICount || 0,
|
|
4343
4570
|
} ) );
|
|
4344
4571
|
|
|
4345
4572
|
const ATTACHMENT_THRESHOLD = 10;
|
|
@@ -4350,31 +4577,43 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4350
4577
|
threshold: thresholdShown,
|
|
4351
4578
|
isMultiStore,
|
|
4352
4579
|
isMultiChecklist,
|
|
4580
|
+
isMultiStoreSingleChecklist,
|
|
4581
|
+
isUserCoverage: isAllUser,
|
|
4353
4582
|
showTable: isMultiStore || isMultiChecklist,
|
|
4354
4583
|
hasAttachment,
|
|
4355
4584
|
domain: flagDomain,
|
|
4356
4585
|
rows: displayRows,
|
|
4586
|
+
subjectLabel,
|
|
4587
|
+
subjectLabelPlural,
|
|
4588
|
+
subjectLabelLower,
|
|
4589
|
+
subjectLabelPluralLower,
|
|
4357
4590
|
};
|
|
4358
4591
|
|
|
4359
4592
|
if ( isMultiStore ) {
|
|
4360
4593
|
data.highlights = {
|
|
4361
|
-
|
|
4594
|
+
totalSubjects: subjects.size,
|
|
4595
|
+
totalStores: subjects.size, // legacy alias
|
|
4362
4596
|
totalChecklists: checklists.size,
|
|
4363
4597
|
totalFlags: items.length,
|
|
4364
4598
|
};
|
|
4599
|
+
if ( isMultiStoreSingleChecklist ) {
|
|
4600
|
+
// Show the single checklist name in the intro line for this sub-mode.
|
|
4601
|
+
data.checklistName = items[0].checklistName;
|
|
4602
|
+
}
|
|
4365
4603
|
} else if ( isMultiChecklist ) {
|
|
4366
|
-
data.
|
|
4604
|
+
data.subjectName = items[0].subjectName;
|
|
4605
|
+
data.storeName = items[0].subjectName;
|
|
4367
4606
|
} else {
|
|
4368
|
-
|
|
4369
|
-
|
|
4607
|
+
// Single mode: one (subject, checklist) — totalFlags = sop question flags + runAI flags.
|
|
4608
|
+
const single = rows[0];
|
|
4609
|
+
data.subjectName = single.subjectName;
|
|
4610
|
+
data.storeName = single.subjectName;
|
|
4370
4611
|
data.checklistName = single.checklistName;
|
|
4371
|
-
data.questionName = single.qname;
|
|
4372
4612
|
data.lastSubmittedBy = single.lastSubmittedBy;
|
|
4373
4613
|
data.lastSubmissionDate = single.lastSubmissionDate;
|
|
4374
|
-
data.
|
|
4375
|
-
data.
|
|
4376
|
-
data.
|
|
4377
|
-
data.flagCountPlural = false;
|
|
4614
|
+
data.flagCount = single.flagCount;
|
|
4615
|
+
data.runAICount = single.runAICount;
|
|
4616
|
+
data.totalFlags = single.totalFlags;
|
|
4378
4617
|
}
|
|
4379
4618
|
|
|
4380
4619
|
const html = compiled( { data } );
|
|
@@ -4389,7 +4628,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4389
4628
|
|
|
4390
4629
|
if ( hasAttachment ) {
|
|
4391
4630
|
try {
|
|
4392
|
-
const buf = await buildRecurringFlagExcel(
|
|
4631
|
+
const buf = await buildRecurringFlagExcel( excelRows, subjectLabel );
|
|
4393
4632
|
params.attachment = {
|
|
4394
4633
|
filename: 'Recurring-Flags-Summary.xlsx',
|
|
4395
4634
|
content: Buffer.from( buf ),
|
|
@@ -4399,19 +4638,31 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4399
4638
|
logger.error( { functionName: 'recurringFlagAlert.buildExcel', error: e } );
|
|
4400
4639
|
}
|
|
4401
4640
|
}
|
|
4402
|
-
|
|
4403
4641
|
sendEmailWithSES( params.toEmail, params.mailSubject, params.htmlBody, params.attachment, params.sourceEmail );
|
|
4404
4642
|
sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
|
|
4405
4643
|
} ) );
|
|
4406
4644
|
|
|
4407
|
-
// Reset
|
|
4645
|
+
// Reset only the streak(s) that crossed threshold. Stamp lastEmailDate to the most recent triggering
|
|
4646
|
+
// flag date so dedup remains correct. Append that date to emailHistory (rolling last 60).
|
|
4408
4647
|
if ( trackerIdsToReset.length ) {
|
|
4409
|
-
const resetOps = trackerIdsToReset.map( (
|
|
4410
|
-
|
|
4411
|
-
|
|
4412
|
-
|
|
4413
|
-
|
|
4414
|
-
|
|
4648
|
+
const resetOps = trackerIdsToReset.map( ( r ) => {
|
|
4649
|
+
const set = {};
|
|
4650
|
+
if ( r.sopFired ) set.consecutiveCount = 0;
|
|
4651
|
+
if ( r.runAIFired ) set.runAICount = 0;
|
|
4652
|
+
const stampDate = r.runAIFired ?
|
|
4653
|
+
( r.lastRunAIFlaggedDate || r.lastFlaggedDate || '' ) :
|
|
4654
|
+
( r.lastFlaggedDate || r.lastRunAIFlaggedDate || '' );
|
|
4655
|
+
set.lastEmailDate = stampDate;
|
|
4656
|
+
return {
|
|
4657
|
+
updateOne: {
|
|
4658
|
+
filter: { _id: r._id },
|
|
4659
|
+
update: {
|
|
4660
|
+
$set: set,
|
|
4661
|
+
$push: { emailHistory: { $each: [ stampDate ], $slice: -60 } },
|
|
4662
|
+
},
|
|
4663
|
+
},
|
|
4664
|
+
};
|
|
4665
|
+
} );
|
|
4415
4666
|
await recurringFlagTracker.bulkWrite( resetOps );
|
|
4416
4667
|
}
|
|
4417
4668
|
|
|
@@ -4422,41 +4673,510 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4422
4673
|
}
|
|
4423
4674
|
}
|
|
4424
4675
|
|
|
4676
|
+
function getLastWeekRange( ref = dayjs() ) {
|
|
4677
|
+
const today = ref.startOf( 'day' );
|
|
4678
|
+
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4679
|
+
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4680
|
+
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4681
|
+
// Current week (Mon..Sun) — primary range surfaced in the email.
|
|
4682
|
+
const weekStart = thisMonday;
|
|
4683
|
+
const weekEnd = thisMonday.add( 6, 'day' );
|
|
4684
|
+
// Previous full week (Mon..Sun) — used for WoW comparisons.
|
|
4685
|
+
const prevWeekStart = thisMonday.subtract( 7, 'day' );
|
|
4686
|
+
const prevWeekEnd = thisMonday.subtract( 1, 'day' );
|
|
4687
|
+
return {
|
|
4688
|
+
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4689
|
+
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
4690
|
+
prevWeekStart: prevWeekStart.format( 'YYYY-MM-DD' ),
|
|
4691
|
+
prevWeekEnd: prevWeekEnd.format( 'YYYY-MM-DD' ),
|
|
4692
|
+
};
|
|
4693
|
+
}
|
|
4694
|
+
|
|
4695
|
+
function computeWow( current, previous ) {
|
|
4696
|
+
if ( !previous ) {
|
|
4697
|
+
if ( current ) return { value: '100%', direction: 'up' };
|
|
4698
|
+
return { value: '', direction: 'up' };
|
|
4699
|
+
}
|
|
4700
|
+
const delta = current - previous;
|
|
4701
|
+
if ( delta === 0 ) return { value: '0%', direction: 'up' };
|
|
4702
|
+
const pct = Math.min( 100, Math.round( ( Math.abs( delta ) / previous ) * 100 ) );
|
|
4703
|
+
return { value: `${pct}%`, direction: delta > 0 ? 'up' : 'down' };
|
|
4704
|
+
}
|
|
4705
|
+
|
|
4706
|
+
async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
4707
|
+
const rows = await processedchecklist.aggregate( [
|
|
4708
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, checkListType: 'custom' } },
|
|
4709
|
+
{ $group: {
|
|
4710
|
+
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4711
|
+
storeName: { $last: '$storeName' },
|
|
4712
|
+
questionFlag: { $sum: { $ifNull: [ '$questionFlag', 0 ] } },
|
|
4713
|
+
timeFlag: { $sum: { $ifNull: [ '$timeFlag', 0 ] } },
|
|
4714
|
+
runAIFlag: { $sum: { $ifNull: [ '$runAIFlag', 0 ] } },
|
|
4715
|
+
flaggedChecklistIds: { $addToSet: {
|
|
4716
|
+
$cond: [
|
|
4717
|
+
{ $or: [
|
|
4718
|
+
{ $gt: [ { $ifNull: [ '$questionFlag', 0 ] }, 0 ] },
|
|
4719
|
+
{ $gt: [ { $ifNull: [ '$timeFlag', 0 ] }, 0 ] },
|
|
4720
|
+
{ $gt: [ { $ifNull: [ '$runAIFlag', 0 ] }, 0 ] },
|
|
4721
|
+
] },
|
|
4722
|
+
'$sourceCheckList_id',
|
|
4723
|
+
null,
|
|
4724
|
+
],
|
|
4725
|
+
} },
|
|
4726
|
+
} },
|
|
4727
|
+
] );
|
|
4728
|
+
const map = new Map();
|
|
4729
|
+
for ( const r of rows ) {
|
|
4730
|
+
const key = `${r._id.client_id}::${r._id.store_id}`;
|
|
4731
|
+
const flaggedChecklists = ( r.flaggedChecklistIds || [] ).filter( ( id ) => id ).map( ( id ) => String( id ) );
|
|
4732
|
+
const totalFlags = ( r.questionFlag || 0 ) + ( r.timeFlag || 0 ) + ( r.runAIFlag || 0 );
|
|
4733
|
+
map.set( key, {
|
|
4734
|
+
client_id: r._id.client_id,
|
|
4735
|
+
store_id: r._id.store_id,
|
|
4736
|
+
storeName: r.storeName,
|
|
4737
|
+
questionFlag: r.questionFlag || 0,
|
|
4738
|
+
timeFlag: r.timeFlag || 0,
|
|
4739
|
+
runAIFlag: r.runAIFlag || 0,
|
|
4740
|
+
flaggedChecklists,
|
|
4741
|
+
totalFlags,
|
|
4742
|
+
} );
|
|
4743
|
+
}
|
|
4744
|
+
return map;
|
|
4745
|
+
}
|
|
4746
|
+
|
|
4747
|
+
async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
4748
|
+
// Counts how many distinct days within the week the store received a recurring-flag email.
|
|
4749
|
+
// Each tracker doc is per-question, so a single email covering N flagged questions adds the same date
|
|
4750
|
+
// to N tracker docs — we de-dupe by date per store via $addToSet.
|
|
4751
|
+
const rows = await recurringFlagTracker.aggregate( [
|
|
4752
|
+
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4753
|
+
{ $project: {
|
|
4754
|
+
client_id: 1,
|
|
4755
|
+
store_id: 1,
|
|
4756
|
+
storeName: 1,
|
|
4757
|
+
datesInWeek: {
|
|
4758
|
+
$filter: {
|
|
4759
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4760
|
+
as: 'd',
|
|
4761
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4762
|
+
},
|
|
4763
|
+
},
|
|
4764
|
+
} },
|
|
4765
|
+
{ $unwind: '$datesInWeek' },
|
|
4766
|
+
{ $group: {
|
|
4767
|
+
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4768
|
+
storeName: { $last: '$storeName' },
|
|
4769
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4770
|
+
} },
|
|
4771
|
+
{ $project: {
|
|
4772
|
+
_id: 1,
|
|
4773
|
+
storeName: 1,
|
|
4774
|
+
count: { $size: '$uniqueDates' },
|
|
4775
|
+
} },
|
|
4776
|
+
] );
|
|
4777
|
+
const map = new Map();
|
|
4778
|
+
for ( const r of rows ) {
|
|
4779
|
+
map.set( `${r._id.client_id}::${r._id.store_id}`, {
|
|
4780
|
+
client_id: r._id.client_id,
|
|
4781
|
+
store_id: r._id.store_id,
|
|
4782
|
+
storeName: r.storeName,
|
|
4783
|
+
count: r.count,
|
|
4784
|
+
} );
|
|
4785
|
+
}
|
|
4786
|
+
return map;
|
|
4787
|
+
}
|
|
4788
|
+
|
|
4789
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4790
|
+
const workbook = new ExcelJS.Workbook();
|
|
4791
|
+
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4792
|
+
sheet.addRow( [ header ] );
|
|
4793
|
+
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4794
|
+
sheet.addRow( [] );
|
|
4795
|
+
sheet.columns = [
|
|
4796
|
+
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4797
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4798
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4799
|
+
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4800
|
+
{ header: 'Not Submitted Flags', key: 'timeFlag', width: 20 },
|
|
4801
|
+
{ header: 'Run AI Flags', key: 'runAIFlag', width: 14 },
|
|
4802
|
+
{ header: 'Recurring Flags', key: 'recurringFlag', width: 18 },
|
|
4803
|
+
];
|
|
4804
|
+
sheet.getRow( 3 ).font = { bold: true };
|
|
4805
|
+
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4806
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4807
|
+
return workbook.xlsx.writeBuffer();
|
|
4808
|
+
}
|
|
4809
|
+
|
|
4810
|
+
// Per-(store, checklist) flag aggregation for the weekly Excel attachment. Same source as
|
|
4811
|
+
// aggregateWeeklyFlagsByStore but additionally grouped by sourceCheckList_id so each row in the export
|
|
4812
|
+
// corresponds to one store × one checklist.
|
|
4813
|
+
async function aggregateWeeklyFlagsByStoreChecklist( weekStart, weekEnd ) {
|
|
4814
|
+
const rows = await processedchecklist.aggregate( [
|
|
4815
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, isdeleted: { $ne: true } } },
|
|
4816
|
+
{ $group: {
|
|
4817
|
+
_id: { client_id: '$client_id', store_id: '$store_id', sourceCheckList_id: '$sourceCheckList_id' },
|
|
4818
|
+
storeName: { $last: '$storeName' },
|
|
4819
|
+
checkListName: { $last: '$checkListName' },
|
|
4820
|
+
questionFlag: { $sum: { $ifNull: [ '$questionFlag', 0 ] } },
|
|
4821
|
+
timeFlag: { $sum: { $ifNull: [ '$timeFlag', 0 ] } },
|
|
4822
|
+
runAIFlag: { $sum: { $ifNull: [ '$runAIFlag', 0 ] } },
|
|
4823
|
+
} },
|
|
4824
|
+
] );
|
|
4825
|
+
const map = new Map();
|
|
4826
|
+
for ( const r of rows ) {
|
|
4827
|
+
const key = `${r._id.client_id}::${r._id.store_id}::${r._id.sourceCheckList_id}`;
|
|
4828
|
+
map.set( key, {
|
|
4829
|
+
client_id: r._id.client_id,
|
|
4830
|
+
store_id: r._id.store_id,
|
|
4831
|
+
sourceCheckList_id: String( r._id.sourceCheckList_id || '' ),
|
|
4832
|
+
storeName: r.storeName,
|
|
4833
|
+
checkListName: r.checkListName,
|
|
4834
|
+
questionFlag: r.questionFlag || 0,
|
|
4835
|
+
timeFlag: r.timeFlag || 0,
|
|
4836
|
+
runAIFlag: r.runAIFlag || 0,
|
|
4837
|
+
} );
|
|
4838
|
+
}
|
|
4839
|
+
return map;
|
|
4840
|
+
}
|
|
4841
|
+
|
|
4842
|
+
// Per-(store, checklist) recurring count = distinct days the (store, checklist) was emailed in the week.
|
|
4843
|
+
async function aggregateWeeklyRecurringByStoreChecklist( weekStart, weekEnd ) {
|
|
4844
|
+
const rows = await recurringFlagTracker.aggregate( [
|
|
4845
|
+
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4846
|
+
{ $project: {
|
|
4847
|
+
client_id: 1,
|
|
4848
|
+
store_id: 1,
|
|
4849
|
+
storeName: 1,
|
|
4850
|
+
sourceCheckList_id: 1,
|
|
4851
|
+
checkListName: 1,
|
|
4852
|
+
datesInWeek: {
|
|
4853
|
+
$filter: {
|
|
4854
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4855
|
+
as: 'd',
|
|
4856
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4857
|
+
},
|
|
4858
|
+
},
|
|
4859
|
+
} },
|
|
4860
|
+
{ $unwind: '$datesInWeek' },
|
|
4861
|
+
{ $group: {
|
|
4862
|
+
_id: { client_id: '$client_id', store_id: '$store_id', sourceCheckList_id: '$sourceCheckList_id' },
|
|
4863
|
+
storeName: { $last: '$storeName' },
|
|
4864
|
+
checkListName: { $last: '$checkListName' },
|
|
4865
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4866
|
+
} },
|
|
4867
|
+
{ $project: {
|
|
4868
|
+
_id: 1,
|
|
4869
|
+
storeName: 1,
|
|
4870
|
+
checkListName: 1,
|
|
4871
|
+
count: { $size: '$uniqueDates' },
|
|
4872
|
+
} },
|
|
4873
|
+
] );
|
|
4874
|
+
const map = new Map();
|
|
4875
|
+
for ( const r of rows ) {
|
|
4876
|
+
const key = `${r._id.client_id}::${r._id.store_id}::${r._id.sourceCheckList_id}`;
|
|
4877
|
+
map.set( key, {
|
|
4878
|
+
client_id: r._id.client_id,
|
|
4879
|
+
store_id: r._id.store_id,
|
|
4880
|
+
sourceCheckList_id: String( r._id.sourceCheckList_id || '' ),
|
|
4881
|
+
storeName: r.storeName,
|
|
4882
|
+
checkListName: r.checkListName,
|
|
4883
|
+
count: r.count,
|
|
4884
|
+
} );
|
|
4885
|
+
}
|
|
4886
|
+
return map;
|
|
4887
|
+
}
|
|
4888
|
+
|
|
4889
|
+
// Resolves the set of storeIds a user can see. Returns:
|
|
4890
|
+
// null — full access (superadmin or non-client userType); caller should not filter
|
|
4891
|
+
// Set<storeId> — restricted access; iterate this user's reachable stores
|
|
4892
|
+
// Mirrors the existing pattern at internalTrax.controller.js ~L4192 (assignedStores + clusters where the
|
|
4893
|
+
// user is a Teamlead + teams the user leads + teams the user is a member of, all expanded to storeIds).
|
|
4894
|
+
async function resolveUserAssignedStores( userDetails ) {
|
|
4895
|
+
if ( !userDetails ) return new Set();
|
|
4896
|
+
if ( userDetails.userType !== 'client' || userDetails.role === 'superadmin' ) {
|
|
4897
|
+
return null;
|
|
4898
|
+
}
|
|
4899
|
+
const storeIds = new Set( ( userDetails.assignedStores || [] ).map( ( s ) => s?.storeId ).filter( Boolean ) );
|
|
4900
|
+
|
|
4901
|
+
const [ leadClusters, leadTeams ] = await Promise.all( [
|
|
4902
|
+
clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
|
|
4903
|
+
teamsServices.findteams( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
|
|
4904
|
+
] );
|
|
4905
|
+
|
|
4906
|
+
for ( const cluster of leadClusters || [] ) {
|
|
4907
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4908
|
+
}
|
|
4909
|
+
|
|
4910
|
+
// Teams the user leads — pull each member's assignedStores + clusters where THAT member is Teamlead.
|
|
4911
|
+
for ( const team of leadTeams || [] ) {
|
|
4912
|
+
for ( const member of team.users || [] ) {
|
|
4913
|
+
const memberDetails = await userService.findOne( { _id: member.userId } );
|
|
4914
|
+
if ( memberDetails?.assignedStores?.length ) {
|
|
4915
|
+
memberDetails.assignedStores.forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4916
|
+
}
|
|
4917
|
+
if ( memberDetails?.email ) {
|
|
4918
|
+
const memberClusters = await clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: memberDetails.email } } } );
|
|
4919
|
+
for ( const cluster of memberClusters || [] ) {
|
|
4920
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4921
|
+
}
|
|
4922
|
+
}
|
|
4923
|
+
}
|
|
4924
|
+
}
|
|
4925
|
+
|
|
4926
|
+
// Teams the user is a member of — pull clusters that reference those teams.
|
|
4927
|
+
const memberTeams = await teamsServices.findteams( { clientId: userDetails.clientId, users: { $elemMatch: { email: userDetails.email } } } );
|
|
4928
|
+
for ( const team of memberTeams || [] ) {
|
|
4929
|
+
const clusters = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
|
|
4930
|
+
for ( const cluster of clusters || [] ) {
|
|
4931
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4932
|
+
}
|
|
4933
|
+
}
|
|
4934
|
+
|
|
4935
|
+
// Teams the user leads — also pull clusters that reference those teams.
|
|
4936
|
+
for ( const team of leadTeams || [] ) {
|
|
4937
|
+
const clusters = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
|
|
4938
|
+
for ( const cluster of clusters || [] ) {
|
|
4939
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4940
|
+
}
|
|
4941
|
+
}
|
|
4942
|
+
|
|
4943
|
+
return storeIds;
|
|
4944
|
+
}
|
|
4945
|
+
|
|
4946
|
+
export async function weeklyWrapAlert( req, res ) {
|
|
4947
|
+
try {
|
|
4948
|
+
const range = getLastWeekRange();
|
|
4949
|
+
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4950
|
+
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4951
|
+
|
|
4952
|
+
// Only send to clients that opted in via weeklyFlagEmail. Build the allow-list once.
|
|
4953
|
+
const enabledClients = await clientService.find( { weeklyFlagEmail: true }, { clientId: 1 } );
|
|
4954
|
+
console.log( enabledClients );
|
|
4955
|
+
if ( !enabledClients.length ) {
|
|
4956
|
+
return res.sendSuccess( { message: 'No clients configured for weeklyFlagEmail', sent: [] } );
|
|
4957
|
+
}
|
|
4958
|
+
const enabledClientIds = new Set( enabledClients.map( ( c ) => String( c.clientId ) ) );
|
|
4959
|
+
|
|
4960
|
+
// Aggregate flag data for both weeks across all stores in one shot. The per-(store, checklist)
|
|
4961
|
+
// maps drive the Excel attachment rows; the per-store maps drive the email body's totals + top picks.
|
|
4962
|
+
const [ flagsThis, flagsPrev, recurThis, recurPrev, flagsThisCL, recurThisCL ] = await Promise.all( [
|
|
4963
|
+
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4964
|
+
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4965
|
+
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4966
|
+
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4967
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4968
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4969
|
+
] );
|
|
4970
|
+
|
|
4971
|
+
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4972
|
+
const compiled = handlebars.compile( fileContent );
|
|
4973
|
+
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4974
|
+
const sentSummary = [];
|
|
4975
|
+
|
|
4976
|
+
// Build one digest per client present in the data, restricted to clients that opted in.
|
|
4977
|
+
const clientIds = new Set();
|
|
4978
|
+
for ( const v of flagsThis.values() ) {
|
|
4979
|
+
if ( enabledClientIds.has( String( v.client_id ) ) ) clientIds.add( v.client_id );
|
|
4980
|
+
}
|
|
4981
|
+
for ( const v of recurThis.values() ) {
|
|
4982
|
+
if ( enabledClientIds.has( String( v.client_id ) ) ) clientIds.add( v.client_id );
|
|
4983
|
+
}
|
|
4984
|
+
|
|
4985
|
+
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4986
|
+
try {
|
|
4987
|
+
// Recipients = admin + superadmin users for this client. Each user's digest is scoped via
|
|
4988
|
+
// resolveUserAssignedStores — superadmin / tango users see everything, regular admins see only
|
|
4989
|
+
// stores they own (assignedStores + clusters they lead + teams they're in/lead).
|
|
4990
|
+
const adminUsers = await userService.find(
|
|
4991
|
+
{ clientId: String( clientId ), role: { $in: [ 'admin', 'superadmin' ] } },
|
|
4992
|
+
{ email: 1, assignedStores: 1, userName: 1, userType: 1, role: 1, clientId: 1 },
|
|
4993
|
+
);
|
|
4994
|
+
console.log( adminUsers );
|
|
4995
|
+
if ( !adminUsers.length ) {
|
|
4996
|
+
logger.info( `[weeklyWrapAlert] no admin/superadmin users for client ${clientId}, skipping` );
|
|
4997
|
+
return;
|
|
4998
|
+
}
|
|
4999
|
+
|
|
5000
|
+
for ( const user of adminUsers ) {
|
|
5001
|
+
try {
|
|
5002
|
+
if ( !user?.email ) continue;
|
|
5003
|
+
const allowed = await resolveUserAssignedStores( user );
|
|
5004
|
+
// null = full access (superadmin / tango). Empty set = no reachable stores → skip.
|
|
5005
|
+
if ( allowed && allowed.size === 0 ) {
|
|
5006
|
+
logger.info( `[weeklyWrapAlert] user ${user.email} has no reachable stores, skipping` );
|
|
5007
|
+
continue;
|
|
5008
|
+
}
|
|
5009
|
+
|
|
5010
|
+
// Iterate stores belonging to this client; intersect with the user's allowed set when present.
|
|
5011
|
+
const clientPrefix = `${clientId}::`;
|
|
5012
|
+
const isAllowed = ( storeId ) => allowed === null || allowed.has( storeId );
|
|
5013
|
+
const clientStoreKeys = new Set();
|
|
5014
|
+
for ( const k of flagsThis.keys() ) {
|
|
5015
|
+
if ( k.startsWith( clientPrefix ) && isAllowed( k.slice( clientPrefix.length ) ) ) clientStoreKeys.add( k );
|
|
5016
|
+
}
|
|
5017
|
+
for ( const k of flagsPrev.keys() ) {
|
|
5018
|
+
if ( k.startsWith( clientPrefix ) && isAllowed( k.slice( clientPrefix.length ) ) ) clientStoreKeys.add( k );
|
|
5019
|
+
}
|
|
5020
|
+
for ( const k of recurThis.keys() ) {
|
|
5021
|
+
if ( k.startsWith( clientPrefix ) && isAllowed( k.slice( clientPrefix.length ) ) ) clientStoreKeys.add( k );
|
|
5022
|
+
}
|
|
5023
|
+
|
|
5024
|
+
let storesFlagged = 0;
|
|
5025
|
+
let totalFlags = 0;
|
|
5026
|
+
let prevTotalFlags = 0;
|
|
5027
|
+
let prevStoresFlagged = 0;
|
|
5028
|
+
const flaggedChecklistsThis = new Set();
|
|
5029
|
+
const flaggedChecklistsPrev = new Set();
|
|
5030
|
+
let questionFlags = 0;
|
|
5031
|
+
let notSubmittedFlags = 0;
|
|
5032
|
+
let runAIFlags = 0;
|
|
5033
|
+
let recurringFlags = 0;
|
|
5034
|
+
const excelRows = [];
|
|
5035
|
+
// Track top flagged + top recurring within this user's assigned stores only.
|
|
5036
|
+
let topFlag = null;
|
|
5037
|
+
let topRecurring = null;
|
|
5038
|
+
|
|
5039
|
+
for ( const k of clientStoreKeys ) {
|
|
5040
|
+
const cur = flagsThis.get( k );
|
|
5041
|
+
const prev = flagsPrev.get( k );
|
|
5042
|
+
const recCur = recurThis.get( k );
|
|
5043
|
+
const recPrev = recurPrev.get( k );
|
|
5044
|
+
const storeRecurring = recCur?.count || 0;
|
|
5045
|
+
const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
|
|
5046
|
+
const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
|
|
5047
|
+
|
|
5048
|
+
if ( storeTotal > 0 ) storesFlagged += 1;
|
|
5049
|
+
if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
|
|
5050
|
+
totalFlags += storeTotal;
|
|
5051
|
+
prevTotalFlags += prevStoreTotal;
|
|
5052
|
+
questionFlags += cur?.questionFlag || 0;
|
|
5053
|
+
notSubmittedFlags += cur?.timeFlag || 0;
|
|
5054
|
+
runAIFlags += cur?.runAIFlag || 0;
|
|
5055
|
+
recurringFlags += storeRecurring;
|
|
5056
|
+
|
|
5057
|
+
( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
|
|
5058
|
+
( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
|
|
5059
|
+
|
|
5060
|
+
if ( cur && ( !topFlag || ( cur.totalFlags || 0 ) > topFlag.value ) ) {
|
|
5061
|
+
topFlag = { storeId: cur.store_id, storeName: cur.storeName, value: cur.totalFlags || 0 };
|
|
5062
|
+
}
|
|
5063
|
+
if ( recCur && ( !topRecurring || ( recCur.count || 0 ) > topRecurring.value ) ) {
|
|
5064
|
+
topRecurring = { storeId: recCur.store_id, storeName: recCur.storeName, value: recCur.count || 0 };
|
|
5065
|
+
}
|
|
5066
|
+
}
|
|
5067
|
+
|
|
5068
|
+
if ( totalFlags === 0 && prevTotalFlags === 0 ) {
|
|
5069
|
+
continue; // user has nothing to report this week
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
// Build the Excel attachment as one row per (store, checklist) the user can see.
|
|
5073
|
+
// Pulls from the per-checklist flag aggregation + per-checklist recurring email count.
|
|
5074
|
+
const seenStoreChecklist = new Set();
|
|
5075
|
+
for ( const k of flagsThisCL.keys() ) {
|
|
5076
|
+
if ( !k.startsWith( clientPrefix ) ) continue;
|
|
5077
|
+
const cur = flagsThisCL.get( k );
|
|
5078
|
+
if ( !isAllowed( cur.store_id ) ) continue;
|
|
5079
|
+
const recCur = recurThisCL.get( k );
|
|
5080
|
+
const recurringCount = recCur?.count || 0;
|
|
5081
|
+
const flags = ( cur.questionFlag || 0 ) + ( cur.timeFlag || 0 ) + ( cur.runAIFlag || 0 ) + recurringCount;
|
|
5082
|
+
if ( flags === 0 ) continue;
|
|
5083
|
+
excelRows.push( {
|
|
5084
|
+
storeName: cur.storeName || '',
|
|
5085
|
+
checkListName: cur.checkListName || '',
|
|
5086
|
+
flags,
|
|
5087
|
+
questionFlag: cur.questionFlag || 0,
|
|
5088
|
+
timeFlag: cur.timeFlag || 0,
|
|
5089
|
+
runAIFlag: cur.runAIFlag || 0,
|
|
5090
|
+
recurringFlag: recurringCount,
|
|
5091
|
+
} );
|
|
5092
|
+
seenStoreChecklist.add( k );
|
|
5093
|
+
}
|
|
5094
|
+
// (store, checklist) pairs that hit recurring threshold but had no flag aggregation entry.
|
|
5095
|
+
for ( const k of recurThisCL.keys() ) {
|
|
5096
|
+
if ( !k.startsWith( clientPrefix ) ) continue;
|
|
5097
|
+
if ( seenStoreChecklist.has( k ) ) continue;
|
|
5098
|
+
const recCur = recurThisCL.get( k );
|
|
5099
|
+
if ( !isAllowed( recCur.store_id ) ) continue;
|
|
5100
|
+
if ( !recCur.count ) continue;
|
|
5101
|
+
excelRows.push( {
|
|
5102
|
+
storeName: recCur.storeName || '',
|
|
5103
|
+
checkListName: recCur.checkListName || '',
|
|
5104
|
+
flags: recCur.count,
|
|
5105
|
+
questionFlag: 0,
|
|
5106
|
+
timeFlag: 0,
|
|
5107
|
+
runAIFlag: 0,
|
|
5108
|
+
recurringFlag: recCur.count,
|
|
5109
|
+
} );
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
let highestFlaggedStoreWow = { value: '', direction: 'up' };
|
|
5113
|
+
if ( topFlag ) {
|
|
5114
|
+
const prev = flagsPrev.get( `${clientId}::${topFlag.storeId}` );
|
|
5115
|
+
highestFlaggedStoreWow = computeWow( topFlag.value, prev?.totalFlags || 0 );
|
|
5116
|
+
}
|
|
5117
|
+
let highestRecurringStoreWow = { value: '', direction: 'up' };
|
|
5118
|
+
if ( topRecurring ) {
|
|
5119
|
+
const prev = recurPrev.get( `${clientId}::${topRecurring.storeId}` );
|
|
5120
|
+
highestRecurringStoreWow = computeWow( topRecurring.value, prev?.count || 0 );
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
const excelHeader = `Weekly Summary ${startDateLabel} - ${endDateLabel}`;
|
|
5124
|
+
const buf = await buildWeeklyWrapExcel( excelHeader, excelRows );
|
|
5125
|
+
const attachmentBuffer = Buffer.from( buf );
|
|
5126
|
+
const sizeKb = Math.max( 1, Math.round( attachmentBuffer.length / 1024 ) );
|
|
5127
|
+
|
|
5128
|
+
const data = {
|
|
5129
|
+
startDate: startDateLabel,
|
|
5130
|
+
endDate: endDateLabel,
|
|
5131
|
+
storesFlagged,
|
|
5132
|
+
storesFlaggedWow: computeWow( storesFlagged, prevStoresFlagged ),
|
|
5133
|
+
totalFlags,
|
|
5134
|
+
totalFlagsWow: computeWow( totalFlags, prevTotalFlags ),
|
|
5135
|
+
checklistFlags: flaggedChecklistsThis.size,
|
|
5136
|
+
checklistFlagsWow: computeWow( flaggedChecklistsThis.size, flaggedChecklistsPrev.size ),
|
|
5137
|
+
questionFlags,
|
|
5138
|
+
notSubmittedFlags,
|
|
5139
|
+
runAIFlags,
|
|
5140
|
+
recurringFlags,
|
|
5141
|
+
highestFlaggedStore: topFlag?.storeName || topFlag?.storeId || '--',
|
|
5142
|
+
highestFlaggedStoreWow,
|
|
5143
|
+
highestRecurringStore: topRecurring?.storeName || topRecurring?.storeId || '--',
|
|
5144
|
+
highestRecurringStoreWow,
|
|
5145
|
+
attachmentName: `${excelHeader}.xlsx`,
|
|
5146
|
+
attachmentSize: `${sizeKb} KB`,
|
|
5147
|
+
domain: flagDomain,
|
|
5148
|
+
};
|
|
5149
|
+
|
|
5150
|
+
const html = compiled( { data } );
|
|
5151
|
+
const attachment = {
|
|
5152
|
+
filename: `${excelHeader}.xlsx`,
|
|
5153
|
+
content: attachmentBuffer,
|
|
5154
|
+
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
5155
|
+
};
|
|
5156
|
+
const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
|
|
5157
|
+
const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
|
|
5158
|
+
|
|
5159
|
+
sendEmailWithSES( user.email, subject, html, attachment, sourceEmail );
|
|
5160
|
+
sentSummary.push( { recipient: user.email, clientId, totalFlags } );
|
|
5161
|
+
} catch ( e ) {
|
|
5162
|
+
logger.error( { functionName: 'weeklyWrapAlert.user', clientId, email: user?.email, error: e } );
|
|
5163
|
+
}
|
|
5164
|
+
}
|
|
5165
|
+
} catch ( e ) {
|
|
5166
|
+
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
5167
|
+
}
|
|
5168
|
+
} ) );
|
|
5169
|
+
|
|
5170
|
+
return res.sendSuccess( { message: 'Weekly wrap dispatched', range, sent: sentSummary } );
|
|
5171
|
+
} catch ( e ) {
|
|
5172
|
+
logger.error( { functionName: 'weeklyWrapAlert', error: e } );
|
|
5173
|
+
return res.sendError( e, 500 );
|
|
5174
|
+
}
|
|
5175
|
+
}
|
|
5176
|
+
|
|
4425
5177
|
export async function updateStoreLatLong( req, res ) {
|
|
4426
5178
|
try {
|
|
4427
|
-
const defaultStores = [
|
|
4428
|
-
{ 'storeName': 'OFLBTM', 'lat': 12.9059052, 'long': 77.6057203 },
|
|
4429
|
-
{ 'storeName': 'OFLRRN', 'lat': 12.9096852, 'long': 77.5135813 },
|
|
4430
|
-
{ 'storeName': 'OFLGUN', 'lat': 12.9286282, 'long': 77.738055 },
|
|
4431
|
-
{ 'storeName': 'OFLJPN', 'lat': 12.8915891, 'long': 77.5776564 },
|
|
4432
|
-
{ 'storeName': 'OFLAECS', 'lat': 12.9638564, 'long': 77.7125467 },
|
|
4433
|
-
{ 'storeName': 'Seegehalli', 'lat': 13.0083064, 'long': 77.7588426 },
|
|
4434
|
-
{ 'storeName': 'OFLJPNS', 'lat': 12.8695409, 'long': 77.5820004 },
|
|
4435
|
-
{ 'storeName': 'OFLHAR', 'lat': 12.9136122, 'long': 77.6649999 },
|
|
4436
|
-
{ 'storeName': 'OFLKSNR', 'lat': 13.0051196, 'long': 77.6601987 },
|
|
4437
|
-
{ 'storeName': 'Hosa Road', 'lat': 12.8792369, 'long': 77.6721843 },
|
|
4438
|
-
{ 'storeName': 'OFLDEV', 'lat': 12.8942057, 'long': 77.6019696 },
|
|
4439
|
-
{ 'storeName': 'OFLAYN', 'lat': 12.958161, 'long': 77.570055 },
|
|
4440
|
-
{ 'storeName': 'OFLKAG', 'lat': 12.9845196, 'long': 77.6758945 },
|
|
4441
|
-
{ 'storeName': 'OFLBVR', 'lat': 12.9858428, 'long': 77.5424725 },
|
|
4442
|
-
{ 'storeName': 'OFLMTK', 'lat': 13.0279313, 'long': 77.5587934 },
|
|
4443
|
-
{ 'storeName': 'OFLMAG', 'lat': 12.9837929, 'long': 77.5325324 },
|
|
4444
|
-
{ 'storeName': 'OFLBEL', 'lat': 13.0370029, 'long': 77.5622911 },
|
|
4445
|
-
{ 'storeName': 'OFLKDU', 'lat': 12.8835726, 'long': 77.517709 },
|
|
4446
|
-
{ 'storeName': 'OFLAMLI', 'lat': 13.0685032, 'long': 77.5972815 },
|
|
4447
|
-
{ 'storeName': 'OFLSAN', 'lat': 19.0603798, 'long': 73.0041633 },
|
|
4448
|
-
{ 'storeName': 'OFLLOK', 'lat': 19.1471544, 'long': 72.5405682 },
|
|
4449
|
-
{ 'storeName': 'OFLTSTG', 'lat': 19.2471119, 'long': 72.9769107 },
|
|
4450
|
-
{ 'storeName': 'OFLKHGRS', 'lat': 19.0583611, 'long': 73.0584353 },
|
|
4451
|
-
{ 'storeName': 'OFLMAL', 'lat': 12.9673877, 'long': 77.499519 },
|
|
4452
|
-
{ 'storeName': 'OFLBAG', 'lat': 13.1218427, 'long': 77.6234015 },
|
|
4453
|
-
{ 'storeName': 'OFLYEL', 'lat': 13.114551, 'long': 77.5401312 },
|
|
4454
|
-
{ 'storeName': 'OFLTCP', 'lat': 13.0240695, 'long': 77.6928401 },
|
|
4455
|
-
{ 'storeName': 'OFLECTN', 'lat': 12.8185795, 'long': 77.6520784 },
|
|
4456
|
-
{ 'storeName': 'OFLHBL', 'lat': 13.0559812, 'long': 77.593941 },
|
|
4457
|
-
{ 'storeName': 'OFLBWR', 'lat': 12.9691559, 'long': 77.739599 },
|
|
4458
|
-
{ 'storeName': 'OFLHSR', 'lat': 12.9183666, 'long': 77.5793797 },
|
|
4459
|
-
];
|
|
5179
|
+
const defaultStores = [];
|
|
4460
5180
|
|
|
4461
5181
|
const list = Array.isArray( req.body?.stores ) && req.body.stores.length ? req.body.stores : defaultStores;
|
|
4462
5182
|
|
|
@@ -4506,3 +5226,4 @@ export async function updateStoreLatLong( req, res ) {
|
|
|
4506
5226
|
return res.sendError( e, 500 );
|
|
4507
5227
|
}
|
|
4508
5228
|
}
|
|
5229
|
+
|