tango-app-api-trax 3.8.25 → 3.8.26-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 +418 -172
- package/src/controllers/mobileTrax.controller.js +770 -168
- package/src/controllers/teaxFlag.controller.js +5 -5
- package/src/controllers/trax.controller.js +6 -2
- package/src/hbs/flag.hbs +248 -248
- package/src/hbs/login-otp.hbs +943 -943
- package/src/hbs/recurringFlag.hbs +250 -253
- package/src/hbs/template.hbs +6 -2
- package/src/hbs/visit-checklist.hbs +82 -38
- package/src/hbs/weeklyWrap.hbs +218 -218
- package/src/routes/mobileTrax.routes.js +1 -0
- 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' );
|
|
@@ -4360,7 +4365,6 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4360
4365
|
],
|
|
4361
4366
|
},
|
|
4362
4367
|
}, { _id: 1, checkListName: 1, recurringFlag: 1, notifyFlags: 1, approver: 1, client_id: 1 } );
|
|
4363
|
-
console.log( JSON.stringify( checklistDetails ) );
|
|
4364
4368
|
if ( !checklistDetails.length ) {
|
|
4365
4369
|
return res.sendSuccess( 'No checklists configured for recurring flag' );
|
|
4366
4370
|
}
|
|
@@ -4390,6 +4394,20 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4390
4394
|
|
|
4391
4395
|
if ( !recipients.length ) return;
|
|
4392
4396
|
|
|
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
|
+
|
|
4393
4411
|
// Read tracker rows where EITHER the sop streak or the runAI count has hit threshold and the
|
|
4394
4412
|
// current trigger hasn't been emailed yet (lastEmailDate vs the relevant flagged date).
|
|
4395
4413
|
const trackerRows = await recurringFlagTracker.find( {
|
|
@@ -4414,7 +4432,12 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4414
4432
|
// Determine which streak crossed threshold for this row — drives reset granularity below.
|
|
4415
4433
|
const sopFired = ( t.consecutiveCount || 0 ) >= threshold && t.lastEmailDate !== t.lastFlaggedDate;
|
|
4416
4434
|
const runAIFired = ( t.runAICount || 0 ) >= threshold && t.lastEmailDate !== t.lastRunAIFlaggedDate;
|
|
4417
|
-
|
|
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;
|
|
4418
4441
|
triggers.push( {
|
|
4419
4442
|
recipient,
|
|
4420
4443
|
clientId: t.client_id,
|
|
@@ -4438,14 +4461,19 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4438
4461
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4439
4462
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4440
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
|
+
} );
|
|
4441
4476
|
}
|
|
4442
|
-
trackerIdsToReset.push( {
|
|
4443
|
-
_id: t._id,
|
|
4444
|
-
lastFlaggedDate: t.lastFlaggedDate,
|
|
4445
|
-
lastRunAIFlaggedDate: t.lastRunAIFlaggedDate,
|
|
4446
|
-
sopFired,
|
|
4447
|
-
runAIFired,
|
|
4448
|
-
} );
|
|
4449
4477
|
}
|
|
4450
4478
|
} ) );
|
|
4451
4479
|
|
|
@@ -4470,9 +4498,11 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4470
4498
|
await Promise.all( [ ...byRecipient.entries() ].map( async ( [ recipient, items ] ) => {
|
|
4471
4499
|
const subjects = new Set( items.map( ( i ) => i.subjectId ) );
|
|
4472
4500
|
const checklists = new Set( items.map( ( i ) => i.checklistId ) );
|
|
4473
|
-
console.log( checklists );
|
|
4474
4501
|
const isMultiStore = subjects.size > 1; // "isMultiStore" name retained for template back-compat
|
|
4475
4502
|
const isMultiChecklist = !isMultiStore && checklists.size > 1;
|
|
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;
|
|
4476
4506
|
// Threshold for the message line — when grouping spans multiple checklists/subjects, take min threshold seen.
|
|
4477
4507
|
const thresholdShown = items.reduce( ( acc, it ) => Math.min( acc, it.days ), items[0].days );
|
|
4478
4508
|
|
|
@@ -4508,8 +4538,8 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4508
4538
|
} );
|
|
4509
4539
|
}
|
|
4510
4540
|
const g = groupMap.get( k );
|
|
4511
|
-
g.questionCount += 1;
|
|
4512
|
-
|
|
4541
|
+
if ( i.sopFired ) g.questionCount += 1;
|
|
4542
|
+
if ( i.runAIFired ) g.runAICount += 1;
|
|
4513
4543
|
if ( parseSubmissionDate( i.lastSubmissionDate ) > parseSubmissionDate( g.lastSubmissionDate ) ) {
|
|
4514
4544
|
g.lastSubmissionDate = i.lastSubmissionDate;
|
|
4515
4545
|
g.lastSubmittedBy = i.lastSubmittedBy;
|
|
@@ -4521,9 +4551,10 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4521
4551
|
checklistName: g.checklistName,
|
|
4522
4552
|
lastSubmittedBy: g.lastSubmittedBy,
|
|
4523
4553
|
lastSubmissionDate: g.lastSubmissionDate,
|
|
4524
|
-
days: g.questionCount, //
|
|
4554
|
+
days: g.questionCount, // legacy alias kept for back-compat with older template builds
|
|
4525
4555
|
flagCount: g.questionCount,
|
|
4526
4556
|
runAICount: g.runAICount,
|
|
4557
|
+
totalFlags: g.questionCount + g.runAICount,
|
|
4527
4558
|
} ) );
|
|
4528
4559
|
|
|
4529
4560
|
// Excel attachment keeps per-question detail (one row per flagged question).
|
|
@@ -4546,6 +4577,8 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4546
4577
|
threshold: thresholdShown,
|
|
4547
4578
|
isMultiStore,
|
|
4548
4579
|
isMultiChecklist,
|
|
4580
|
+
isMultiStoreSingleChecklist,
|
|
4581
|
+
isUserCoverage: isAllUser,
|
|
4549
4582
|
showTable: isMultiStore || isMultiChecklist,
|
|
4550
4583
|
hasAttachment,
|
|
4551
4584
|
domain: flagDomain,
|
|
@@ -4563,11 +4596,15 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4563
4596
|
totalChecklists: checklists.size,
|
|
4564
4597
|
totalFlags: items.length,
|
|
4565
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
|
+
}
|
|
4566
4603
|
} else if ( isMultiChecklist ) {
|
|
4567
4604
|
data.subjectName = items[0].subjectName;
|
|
4568
4605
|
data.storeName = items[0].subjectName;
|
|
4569
4606
|
} else {
|
|
4570
|
-
// Single mode: one (subject, checklist) —
|
|
4607
|
+
// Single mode: one (subject, checklist) — totalFlags = sop question flags + runAI flags.
|
|
4571
4608
|
const single = rows[0];
|
|
4572
4609
|
data.subjectName = single.subjectName;
|
|
4573
4610
|
data.storeName = single.subjectName;
|
|
@@ -4575,9 +4612,8 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4575
4612
|
data.lastSubmittedBy = single.lastSubmittedBy;
|
|
4576
4613
|
data.lastSubmissionDate = single.lastSubmissionDate;
|
|
4577
4614
|
data.flagCount = single.flagCount;
|
|
4578
|
-
data.flagCountPlural = single.flagCount > 1;
|
|
4579
4615
|
data.runAICount = single.runAICount;
|
|
4580
|
-
data.
|
|
4616
|
+
data.totalFlags = single.totalFlags;
|
|
4581
4617
|
}
|
|
4582
4618
|
|
|
4583
4619
|
const html = compiled( { data } );
|
|
@@ -4602,7 +4638,6 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4602
4638
|
logger.error( { functionName: 'recurringFlagAlert.buildExcel', error: e } );
|
|
4603
4639
|
}
|
|
4604
4640
|
}
|
|
4605
|
-
console.log( params.toEmail );
|
|
4606
4641
|
sendEmailWithSES( params.toEmail, params.mailSubject, params.htmlBody, params.attachment, params.sourceEmail );
|
|
4607
4642
|
sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
|
|
4608
4643
|
} ) );
|
|
@@ -4643,10 +4678,12 @@ function getLastWeekRange( ref = dayjs() ) {
|
|
|
4643
4678
|
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4644
4679
|
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4645
4680
|
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4646
|
-
|
|
4647
|
-
const
|
|
4648
|
-
const
|
|
4649
|
-
|
|
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' );
|
|
4650
4687
|
return {
|
|
4651
4688
|
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4652
4689
|
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
@@ -4668,7 +4705,7 @@ function computeWow( current, previous ) {
|
|
|
4668
4705
|
|
|
4669
4706
|
async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
4670
4707
|
const rows = await processedchecklist.aggregate( [
|
|
4671
|
-
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd },
|
|
4708
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, checkListType: 'custom' } },
|
|
4672
4709
|
{ $group: {
|
|
4673
4710
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4674
4711
|
storeName: { $last: '$storeName' },
|
|
@@ -4708,26 +4745,33 @@ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
|
4708
4745
|
}
|
|
4709
4746
|
|
|
4710
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.
|
|
4711
4751
|
const rows = await recurringFlagTracker.aggregate( [
|
|
4712
4752
|
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4713
4753
|
{ $project: {
|
|
4714
4754
|
client_id: 1,
|
|
4715
4755
|
store_id: 1,
|
|
4716
4756
|
storeName: 1,
|
|
4717
|
-
|
|
4718
|
-
$
|
|
4719
|
-
$
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4723
|
-
},
|
|
4757
|
+
datesInWeek: {
|
|
4758
|
+
$filter: {
|
|
4759
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4760
|
+
as: 'd',
|
|
4761
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4724
4762
|
},
|
|
4725
4763
|
},
|
|
4726
4764
|
} },
|
|
4765
|
+
{ $unwind: '$datesInWeek' },
|
|
4727
4766
|
{ $group: {
|
|
4728
4767
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4729
4768
|
storeName: { $last: '$storeName' },
|
|
4730
|
-
|
|
4769
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4770
|
+
} },
|
|
4771
|
+
{ $project: {
|
|
4772
|
+
_id: 1,
|
|
4773
|
+
storeName: 1,
|
|
4774
|
+
count: { $size: '$uniqueDates' },
|
|
4731
4775
|
} },
|
|
4732
4776
|
] );
|
|
4733
4777
|
const map = new Map();
|
|
@@ -4742,180 +4786,382 @@ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
|
4742
4786
|
return map;
|
|
4743
4787
|
}
|
|
4744
4788
|
|
|
4745
|
-
function buildWeeklyWrapExcel( header,
|
|
4789
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4746
4790
|
const workbook = new ExcelJS.Workbook();
|
|
4747
4791
|
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4748
4792
|
sheet.addRow( [ header ] );
|
|
4749
4793
|
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4750
4794
|
sheet.addRow( [] );
|
|
4751
4795
|
sheet.columns = [
|
|
4752
|
-
{ header: 'Store ID', key: 'storeId', width: 18 },
|
|
4753
4796
|
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4797
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4798
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4754
4799
|
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4755
|
-
{ header: 'Not Submitted', key: 'timeFlag', width:
|
|
4756
|
-
{ 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 },
|
|
4757
4802
|
{ header: 'Recurring Flags', key: 'recurringFlag', width: 18 },
|
|
4758
|
-
{ header: 'Total Flags', key: 'totalFlags', width: 14 },
|
|
4759
|
-
{ header: 'Last Week Total', key: 'prevTotal', width: 18 },
|
|
4760
|
-
{ header: 'WoW %', key: 'wow', width: 12 },
|
|
4761
4803
|
];
|
|
4762
4804
|
sheet.getRow( 3 ).font = { bold: true };
|
|
4763
4805
|
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4764
|
-
for ( const r of
|
|
4806
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4765
4807
|
return workbook.xlsx.writeBuffer();
|
|
4766
4808
|
}
|
|
4767
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
|
+
|
|
4768
4946
|
export async function weeklyWrapAlert( req, res ) {
|
|
4769
4947
|
try {
|
|
4770
4948
|
const range = getLastWeekRange();
|
|
4771
4949
|
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4772
4950
|
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4773
4951
|
|
|
4774
|
-
//
|
|
4775
|
-
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 ) ) );
|
|
4776
4959
|
|
|
4777
|
-
// Aggregate flag data for both weeks across all stores in one shot.
|
|
4778
|
-
|
|
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( [
|
|
4779
4963
|
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4780
4964
|
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4781
4965
|
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4782
4966
|
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4967
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4968
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4783
4969
|
] );
|
|
4784
4970
|
|
|
4785
|
-
// Compute per-client highest flagged store and highest recurring flagged store (this week).
|
|
4786
|
-
const topByClient = new Map(); // clientId -> { topFlag, topRecurring }
|
|
4787
|
-
const upsertTop = ( client, key, candidate ) => {
|
|
4788
|
-
if ( !topByClient.has( client ) ) topByClient.set( client, { topFlag: null, topRecurring: null } );
|
|
4789
|
-
const entry = topByClient.get( client );
|
|
4790
|
-
if ( !entry[key] || candidate.value > entry[key].value ) entry[key] = candidate;
|
|
4791
|
-
};
|
|
4792
|
-
for ( const v of flagsThis.values() ) {
|
|
4793
|
-
upsertTop( v.client_id, 'topFlag', { storeId: v.store_id, storeName: v.storeName, value: v.totalFlags } );
|
|
4794
|
-
}
|
|
4795
|
-
for ( const v of recurThis.values() ) {
|
|
4796
|
-
upsertTop( v.client_id, 'topRecurring', { storeId: v.store_id, storeName: v.storeName, value: v.count } );
|
|
4797
|
-
}
|
|
4798
|
-
|
|
4799
4971
|
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4800
4972
|
const compiled = handlebars.compile( fileContent );
|
|
4801
4973
|
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4802
4974
|
const sentSummary = [];
|
|
4803
4975
|
|
|
4804
|
-
// 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.
|
|
4805
4977
|
const clientIds = new Set();
|
|
4806
|
-
for ( const v of flagsThis.values() )
|
|
4807
|
-
|
|
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
|
+
}
|
|
4808
4984
|
|
|
4809
4985
|
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4810
4986
|
try {
|
|
4811
|
-
|
|
4812
|
-
|
|
4813
|
-
|
|
4814
|
-
|
|
4815
|
-
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
// Iterate only stores belonging to this client (key prefix match).
|
|
4824
|
-
const clientPrefix = `${clientId}::`;
|
|
4825
|
-
const clientStoreKeys = new Set();
|
|
4826
|
-
for ( const k of flagsThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4827
|
-
for ( const k of flagsPrev.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4828
|
-
for ( const k of recurThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4829
|
-
|
|
4830
|
-
for ( const k of clientStoreKeys ) {
|
|
4831
|
-
const cur = flagsThis.get( k );
|
|
4832
|
-
const prev = flagsPrev.get( k );
|
|
4833
|
-
const recCur = recurThis.get( k );
|
|
4834
|
-
const recPrev = recurPrev.get( k );
|
|
4835
|
-
const storeRecurring = recCur?.count || 0;
|
|
4836
|
-
const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
|
|
4837
|
-
const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
|
|
4838
|
-
|
|
4839
|
-
if ( storeTotal > 0 ) storesFlagged += 1;
|
|
4840
|
-
if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
|
|
4841
|
-
totalFlags += storeTotal;
|
|
4842
|
-
prevTotalFlags += prevStoreTotal;
|
|
4843
|
-
questionFlags += cur?.questionFlag || 0;
|
|
4844
|
-
notSubmittedFlags += cur?.timeFlag || 0;
|
|
4845
|
-
runAIFlags += cur?.runAIFlag || 0;
|
|
4846
|
-
recurringFlags += storeRecurring;
|
|
4847
|
-
|
|
4848
|
-
( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
|
|
4849
|
-
( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
|
|
4850
|
-
|
|
4851
|
-
if ( storeTotal > 0 || prevStoreTotal > 0 ) {
|
|
4852
|
-
const w = computeWow( storeTotal, prevStoreTotal );
|
|
4853
|
-
excelRows.push( {
|
|
4854
|
-
storeId: ( cur?.store_id || prev?.store_id || k.slice( clientPrefix.length ) ),
|
|
4855
|
-
storeName: cur?.storeName || prev?.storeName || '',
|
|
4856
|
-
questionFlag: cur?.questionFlag || 0,
|
|
4857
|
-
timeFlag: cur?.timeFlag || 0,
|
|
4858
|
-
runAIFlag: cur?.runAIFlag || 0,
|
|
4859
|
-
recurringFlag: storeRecurring,
|
|
4860
|
-
totalFlags: storeTotal,
|
|
4861
|
-
prevTotal: prevStoreTotal,
|
|
4862
|
-
wow: w.value ? `${w.direction === 'up' ? '+' : '-'}${w.value}` : '0%',
|
|
4863
|
-
} );
|
|
4864
|
-
}
|
|
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;
|
|
4865
4998
|
}
|
|
4866
4999
|
|
|
4867
|
-
|
|
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
|
+
}
|
|
4868
5009
|
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
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
|
+
}
|
|
4880
5023
|
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
4885
|
-
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
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
|
+
}
|
|
4907
5067
|
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
4912
|
-
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
5068
|
+
if ( totalFlags === 0 && prevTotalFlags === 0 ) {
|
|
5069
|
+
continue; // user has nothing to report this week
|
|
5070
|
+
}
|
|
5071
|
+
|
|
5072
|
+
// Build the Excel attachment as one row per (store, checklist) the user can see.
|
|
5073
|
+
// Pulls from the per-checklist flag aggregation + per-checklist recurring email count.
|
|
5074
|
+
const seenStoreChecklist = new Set();
|
|
5075
|
+
for ( const k of flagsThisCL.keys() ) {
|
|
5076
|
+
if ( !k.startsWith( clientPrefix ) ) continue;
|
|
5077
|
+
const cur = flagsThisCL.get( k );
|
|
5078
|
+
if ( !isAllowed( cur.store_id ) ) continue;
|
|
5079
|
+
const recCur = recurThisCL.get( k );
|
|
5080
|
+
const recurringCount = recCur?.count || 0;
|
|
5081
|
+
const flags = ( cur.questionFlag || 0 ) + ( cur.timeFlag || 0 ) + ( cur.runAIFlag || 0 ) + recurringCount;
|
|
5082
|
+
if ( flags === 0 ) continue;
|
|
5083
|
+
excelRows.push( {
|
|
5084
|
+
storeName: cur.storeName || '',
|
|
5085
|
+
checkListName: cur.checkListName || '',
|
|
5086
|
+
flags,
|
|
5087
|
+
questionFlag: cur.questionFlag || 0,
|
|
5088
|
+
timeFlag: cur.timeFlag || 0,
|
|
5089
|
+
runAIFlag: cur.runAIFlag || 0,
|
|
5090
|
+
recurringFlag: recurringCount,
|
|
5091
|
+
} );
|
|
5092
|
+
seenStoreChecklist.add( k );
|
|
5093
|
+
}
|
|
5094
|
+
// (store, checklist) pairs that hit recurring threshold but had no flag aggregation entry.
|
|
5095
|
+
for ( const k of recurThisCL.keys() ) {
|
|
5096
|
+
if ( !k.startsWith( clientPrefix ) ) continue;
|
|
5097
|
+
if ( seenStoreChecklist.has( k ) ) continue;
|
|
5098
|
+
const recCur = recurThisCL.get( k );
|
|
5099
|
+
if ( !isAllowed( recCur.store_id ) ) continue;
|
|
5100
|
+
if ( !recCur.count ) continue;
|
|
5101
|
+
excelRows.push( {
|
|
5102
|
+
storeName: recCur.storeName || '',
|
|
5103
|
+
checkListName: recCur.checkListName || '',
|
|
5104
|
+
flags: recCur.count,
|
|
5105
|
+
questionFlag: 0,
|
|
5106
|
+
timeFlag: 0,
|
|
5107
|
+
runAIFlag: 0,
|
|
5108
|
+
recurringFlag: recCur.count,
|
|
5109
|
+
} );
|
|
5110
|
+
}
|
|
5111
|
+
|
|
5112
|
+
let highestFlaggedStoreWow = { value: '', direction: 'up' };
|
|
5113
|
+
if ( topFlag ) {
|
|
5114
|
+
const prev = flagsPrev.get( `${clientId}::${topFlag.storeId}` );
|
|
5115
|
+
highestFlaggedStoreWow = computeWow( topFlag.value, prev?.totalFlags || 0 );
|
|
5116
|
+
}
|
|
5117
|
+
let highestRecurringStoreWow = { value: '', direction: 'up' };
|
|
5118
|
+
if ( topRecurring ) {
|
|
5119
|
+
const prev = recurPrev.get( `${clientId}::${topRecurring.storeId}` );
|
|
5120
|
+
highestRecurringStoreWow = computeWow( topRecurring.value, prev?.count || 0 );
|
|
5121
|
+
}
|
|
5122
|
+
|
|
5123
|
+
const excelHeader = `Weekly Summary ${startDateLabel} - ${endDateLabel}`;
|
|
5124
|
+
const buf = await buildWeeklyWrapExcel( excelHeader, excelRows );
|
|
5125
|
+
const attachmentBuffer = Buffer.from( buf );
|
|
5126
|
+
const sizeKb = Math.max( 1, Math.round( attachmentBuffer.length / 1024 ) );
|
|
5127
|
+
|
|
5128
|
+
const data = {
|
|
5129
|
+
startDate: startDateLabel,
|
|
5130
|
+
endDate: endDateLabel,
|
|
5131
|
+
storesFlagged,
|
|
5132
|
+
storesFlaggedWow: computeWow( storesFlagged, prevStoresFlagged ),
|
|
5133
|
+
totalFlags,
|
|
5134
|
+
totalFlagsWow: computeWow( totalFlags, prevTotalFlags ),
|
|
5135
|
+
checklistFlags: flaggedChecklistsThis.size,
|
|
5136
|
+
checklistFlagsWow: computeWow( flaggedChecklistsThis.size, flaggedChecklistsPrev.size ),
|
|
5137
|
+
questionFlags,
|
|
5138
|
+
notSubmittedFlags,
|
|
5139
|
+
runAIFlags,
|
|
5140
|
+
recurringFlags,
|
|
5141
|
+
highestFlaggedStore: topFlag?.storeName || topFlag?.storeId || '--',
|
|
5142
|
+
highestFlaggedStoreWow,
|
|
5143
|
+
highestRecurringStore: topRecurring?.storeName || topRecurring?.storeId || '--',
|
|
5144
|
+
highestRecurringStoreWow,
|
|
5145
|
+
attachmentName: `${excelHeader}.xlsx`,
|
|
5146
|
+
attachmentSize: `${sizeKb} KB`,
|
|
5147
|
+
domain: flagDomain,
|
|
5148
|
+
};
|
|
5149
|
+
|
|
5150
|
+
const html = compiled( { data } );
|
|
5151
|
+
const attachment = {
|
|
5152
|
+
filename: `${excelHeader}.xlsx`,
|
|
5153
|
+
content: attachmentBuffer,
|
|
5154
|
+
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
5155
|
+
};
|
|
5156
|
+
const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
|
|
5157
|
+
const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
|
|
4916
5158
|
|
|
4917
|
-
|
|
4918
|
-
|
|
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
|
+
}
|
|
4919
5165
|
} catch ( e ) {
|
|
4920
5166
|
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
4921
5167
|
}
|