tango-app-api-trax 3.8.24 → 3.8.25-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 +648 -194
- package/src/controllers/mobileTrax.controller.js +792 -180
- 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/routes/mobileTrax.routes.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
|
-
console.log( JSON.stringify( checklistDetails ) );
|
|
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 || [];
|
|
@@ -4277,39 +4387,93 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4277
4387
|
recipients = [ ...recipients, ...nfUsers.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4278
4388
|
}
|
|
4279
4389
|
if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
|
|
4280
|
-
recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
|
|
4390
|
+
recipients = [ ...recipients, ...cl.approver.map( ( a ) => a?.value ).filter( Boolean ) ];
|
|
4281
4391
|
}
|
|
4282
4392
|
recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
|
|
4283
4393
|
recipients = [ ...new Set( recipients ) ];
|
|
4284
4394
|
|
|
4285
4395
|
if ( !recipients.length ) return;
|
|
4286
4396
|
|
|
4287
|
-
//
|
|
4288
|
-
//
|
|
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).
|
|
4289
4413
|
const trackerRows = await recurringFlagTracker.find( {
|
|
4290
4414
|
sourceCheckList_id: cl._id,
|
|
4291
|
-
|
|
4292
|
-
|
|
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
|
+
],
|
|
4293
4425
|
} );
|
|
4294
4426
|
|
|
4295
4427
|
for ( const t of trackerRows ) {
|
|
4296
|
-
|
|
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;
|
|
4297
4441
|
triggers.push( {
|
|
4298
4442
|
recipient,
|
|
4299
4443
|
clientId: t.client_id,
|
|
4300
|
-
|
|
4301
|
-
|
|
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 || '',
|
|
4302
4452
|
checklistId: cl._id.toString(),
|
|
4303
4453
|
checklistName: cl.checkListName?.trim() || t.checkListName || '',
|
|
4304
4454
|
sectionName: t.sectionName,
|
|
4305
4455
|
qno: t.qno,
|
|
4306
4456
|
qname: t.qname,
|
|
4307
4457
|
days: t.consecutiveCount,
|
|
4458
|
+
runAICount: t.runAICount || 0,
|
|
4459
|
+
sopFired,
|
|
4460
|
+
runAIFired,
|
|
4308
4461
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4309
4462
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4310
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
|
+
} );
|
|
4311
4476
|
}
|
|
4312
|
-
trackerIdsToReset.push( { _id: t._id, lastFlaggedDate: t.lastFlaggedDate } );
|
|
4313
4477
|
}
|
|
4314
4478
|
} ) );
|
|
4315
4479
|
|
|
@@ -4317,6 +4481,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4317
4481
|
return res.sendSuccess( 'No recurring flags reached threshold' );
|
|
4318
4482
|
}
|
|
4319
4483
|
|
|
4484
|
+
|
|
4320
4485
|
// Group triggers by recipient.
|
|
4321
4486
|
const byRecipient = new Map();
|
|
4322
4487
|
for ( const t of triggers ) {
|
|
@@ -4331,21 +4496,77 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4331
4496
|
const sentSummary = [];
|
|
4332
4497
|
|
|
4333
4498
|
await Promise.all( [ ...byRecipient.entries() ].map( async ( [ recipient, items ] ) => {
|
|
4334
|
-
const
|
|
4499
|
+
const subjects = new Set( items.map( ( i ) => i.subjectId ) );
|
|
4335
4500
|
const checklists = new Set( items.map( ( i ) => i.checklistId ) );
|
|
4336
|
-
const isMultiStore =
|
|
4501
|
+
const isMultiStore = subjects.size > 1; // "isMultiStore" name retained for template back-compat
|
|
4337
4502
|
const isMultiChecklist = !isMultiStore && checklists.size > 1;
|
|
4338
|
-
//
|
|
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.
|
|
4339
4507
|
const thresholdShown = items.reduce( ( acc, it ) => Math.min( acc, it.days ), items[0].days );
|
|
4340
4508
|
|
|
4341
|
-
|
|
4342
|
-
|
|
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,
|
|
4343
4563
|
checklistName: i.checklistName,
|
|
4344
4564
|
sectionName: i.sectionName,
|
|
4345
4565
|
questionName: i.qname,
|
|
4346
4566
|
lastSubmittedBy: i.lastSubmittedBy,
|
|
4347
4567
|
lastSubmissionDate: i.lastSubmissionDate,
|
|
4348
4568
|
days: i.days,
|
|
4569
|
+
runAICount: i.runAICount || 0,
|
|
4349
4570
|
} ) );
|
|
4350
4571
|
|
|
4351
4572
|
const ATTACHMENT_THRESHOLD = 10;
|
|
@@ -4356,31 +4577,43 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4356
4577
|
threshold: thresholdShown,
|
|
4357
4578
|
isMultiStore,
|
|
4358
4579
|
isMultiChecklist,
|
|
4580
|
+
isMultiStoreSingleChecklist,
|
|
4581
|
+
isUserCoverage: isAllUser,
|
|
4359
4582
|
showTable: isMultiStore || isMultiChecklist,
|
|
4360
4583
|
hasAttachment,
|
|
4361
4584
|
domain: flagDomain,
|
|
4362
4585
|
rows: displayRows,
|
|
4586
|
+
subjectLabel,
|
|
4587
|
+
subjectLabelPlural,
|
|
4588
|
+
subjectLabelLower,
|
|
4589
|
+
subjectLabelPluralLower,
|
|
4363
4590
|
};
|
|
4364
4591
|
|
|
4365
4592
|
if ( isMultiStore ) {
|
|
4366
4593
|
data.highlights = {
|
|
4367
|
-
|
|
4594
|
+
totalSubjects: subjects.size,
|
|
4595
|
+
totalStores: subjects.size, // legacy alias
|
|
4368
4596
|
totalChecklists: checklists.size,
|
|
4369
4597
|
totalFlags: items.length,
|
|
4370
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
|
+
}
|
|
4371
4603
|
} else if ( isMultiChecklist ) {
|
|
4372
|
-
data.
|
|
4604
|
+
data.subjectName = items[0].subjectName;
|
|
4605
|
+
data.storeName = items[0].subjectName;
|
|
4373
4606
|
} else {
|
|
4374
|
-
|
|
4375
|
-
|
|
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;
|
|
4376
4611
|
data.checklistName = single.checklistName;
|
|
4377
|
-
data.questionName = single.qname;
|
|
4378
4612
|
data.lastSubmittedBy = single.lastSubmittedBy;
|
|
4379
4613
|
data.lastSubmissionDate = single.lastSubmissionDate;
|
|
4380
|
-
data.
|
|
4381
|
-
data.
|
|
4382
|
-
data.
|
|
4383
|
-
data.flagCountPlural = false;
|
|
4614
|
+
data.flagCount = single.flagCount;
|
|
4615
|
+
data.runAICount = single.runAICount;
|
|
4616
|
+
data.totalFlags = single.totalFlags;
|
|
4384
4617
|
}
|
|
4385
4618
|
|
|
4386
4619
|
const html = compiled( { data } );
|
|
@@ -4395,7 +4628,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4395
4628
|
|
|
4396
4629
|
if ( hasAttachment ) {
|
|
4397
4630
|
try {
|
|
4398
|
-
const buf = await buildRecurringFlagExcel(
|
|
4631
|
+
const buf = await buildRecurringFlagExcel( excelRows, subjectLabel );
|
|
4399
4632
|
params.attachment = {
|
|
4400
4633
|
filename: 'Recurring-Flags-Summary.xlsx',
|
|
4401
4634
|
content: Buffer.from( buf ),
|
|
@@ -4405,22 +4638,31 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4405
4638
|
logger.error( { functionName: 'recurringFlagAlert.buildExcel', error: e } );
|
|
4406
4639
|
}
|
|
4407
4640
|
}
|
|
4408
|
-
|
|
4409
4641
|
sendEmailWithSES( params.toEmail, params.mailSubject, params.htmlBody, params.attachment, params.sourceEmail );
|
|
4410
4642
|
sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
|
|
4411
4643
|
} ) );
|
|
4412
4644
|
|
|
4413
|
-
// 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).
|
|
4414
4647
|
if ( trackerIdsToReset.length ) {
|
|
4415
|
-
const resetOps = trackerIdsToReset.map( (
|
|
4416
|
-
|
|
4417
|
-
|
|
4418
|
-
|
|
4419
|
-
|
|
4420
|
-
|
|
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
|
+
},
|
|
4421
4663
|
},
|
|
4422
|
-
}
|
|
4423
|
-
} )
|
|
4664
|
+
};
|
|
4665
|
+
} );
|
|
4424
4666
|
await recurringFlagTracker.bulkWrite( resetOps );
|
|
4425
4667
|
}
|
|
4426
4668
|
|
|
@@ -4436,10 +4678,12 @@ function getLastWeekRange( ref = dayjs() ) {
|
|
|
4436
4678
|
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4437
4679
|
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4438
4680
|
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4439
|
-
|
|
4440
|
-
const
|
|
4441
|
-
const
|
|
4442
|
-
|
|
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' );
|
|
4443
4687
|
return {
|
|
4444
4688
|
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4445
4689
|
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
@@ -4461,7 +4705,7 @@ function computeWow( current, previous ) {
|
|
|
4461
4705
|
|
|
4462
4706
|
async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
4463
4707
|
const rows = await processedchecklist.aggregate( [
|
|
4464
|
-
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd },
|
|
4708
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, checkListType: 'custom' } },
|
|
4465
4709
|
{ $group: {
|
|
4466
4710
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4467
4711
|
storeName: { $last: '$storeName' },
|
|
@@ -4501,26 +4745,33 @@ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
|
4501
4745
|
}
|
|
4502
4746
|
|
|
4503
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.
|
|
4504
4751
|
const rows = await recurringFlagTracker.aggregate( [
|
|
4505
4752
|
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4506
4753
|
{ $project: {
|
|
4507
4754
|
client_id: 1,
|
|
4508
4755
|
store_id: 1,
|
|
4509
4756
|
storeName: 1,
|
|
4510
|
-
|
|
4511
|
-
$
|
|
4512
|
-
$
|
|
4513
|
-
|
|
4514
|
-
|
|
4515
|
-
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4516
|
-
},
|
|
4757
|
+
datesInWeek: {
|
|
4758
|
+
$filter: {
|
|
4759
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4760
|
+
as: 'd',
|
|
4761
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4517
4762
|
},
|
|
4518
4763
|
},
|
|
4519
4764
|
} },
|
|
4765
|
+
{ $unwind: '$datesInWeek' },
|
|
4520
4766
|
{ $group: {
|
|
4521
4767
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4522
4768
|
storeName: { $last: '$storeName' },
|
|
4523
|
-
|
|
4769
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4770
|
+
} },
|
|
4771
|
+
{ $project: {
|
|
4772
|
+
_id: 1,
|
|
4773
|
+
storeName: 1,
|
|
4774
|
+
count: { $size: '$uniqueDates' },
|
|
4524
4775
|
} },
|
|
4525
4776
|
] );
|
|
4526
4777
|
const map = new Map();
|
|
@@ -4535,180 +4786,382 @@ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
|
4535
4786
|
return map;
|
|
4536
4787
|
}
|
|
4537
4788
|
|
|
4538
|
-
function buildWeeklyWrapExcel( header,
|
|
4789
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4539
4790
|
const workbook = new ExcelJS.Workbook();
|
|
4540
4791
|
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4541
4792
|
sheet.addRow( [ header ] );
|
|
4542
4793
|
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4543
4794
|
sheet.addRow( [] );
|
|
4544
4795
|
sheet.columns = [
|
|
4545
|
-
{ header: 'Store ID', key: 'storeId', width: 18 },
|
|
4546
4796
|
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4797
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4798
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4547
4799
|
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4548
|
-
{ header: 'Not Submitted', key: 'timeFlag', width:
|
|
4549
|
-
{ 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 },
|
|
4550
4802
|
{ header: 'Recurring Flags', key: 'recurringFlag', width: 18 },
|
|
4551
|
-
{ header: 'Total Flags', key: 'totalFlags', width: 14 },
|
|
4552
|
-
{ header: 'Last Week Total', key: 'prevTotal', width: 18 },
|
|
4553
|
-
{ header: 'WoW %', key: 'wow', width: 12 },
|
|
4554
4803
|
];
|
|
4555
4804
|
sheet.getRow( 3 ).font = { bold: true };
|
|
4556
4805
|
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4557
|
-
for ( const r of
|
|
4806
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4558
4807
|
return workbook.xlsx.writeBuffer();
|
|
4559
4808
|
}
|
|
4560
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
|
+
|
|
4561
4946
|
export async function weeklyWrapAlert( req, res ) {
|
|
4562
4947
|
try {
|
|
4563
4948
|
const range = getLastWeekRange();
|
|
4564
4949
|
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4565
4950
|
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4566
4951
|
|
|
4567
|
-
//
|
|
4568
|
-
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 ) ) );
|
|
4569
4959
|
|
|
4570
|
-
// Aggregate flag data for both weeks across all stores in one shot.
|
|
4571
|
-
|
|
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( [
|
|
4572
4963
|
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4573
4964
|
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4574
4965
|
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4575
4966
|
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4967
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4968
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4576
4969
|
] );
|
|
4577
4970
|
|
|
4578
|
-
// Compute per-client highest flagged store and highest recurring flagged store (this week).
|
|
4579
|
-
const topByClient = new Map(); // clientId -> { topFlag, topRecurring }
|
|
4580
|
-
const upsertTop = ( client, key, candidate ) => {
|
|
4581
|
-
if ( !topByClient.has( client ) ) topByClient.set( client, { topFlag: null, topRecurring: null } );
|
|
4582
|
-
const entry = topByClient.get( client );
|
|
4583
|
-
if ( !entry[key] || candidate.value > entry[key].value ) entry[key] = candidate;
|
|
4584
|
-
};
|
|
4585
|
-
for ( const v of flagsThis.values() ) {
|
|
4586
|
-
upsertTop( v.client_id, 'topFlag', { storeId: v.store_id, storeName: v.storeName, value: v.totalFlags } );
|
|
4587
|
-
}
|
|
4588
|
-
for ( const v of recurThis.values() ) {
|
|
4589
|
-
upsertTop( v.client_id, 'topRecurring', { storeId: v.store_id, storeName: v.storeName, value: v.count } );
|
|
4590
|
-
}
|
|
4591
|
-
|
|
4592
4971
|
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4593
4972
|
const compiled = handlebars.compile( fileContent );
|
|
4594
4973
|
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4595
4974
|
const sentSummary = [];
|
|
4596
4975
|
|
|
4597
|
-
// 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.
|
|
4598
4977
|
const clientIds = new Set();
|
|
4599
|
-
for ( const v of flagsThis.values() )
|
|
4600
|
-
|
|
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
|
+
}
|
|
4601
4984
|
|
|
4602
4985
|
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4603
4986
|
try {
|
|
4604
|
-
|
|
4605
|
-
|
|
4606
|
-
|
|
4607
|
-
|
|
4608
|
-
|
|
4609
|
-
|
|
4610
|
-
|
|
4611
|
-
|
|
4612
|
-
|
|
4613
|
-
|
|
4614
|
-
|
|
4615
|
-
|
|
4616
|
-
// Iterate only stores belonging to this client (key prefix match).
|
|
4617
|
-
const clientPrefix = `${clientId}::`;
|
|
4618
|
-
const clientStoreKeys = new Set();
|
|
4619
|
-
for ( const k of flagsThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4620
|
-
for ( const k of flagsPrev.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4621
|
-
for ( const k of recurThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4622
|
-
|
|
4623
|
-
for ( const k of clientStoreKeys ) {
|
|
4624
|
-
const cur = flagsThis.get( k );
|
|
4625
|
-
const prev = flagsPrev.get( k );
|
|
4626
|
-
const recCur = recurThis.get( k );
|
|
4627
|
-
const recPrev = recurPrev.get( k );
|
|
4628
|
-
const storeRecurring = recCur?.count || 0;
|
|
4629
|
-
const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
|
|
4630
|
-
const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
|
|
4631
|
-
|
|
4632
|
-
if ( storeTotal > 0 ) storesFlagged += 1;
|
|
4633
|
-
if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
|
|
4634
|
-
totalFlags += storeTotal;
|
|
4635
|
-
prevTotalFlags += prevStoreTotal;
|
|
4636
|
-
questionFlags += cur?.questionFlag || 0;
|
|
4637
|
-
notSubmittedFlags += cur?.timeFlag || 0;
|
|
4638
|
-
runAIFlags += cur?.runAIFlag || 0;
|
|
4639
|
-
recurringFlags += storeRecurring;
|
|
4640
|
-
|
|
4641
|
-
( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
|
|
4642
|
-
( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
|
|
4643
|
-
|
|
4644
|
-
if ( storeTotal > 0 || prevStoreTotal > 0 ) {
|
|
4645
|
-
const w = computeWow( storeTotal, prevStoreTotal );
|
|
4646
|
-
excelRows.push( {
|
|
4647
|
-
storeId: ( cur?.store_id || prev?.store_id || k.slice( clientPrefix.length ) ),
|
|
4648
|
-
storeName: cur?.storeName || prev?.storeName || '',
|
|
4649
|
-
questionFlag: cur?.questionFlag || 0,
|
|
4650
|
-
timeFlag: cur?.timeFlag || 0,
|
|
4651
|
-
runAIFlag: cur?.runAIFlag || 0,
|
|
4652
|
-
recurringFlag: storeRecurring,
|
|
4653
|
-
totalFlags: storeTotal,
|
|
4654
|
-
prevTotal: prevStoreTotal,
|
|
4655
|
-
wow: w.value ? `${w.direction === 'up' ? '+' : '-'}${w.value}` : '0%',
|
|
4656
|
-
} );
|
|
4657
|
-
}
|
|
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;
|
|
4658
4998
|
}
|
|
4659
4999
|
|
|
4660
|
-
|
|
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
|
+
}
|
|
4661
5009
|
|
|
4662
|
-
|
|
4663
|
-
|
|
4664
|
-
|
|
4665
|
-
|
|
4666
|
-
|
|
4667
|
-
|
|
4668
|
-
|
|
4669
|
-
|
|
4670
|
-
|
|
4671
|
-
|
|
4672
|
-
|
|
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
|
+
}
|
|
4673
5023
|
|
|
4674
|
-
|
|
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
|
-
|
|
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
|
+
}
|
|
4700
5067
|
|
|
4701
|
-
|
|
4702
|
-
|
|
4703
|
-
|
|
4704
|
-
content: attachmentBuffer,
|
|
4705
|
-
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
4706
|
-
};
|
|
4707
|
-
const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
|
|
4708
|
-
const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
|
|
5068
|
+
if ( totalFlags === 0 && prevTotalFlags === 0 ) {
|
|
5069
|
+
continue; // user has nothing to report this week
|
|
5070
|
+
}
|
|
4709
5071
|
|
|
4710
|
-
|
|
4711
|
-
|
|
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
|
+
}
|
|
4712
5165
|
} catch ( e ) {
|
|
4713
5166
|
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
4714
5167
|
}
|
|
@@ -4773,3 +5226,4 @@ export async function updateStoreLatLong( req, res ) {
|
|
|
4773
5226
|
return res.sendError( e, 500 );
|
|
4774
5227
|
}
|
|
4775
5228
|
}
|
|
5229
|
+
|