tango-app-api-trax 3.8.23 → 3.8.24-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 +92 -28
- package/src/controllers/handlebar-helper.js +1 -0
- package/src/controllers/internalTrax.controller.js +651 -198
- package/src/controllers/mobileTrax.controller.js +254 -171
- 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 +12 -5
- package/src/hbs/visit-checklist.hbs +82 -38
- package/src/routes/internalTraxApi.router.js +1 -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 };
|
|
@@ -2643,6 +2643,9 @@ export async function updateRunAI( req, res ) {
|
|
|
2643
2643
|
if ( !req.body.qName ) {
|
|
2644
2644
|
return res.sendError( 'Question name is required', 400 );
|
|
2645
2645
|
}
|
|
2646
|
+
if ( !req.body.userAnswer ) {
|
|
2647
|
+
return res.sendError( 'answer is required', 400 );
|
|
2648
|
+
}
|
|
2646
2649
|
let getDetails = await processedchecklist.findOne( { _id: req.body.id } );
|
|
2647
2650
|
if ( !getDetails ) {
|
|
2648
2651
|
return res.sendError( 'No data found', 204 );
|
|
@@ -2651,7 +2654,7 @@ export async function updateRunAI( req, res ) {
|
|
|
2651
2654
|
let updateData = {};
|
|
2652
2655
|
|
|
2653
2656
|
for ( let k of Object.keys( req.body.data ) ) {
|
|
2654
|
-
let keyPath = `questionAnswers.$[section].questions.$[question].userAnswer
|
|
2657
|
+
let keyPath = `questionAnswers.$[section].questions.$[question].userAnswer.$[userAnswer].${k}`;
|
|
2655
2658
|
updateData[keyPath] = req.body.data[k];
|
|
2656
2659
|
}
|
|
2657
2660
|
|
|
@@ -2662,7 +2665,9 @@ export async function updateRunAI( req, res ) {
|
|
|
2662
2665
|
arrayFilters: [
|
|
2663
2666
|
{ 'section.section_id': new ObjectId( req.body.sectionId ) },
|
|
2664
2667
|
{ 'question.qname': req.body.qName },
|
|
2668
|
+
{ 'userAnswer.answer': req.body.userAnswer },
|
|
2665
2669
|
],
|
|
2670
|
+
strict: false,
|
|
2666
2671
|
},
|
|
2667
2672
|
);
|
|
2668
2673
|
return res.sendSuccess( 'RunAI details updated successfully' );
|
|
@@ -2697,6 +2702,113 @@ export async function countUpdateRunAI( req, res ) {
|
|
|
2697
2702
|
}
|
|
2698
2703
|
}
|
|
2699
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
|
+
|
|
2700
2812
|
export async function getRunAIQuestions( req, res ) {
|
|
2701
2813
|
try {
|
|
2702
2814
|
let requestData = req.body;
|
|
@@ -3747,7 +3859,6 @@ export async function checklistAutoMailList( req, res ) {
|
|
|
3747
3859
|
}
|
|
3748
3860
|
}
|
|
3749
3861
|
|
|
3750
|
-
|
|
3751
3862
|
export const downloadInsertPdfOld = async ( req, res ) => {
|
|
3752
3863
|
try {
|
|
3753
3864
|
setImmediate( async () => {
|
|
@@ -4225,17 +4336,18 @@ export async function getEyetestStream( req, res ) {
|
|
|
4225
4336
|
}
|
|
4226
4337
|
}
|
|
4227
4338
|
|
|
4228
|
-
function buildRecurringFlagExcel( rows ) {
|
|
4339
|
+
function buildRecurringFlagExcel( rows, subjectLabel = 'Store' ) {
|
|
4229
4340
|
const workbook = new ExcelJS.Workbook();
|
|
4230
4341
|
const sheet = workbook.addWorksheet( 'Recurring Flags' );
|
|
4231
4342
|
sheet.columns = [
|
|
4232
|
-
{ header:
|
|
4343
|
+
{ header: `${subjectLabel} Name`, key: 'storeName', width: 25 },
|
|
4233
4344
|
{ header: 'Checklist Name', key: 'checklistName', width: 30 },
|
|
4234
4345
|
{ header: 'Section', key: 'sectionName', width: 25 },
|
|
4235
4346
|
{ header: 'Question', key: 'questionName', width: 40 },
|
|
4236
4347
|
{ header: 'Last Submitted By', key: 'lastSubmittedBy', width: 25 },
|
|
4237
4348
|
{ header: 'Last Submission Date', key: 'lastSubmissionDate', width: 22 },
|
|
4238
4349
|
{ header: 'Recurring Days', key: 'days', width: 16 },
|
|
4350
|
+
{ header: 'Run AI Flags', key: 'runAICount', width: 14 },
|
|
4239
4351
|
];
|
|
4240
4352
|
sheet.getRow( 1 ).font = { bold: true };
|
|
4241
4353
|
rows.forEach( ( r ) => sheet.addRow( r ) );
|
|
@@ -4253,7 +4365,6 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4253
4365
|
],
|
|
4254
4366
|
},
|
|
4255
4367
|
}, { _id: 1, checkListName: 1, recurringFlag: 1, notifyFlags: 1, approver: 1, client_id: 1 } );
|
|
4256
|
-
|
|
4257
4368
|
if ( !checklistDetails.length ) {
|
|
4258
4369
|
return res.sendSuccess( 'No checklists configured for recurring flag' );
|
|
4259
4370
|
}
|
|
@@ -4266,7 +4377,6 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4266
4377
|
const threshold = cl?.recurringFlag?.threshold || 3;
|
|
4267
4378
|
const notifyType = cl?.recurringFlag?.notifyType || [];
|
|
4268
4379
|
const users = cl?.recurringFlag?.users || [];
|
|
4269
|
-
|
|
4270
4380
|
let recipients = [];
|
|
4271
4381
|
if ( notifyType.includes( 'sameAsNotify' ) ) {
|
|
4272
4382
|
const nfType = cl?.notifyFlags?.notifyType || [];
|
|
@@ -4275,42 +4385,95 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4275
4385
|
recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
|
|
4276
4386
|
}
|
|
4277
4387
|
recipients = [ ...recipients, ...nfUsers.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4278
|
-
} else {
|
|
4279
|
-
if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
|
|
4280
|
-
recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
|
|
4281
|
-
}
|
|
4282
|
-
recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4283
4388
|
}
|
|
4389
|
+
if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
|
|
4390
|
+
recipients = [ ...recipients, ...cl.approver.map( ( a ) => a?.value ).filter( Boolean ) ];
|
|
4391
|
+
}
|
|
4392
|
+
recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4284
4393
|
recipients = [ ...new Set( recipients ) ];
|
|
4285
4394
|
|
|
4286
4395
|
if ( !recipients.length ) return;
|
|
4287
4396
|
|
|
4288
|
-
//
|
|
4289
|
-
//
|
|
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).
|
|
4290
4413
|
const trackerRows = await recurringFlagTracker.find( {
|
|
4291
4414
|
sourceCheckList_id: cl._id,
|
|
4292
|
-
|
|
4293
|
-
|
|
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
|
+
],
|
|
4294
4425
|
} );
|
|
4295
4426
|
|
|
4296
4427
|
for ( const t of trackerRows ) {
|
|
4297
|
-
|
|
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;
|
|
4298
4441
|
triggers.push( {
|
|
4299
4442
|
recipient,
|
|
4300
4443
|
clientId: t.client_id,
|
|
4301
|
-
|
|
4302
|
-
|
|
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 || '',
|
|
4303
4452
|
checklistId: cl._id.toString(),
|
|
4304
4453
|
checklistName: cl.checkListName?.trim() || t.checkListName || '',
|
|
4305
4454
|
sectionName: t.sectionName,
|
|
4306
4455
|
qno: t.qno,
|
|
4307
4456
|
qname: t.qname,
|
|
4308
4457
|
days: t.consecutiveCount,
|
|
4458
|
+
runAICount: t.runAICount || 0,
|
|
4459
|
+
sopFired,
|
|
4460
|
+
runAIFired,
|
|
4309
4461
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4310
4462
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4311
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
|
+
} );
|
|
4312
4476
|
}
|
|
4313
|
-
trackerIdsToReset.push( { _id: t._id, lastFlaggedDate: t.lastFlaggedDate } );
|
|
4314
4477
|
}
|
|
4315
4478
|
} ) );
|
|
4316
4479
|
|
|
@@ -4318,6 +4481,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4318
4481
|
return res.sendSuccess( 'No recurring flags reached threshold' );
|
|
4319
4482
|
}
|
|
4320
4483
|
|
|
4484
|
+
|
|
4321
4485
|
// Group triggers by recipient.
|
|
4322
4486
|
const byRecipient = new Map();
|
|
4323
4487
|
for ( const t of triggers ) {
|
|
@@ -4332,21 +4496,77 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4332
4496
|
const sentSummary = [];
|
|
4333
4497
|
|
|
4334
4498
|
await Promise.all( [ ...byRecipient.entries() ].map( async ( [ recipient, items ] ) => {
|
|
4335
|
-
const
|
|
4499
|
+
const subjects = new Set( items.map( ( i ) => i.subjectId ) );
|
|
4336
4500
|
const checklists = new Set( items.map( ( i ) => i.checklistId ) );
|
|
4337
|
-
const isMultiStore =
|
|
4501
|
+
const isMultiStore = subjects.size > 1; // "isMultiStore" name retained for template back-compat
|
|
4338
4502
|
const isMultiChecklist = !isMultiStore && checklists.size > 1;
|
|
4339
|
-
//
|
|
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.
|
|
4340
4507
|
const thresholdShown = items.reduce( ( acc, it ) => Math.min( acc, it.days ), items[0].days );
|
|
4341
4508
|
|
|
4342
|
-
|
|
4343
|
-
|
|
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,
|
|
4344
4563
|
checklistName: i.checklistName,
|
|
4345
4564
|
sectionName: i.sectionName,
|
|
4346
4565
|
questionName: i.qname,
|
|
4347
4566
|
lastSubmittedBy: i.lastSubmittedBy,
|
|
4348
4567
|
lastSubmissionDate: i.lastSubmissionDate,
|
|
4349
4568
|
days: i.days,
|
|
4569
|
+
runAICount: i.runAICount || 0,
|
|
4350
4570
|
} ) );
|
|
4351
4571
|
|
|
4352
4572
|
const ATTACHMENT_THRESHOLD = 10;
|
|
@@ -4357,31 +4577,43 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4357
4577
|
threshold: thresholdShown,
|
|
4358
4578
|
isMultiStore,
|
|
4359
4579
|
isMultiChecklist,
|
|
4580
|
+
isMultiStoreSingleChecklist,
|
|
4581
|
+
isUserCoverage: isAllUser,
|
|
4360
4582
|
showTable: isMultiStore || isMultiChecklist,
|
|
4361
4583
|
hasAttachment,
|
|
4362
4584
|
domain: flagDomain,
|
|
4363
4585
|
rows: displayRows,
|
|
4586
|
+
subjectLabel,
|
|
4587
|
+
subjectLabelPlural,
|
|
4588
|
+
subjectLabelLower,
|
|
4589
|
+
subjectLabelPluralLower,
|
|
4364
4590
|
};
|
|
4365
4591
|
|
|
4366
4592
|
if ( isMultiStore ) {
|
|
4367
4593
|
data.highlights = {
|
|
4368
|
-
|
|
4594
|
+
totalSubjects: subjects.size,
|
|
4595
|
+
totalStores: subjects.size, // legacy alias
|
|
4369
4596
|
totalChecklists: checklists.size,
|
|
4370
4597
|
totalFlags: items.length,
|
|
4371
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
|
+
}
|
|
4372
4603
|
} else if ( isMultiChecklist ) {
|
|
4373
|
-
data.
|
|
4604
|
+
data.subjectName = items[0].subjectName;
|
|
4605
|
+
data.storeName = items[0].subjectName;
|
|
4374
4606
|
} else {
|
|
4375
|
-
|
|
4376
|
-
|
|
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;
|
|
4377
4611
|
data.checklistName = single.checklistName;
|
|
4378
|
-
data.questionName = single.qname;
|
|
4379
4612
|
data.lastSubmittedBy = single.lastSubmittedBy;
|
|
4380
4613
|
data.lastSubmissionDate = single.lastSubmissionDate;
|
|
4381
|
-
data.
|
|
4382
|
-
data.
|
|
4383
|
-
data.
|
|
4384
|
-
data.flagCountPlural = false;
|
|
4614
|
+
data.flagCount = single.flagCount;
|
|
4615
|
+
data.runAICount = single.runAICount;
|
|
4616
|
+
data.totalFlags = single.totalFlags;
|
|
4385
4617
|
}
|
|
4386
4618
|
|
|
4387
4619
|
const html = compiled( { data } );
|
|
@@ -4396,7 +4628,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4396
4628
|
|
|
4397
4629
|
if ( hasAttachment ) {
|
|
4398
4630
|
try {
|
|
4399
|
-
const buf = await buildRecurringFlagExcel(
|
|
4631
|
+
const buf = await buildRecurringFlagExcel( excelRows, subjectLabel );
|
|
4400
4632
|
params.attachment = {
|
|
4401
4633
|
filename: 'Recurring-Flags-Summary.xlsx',
|
|
4402
4634
|
content: Buffer.from( buf ),
|
|
@@ -4406,22 +4638,31 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4406
4638
|
logger.error( { functionName: 'recurringFlagAlert.buildExcel', error: e } );
|
|
4407
4639
|
}
|
|
4408
4640
|
}
|
|
4409
|
-
|
|
4410
4641
|
sendEmailWithSES( params.toEmail, params.mailSubject, params.htmlBody, params.attachment, params.sourceEmail );
|
|
4411
4642
|
sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
|
|
4412
4643
|
} ) );
|
|
4413
4644
|
|
|
4414
|
-
// 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).
|
|
4415
4647
|
if ( trackerIdsToReset.length ) {
|
|
4416
|
-
const resetOps = trackerIdsToReset.map( (
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
4421
|
-
|
|
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
|
+
},
|
|
4422
4663
|
},
|
|
4423
|
-
}
|
|
4424
|
-
} )
|
|
4664
|
+
};
|
|
4665
|
+
} );
|
|
4425
4666
|
await recurringFlagTracker.bulkWrite( resetOps );
|
|
4426
4667
|
}
|
|
4427
4668
|
|
|
@@ -4437,10 +4678,12 @@ function getLastWeekRange( ref = dayjs() ) {
|
|
|
4437
4678
|
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4438
4679
|
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4439
4680
|
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4440
|
-
|
|
4441
|
-
const
|
|
4442
|
-
const
|
|
4443
|
-
|
|
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' );
|
|
4444
4687
|
return {
|
|
4445
4688
|
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4446
4689
|
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
@@ -4462,7 +4705,7 @@ function computeWow( current, previous ) {
|
|
|
4462
4705
|
|
|
4463
4706
|
async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
4464
4707
|
const rows = await processedchecklist.aggregate( [
|
|
4465
|
-
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd },
|
|
4708
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, checkListType: 'custom' } },
|
|
4466
4709
|
{ $group: {
|
|
4467
4710
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4468
4711
|
storeName: { $last: '$storeName' },
|
|
@@ -4502,26 +4745,33 @@ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
|
4502
4745
|
}
|
|
4503
4746
|
|
|
4504
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.
|
|
4505
4751
|
const rows = await recurringFlagTracker.aggregate( [
|
|
4506
4752
|
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4507
4753
|
{ $project: {
|
|
4508
4754
|
client_id: 1,
|
|
4509
4755
|
store_id: 1,
|
|
4510
4756
|
storeName: 1,
|
|
4511
|
-
|
|
4512
|
-
$
|
|
4513
|
-
$
|
|
4514
|
-
|
|
4515
|
-
|
|
4516
|
-
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4517
|
-
},
|
|
4757
|
+
datesInWeek: {
|
|
4758
|
+
$filter: {
|
|
4759
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4760
|
+
as: 'd',
|
|
4761
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4518
4762
|
},
|
|
4519
4763
|
},
|
|
4520
4764
|
} },
|
|
4765
|
+
{ $unwind: '$datesInWeek' },
|
|
4521
4766
|
{ $group: {
|
|
4522
4767
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4523
4768
|
storeName: { $last: '$storeName' },
|
|
4524
|
-
|
|
4769
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4770
|
+
} },
|
|
4771
|
+
{ $project: {
|
|
4772
|
+
_id: 1,
|
|
4773
|
+
storeName: 1,
|
|
4774
|
+
count: { $size: '$uniqueDates' },
|
|
4525
4775
|
} },
|
|
4526
4776
|
] );
|
|
4527
4777
|
const map = new Map();
|
|
@@ -4536,180 +4786,382 @@ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
|
4536
4786
|
return map;
|
|
4537
4787
|
}
|
|
4538
4788
|
|
|
4539
|
-
function buildWeeklyWrapExcel( header,
|
|
4789
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4540
4790
|
const workbook = new ExcelJS.Workbook();
|
|
4541
4791
|
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4542
4792
|
sheet.addRow( [ header ] );
|
|
4543
4793
|
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4544
4794
|
sheet.addRow( [] );
|
|
4545
4795
|
sheet.columns = [
|
|
4546
|
-
{ header: 'Store ID', key: 'storeId', width: 18 },
|
|
4547
4796
|
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4797
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4798
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4548
4799
|
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4549
|
-
{ header: 'Not Submitted', key: 'timeFlag', width:
|
|
4550
|
-
{ header: 'Run AI Flags', key: 'runAIFlag', width:
|
|
4800
|
+
{ header: 'Not Submitted Flags', key: 'timeFlag', width: 20 },
|
|
4801
|
+
{ header: 'Run AI Flags', key: 'runAIFlag', width: 14 },
|
|
4551
4802
|
{ header: 'Recurring Flags', key: 'recurringFlag', width: 18 },
|
|
4552
|
-
{ header: 'Total Flags', key: 'totalFlags', width: 14 },
|
|
4553
|
-
{ header: 'Last Week Total', key: 'prevTotal', width: 18 },
|
|
4554
|
-
{ header: 'WoW %', key: 'wow', width: 12 },
|
|
4555
4803
|
];
|
|
4556
4804
|
sheet.getRow( 3 ).font = { bold: true };
|
|
4557
4805
|
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4558
|
-
for ( const r of
|
|
4806
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4559
4807
|
return workbook.xlsx.writeBuffer();
|
|
4560
4808
|
}
|
|
4561
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
|
+
|
|
4562
4946
|
export async function weeklyWrapAlert( req, res ) {
|
|
4563
4947
|
try {
|
|
4564
4948
|
const range = getLastWeekRange();
|
|
4565
4949
|
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4566
4950
|
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4567
4951
|
|
|
4568
|
-
//
|
|
4569
|
-
const
|
|
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 ) ) );
|
|
4570
4959
|
|
|
4571
|
-
// Aggregate flag data for both weeks across all stores in one shot.
|
|
4572
|
-
|
|
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( [
|
|
4573
4963
|
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4574
4964
|
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4575
4965
|
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4576
4966
|
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4967
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4968
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4577
4969
|
] );
|
|
4578
4970
|
|
|
4579
|
-
// Compute per-client highest flagged store and highest recurring flagged store (this week).
|
|
4580
|
-
const topByClient = new Map(); // clientId -> { topFlag, topRecurring }
|
|
4581
|
-
const upsertTop = ( client, key, candidate ) => {
|
|
4582
|
-
if ( !topByClient.has( client ) ) topByClient.set( client, { topFlag: null, topRecurring: null } );
|
|
4583
|
-
const entry = topByClient.get( client );
|
|
4584
|
-
if ( !entry[key] || candidate.value > entry[key].value ) entry[key] = candidate;
|
|
4585
|
-
};
|
|
4586
|
-
for ( const v of flagsThis.values() ) {
|
|
4587
|
-
upsertTop( v.client_id, 'topFlag', { storeId: v.store_id, storeName: v.storeName, value: v.totalFlags } );
|
|
4588
|
-
}
|
|
4589
|
-
for ( const v of recurThis.values() ) {
|
|
4590
|
-
upsertTop( v.client_id, 'topRecurring', { storeId: v.store_id, storeName: v.storeName, value: v.count } );
|
|
4591
|
-
}
|
|
4592
|
-
|
|
4593
4971
|
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4594
4972
|
const compiled = handlebars.compile( fileContent );
|
|
4595
4973
|
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4596
4974
|
const sentSummary = [];
|
|
4597
4975
|
|
|
4598
|
-
// Build one digest per client present in the data
|
|
4976
|
+
// Build one digest per client present in the data, restricted to clients that opted in.
|
|
4599
4977
|
const clientIds = new Set();
|
|
4600
|
-
for ( const v of flagsThis.values() )
|
|
4601
|
-
|
|
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
|
+
}
|
|
4602
4984
|
|
|
4603
4985
|
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4604
4986
|
try {
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
|
|
4617
|
-
// Iterate only stores belonging to this client (key prefix match).
|
|
4618
|
-
const clientPrefix = `${clientId}::`;
|
|
4619
|
-
const clientStoreKeys = new Set();
|
|
4620
|
-
for ( const k of flagsThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4621
|
-
for ( const k of flagsPrev.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4622
|
-
for ( const k of recurThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4623
|
-
|
|
4624
|
-
for ( const k of clientStoreKeys ) {
|
|
4625
|
-
const cur = flagsThis.get( k );
|
|
4626
|
-
const prev = flagsPrev.get( k );
|
|
4627
|
-
const recCur = recurThis.get( k );
|
|
4628
|
-
const recPrev = recurPrev.get( k );
|
|
4629
|
-
const storeRecurring = recCur?.count || 0;
|
|
4630
|
-
const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
|
|
4631
|
-
const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
|
|
4632
|
-
|
|
4633
|
-
if ( storeTotal > 0 ) storesFlagged += 1;
|
|
4634
|
-
if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
|
|
4635
|
-
totalFlags += storeTotal;
|
|
4636
|
-
prevTotalFlags += prevStoreTotal;
|
|
4637
|
-
questionFlags += cur?.questionFlag || 0;
|
|
4638
|
-
notSubmittedFlags += cur?.timeFlag || 0;
|
|
4639
|
-
runAIFlags += cur?.runAIFlag || 0;
|
|
4640
|
-
recurringFlags += storeRecurring;
|
|
4641
|
-
|
|
4642
|
-
( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
|
|
4643
|
-
( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
|
|
4644
|
-
|
|
4645
|
-
if ( storeTotal > 0 || prevStoreTotal > 0 ) {
|
|
4646
|
-
const w = computeWow( storeTotal, prevStoreTotal );
|
|
4647
|
-
excelRows.push( {
|
|
4648
|
-
storeId: ( cur?.store_id || prev?.store_id || k.slice( clientPrefix.length ) ),
|
|
4649
|
-
storeName: cur?.storeName || prev?.storeName || '',
|
|
4650
|
-
questionFlag: cur?.questionFlag || 0,
|
|
4651
|
-
timeFlag: cur?.timeFlag || 0,
|
|
4652
|
-
runAIFlag: cur?.runAIFlag || 0,
|
|
4653
|
-
recurringFlag: storeRecurring,
|
|
4654
|
-
totalFlags: storeTotal,
|
|
4655
|
-
prevTotal: prevStoreTotal,
|
|
4656
|
-
wow: w.value ? `${w.direction === 'up' ? '+' : '-'}${w.value}` : '0%',
|
|
4657
|
-
} );
|
|
4658
|
-
}
|
|
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;
|
|
4659
4998
|
}
|
|
4660
4999
|
|
|
4661
|
-
|
|
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
|
+
}
|
|
4662
5009
|
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
4673
|
-
|
|
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
|
+
}
|
|
4674
5023
|
|
|
4675
|
-
|
|
4676
|
-
|
|
4677
|
-
|
|
4678
|
-
|
|
4679
|
-
|
|
4680
|
-
|
|
4681
|
-
|
|
4682
|
-
|
|
4683
|
-
|
|
4684
|
-
|
|
4685
|
-
|
|
4686
|
-
|
|
4687
|
-
|
|
4688
|
-
|
|
4689
|
-
|
|
4690
|
-
|
|
4691
|
-
|
|
4692
|
-
|
|
4693
|
-
|
|
4694
|
-
|
|
4695
|
-
|
|
4696
|
-
|
|
4697
|
-
|
|
4698
|
-
|
|
4699
|
-
|
|
4700
|
-
|
|
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
|
+
}
|
|
4701
5067
|
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
|
|
4705
|
-
|
|
4706
|
-
|
|
4707
|
-
|
|
4708
|
-
|
|
4709
|
-
|
|
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
|
+
}
|
|
4710
5111
|
|
|
4711
|
-
|
|
4712
|
-
|
|
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
|
+
}
|
|
4713
5165
|
} catch ( e ) {
|
|
4714
5166
|
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
4715
5167
|
}
|
|
@@ -4774,3 +5226,4 @@ export async function updateStoreLatLong( req, res ) {
|
|
|
4774
5226
|
return res.sendError( e, 500 );
|
|
4775
5227
|
}
|
|
4776
5228
|
}
|
|
5229
|
+
|