tango-app-api-trax 3.8.26 → 3.8.27-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 +406 -165
- package/src/controllers/mobileTrax.controller.js +782 -170
- package/src/controllers/teaxFlag.controller.js +5 -5
- package/src/controllers/trax.controller.js +6 -2
- package/src/hbs/recurringFlag.hbs +2 -2
- package/src/hbs/template.hbs +6 -2
- package/src/hbs/visit-checklist.hbs +52 -14
- package/src/routes/mobileTrax.routes.js +1 -0
- package/src/utils/visitChecklistPdf.utils.js +1 -1
|
@@ -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' );
|
|
@@ -4389,6 +4394,20 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4389
4394
|
|
|
4390
4395
|
if ( !recipients.length ) return;
|
|
4391
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
|
+
|
|
4392
4411
|
// Read tracker rows where EITHER the sop streak or the runAI count has hit threshold and the
|
|
4393
4412
|
// current trigger hasn't been emailed yet (lastEmailDate vs the relevant flagged date).
|
|
4394
4413
|
const trackerRows = await recurringFlagTracker.find( {
|
|
@@ -4413,7 +4432,12 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4413
4432
|
// Determine which streak crossed threshold for this row — drives reset granularity below.
|
|
4414
4433
|
const sopFired = ( t.consecutiveCount || 0 ) >= threshold && t.lastEmailDate !== t.lastFlaggedDate;
|
|
4415
4434
|
const runAIFired = ( t.runAICount || 0 ) >= threshold && t.lastEmailDate !== t.lastRunAIFlaggedDate;
|
|
4416
|
-
|
|
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;
|
|
4417
4441
|
triggers.push( {
|
|
4418
4442
|
recipient,
|
|
4419
4443
|
clientId: t.client_id,
|
|
@@ -4437,14 +4461,19 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4437
4461
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4438
4462
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4439
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
|
+
} );
|
|
4440
4476
|
}
|
|
4441
|
-
trackerIdsToReset.push( {
|
|
4442
|
-
_id: t._id,
|
|
4443
|
-
lastFlaggedDate: t.lastFlaggedDate,
|
|
4444
|
-
lastRunAIFlaggedDate: t.lastRunAIFlaggedDate,
|
|
4445
|
-
sopFired,
|
|
4446
|
-
runAIFired,
|
|
4447
|
-
} );
|
|
4448
4477
|
}
|
|
4449
4478
|
} ) );
|
|
4450
4479
|
|
|
@@ -4509,8 +4538,8 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4509
4538
|
} );
|
|
4510
4539
|
}
|
|
4511
4540
|
const g = groupMap.get( k );
|
|
4512
|
-
g.questionCount += 1;
|
|
4513
|
-
|
|
4541
|
+
if ( i.sopFired ) g.questionCount += 1;
|
|
4542
|
+
if ( i.runAIFired ) g.runAICount += 1;
|
|
4514
4543
|
if ( parseSubmissionDate( i.lastSubmissionDate ) > parseSubmissionDate( g.lastSubmissionDate ) ) {
|
|
4515
4544
|
g.lastSubmissionDate = i.lastSubmissionDate;
|
|
4516
4545
|
g.lastSubmittedBy = i.lastSubmittedBy;
|
|
@@ -4549,6 +4578,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4549
4578
|
isMultiStore,
|
|
4550
4579
|
isMultiChecklist,
|
|
4551
4580
|
isMultiStoreSingleChecklist,
|
|
4581
|
+
isUserCoverage: isAllUser,
|
|
4552
4582
|
showTable: isMultiStore || isMultiChecklist,
|
|
4553
4583
|
hasAttachment,
|
|
4554
4584
|
domain: flagDomain,
|
|
@@ -4648,10 +4678,12 @@ function getLastWeekRange( ref = dayjs() ) {
|
|
|
4648
4678
|
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4649
4679
|
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4650
4680
|
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4651
|
-
|
|
4652
|
-
const
|
|
4653
|
-
const
|
|
4654
|
-
|
|
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' );
|
|
4655
4687
|
return {
|
|
4656
4688
|
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4657
4689
|
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
@@ -4673,7 +4705,7 @@ function computeWow( current, previous ) {
|
|
|
4673
4705
|
|
|
4674
4706
|
async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
4675
4707
|
const rows = await processedchecklist.aggregate( [
|
|
4676
|
-
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd },
|
|
4708
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, checkListType: 'custom' } },
|
|
4677
4709
|
{ $group: {
|
|
4678
4710
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4679
4711
|
storeName: { $last: '$storeName' },
|
|
@@ -4713,26 +4745,33 @@ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
|
4713
4745
|
}
|
|
4714
4746
|
|
|
4715
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.
|
|
4716
4751
|
const rows = await recurringFlagTracker.aggregate( [
|
|
4717
4752
|
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4718
4753
|
{ $project: {
|
|
4719
4754
|
client_id: 1,
|
|
4720
4755
|
store_id: 1,
|
|
4721
4756
|
storeName: 1,
|
|
4722
|
-
|
|
4723
|
-
$
|
|
4724
|
-
$
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4728
|
-
},
|
|
4757
|
+
datesInWeek: {
|
|
4758
|
+
$filter: {
|
|
4759
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4760
|
+
as: 'd',
|
|
4761
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4729
4762
|
},
|
|
4730
4763
|
},
|
|
4731
4764
|
} },
|
|
4765
|
+
{ $unwind: '$datesInWeek' },
|
|
4732
4766
|
{ $group: {
|
|
4733
4767
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4734
4768
|
storeName: { $last: '$storeName' },
|
|
4735
|
-
|
|
4769
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4770
|
+
} },
|
|
4771
|
+
{ $project: {
|
|
4772
|
+
_id: 1,
|
|
4773
|
+
storeName: 1,
|
|
4774
|
+
count: { $size: '$uniqueDates' },
|
|
4736
4775
|
} },
|
|
4737
4776
|
] );
|
|
4738
4777
|
const map = new Map();
|
|
@@ -4747,180 +4786,382 @@ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
|
4747
4786
|
return map;
|
|
4748
4787
|
}
|
|
4749
4788
|
|
|
4750
|
-
function buildWeeklyWrapExcel( header,
|
|
4789
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4751
4790
|
const workbook = new ExcelJS.Workbook();
|
|
4752
4791
|
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4753
4792
|
sheet.addRow( [ header ] );
|
|
4754
4793
|
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4755
4794
|
sheet.addRow( [] );
|
|
4756
4795
|
sheet.columns = [
|
|
4757
|
-
{ header: 'Store ID', key: 'storeId', width: 18 },
|
|
4758
4796
|
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4797
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4798
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4759
4799
|
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4760
|
-
{ header: 'Not Submitted', key: 'timeFlag', width:
|
|
4761
|
-
{ 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 },
|
|
4762
4802
|
{ header: 'Recurring Flags', key: 'recurringFlag', width: 18 },
|
|
4763
|
-
{ header: 'Total Flags', key: 'totalFlags', width: 14 },
|
|
4764
|
-
{ header: 'Last Week Total', key: 'prevTotal', width: 18 },
|
|
4765
|
-
{ header: 'WoW %', key: 'wow', width: 12 },
|
|
4766
4803
|
];
|
|
4767
4804
|
sheet.getRow( 3 ).font = { bold: true };
|
|
4768
4805
|
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4769
|
-
for ( const r of
|
|
4806
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4770
4807
|
return workbook.xlsx.writeBuffer();
|
|
4771
4808
|
}
|
|
4772
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
|
+
|
|
4773
4946
|
export async function weeklyWrapAlert( req, res ) {
|
|
4774
4947
|
try {
|
|
4775
4948
|
const range = getLastWeekRange();
|
|
4776
4949
|
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4777
4950
|
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4778
4951
|
|
|
4779
|
-
//
|
|
4780
|
-
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 ) ) );
|
|
4781
4959
|
|
|
4782
|
-
// Aggregate flag data for both weeks across all stores in one shot.
|
|
4783
|
-
|
|
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( [
|
|
4784
4963
|
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4785
4964
|
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4786
4965
|
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4787
4966
|
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4967
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4968
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4788
4969
|
] );
|
|
4789
4970
|
|
|
4790
|
-
// Compute per-client highest flagged store and highest recurring flagged store (this week).
|
|
4791
|
-
const topByClient = new Map(); // clientId -> { topFlag, topRecurring }
|
|
4792
|
-
const upsertTop = ( client, key, candidate ) => {
|
|
4793
|
-
if ( !topByClient.has( client ) ) topByClient.set( client, { topFlag: null, topRecurring: null } );
|
|
4794
|
-
const entry = topByClient.get( client );
|
|
4795
|
-
if ( !entry[key] || candidate.value > entry[key].value ) entry[key] = candidate;
|
|
4796
|
-
};
|
|
4797
|
-
for ( const v of flagsThis.values() ) {
|
|
4798
|
-
upsertTop( v.client_id, 'topFlag', { storeId: v.store_id, storeName: v.storeName, value: v.totalFlags } );
|
|
4799
|
-
}
|
|
4800
|
-
for ( const v of recurThis.values() ) {
|
|
4801
|
-
upsertTop( v.client_id, 'topRecurring', { storeId: v.store_id, storeName: v.storeName, value: v.count } );
|
|
4802
|
-
}
|
|
4803
|
-
|
|
4804
4971
|
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4805
4972
|
const compiled = handlebars.compile( fileContent );
|
|
4806
4973
|
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4807
4974
|
const sentSummary = [];
|
|
4808
4975
|
|
|
4809
|
-
// 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.
|
|
4810
4977
|
const clientIds = new Set();
|
|
4811
|
-
for ( const v of flagsThis.values() )
|
|
4812
|
-
|
|
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
|
+
}
|
|
4813
4984
|
|
|
4814
4985
|
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4815
4986
|
try {
|
|
4816
|
-
|
|
4817
|
-
|
|
4818
|
-
|
|
4819
|
-
|
|
4820
|
-
|
|
4821
|
-
|
|
4822
|
-
|
|
4823
|
-
|
|
4824
|
-
|
|
4825
|
-
|
|
4826
|
-
|
|
4827
|
-
|
|
4828
|
-
// Iterate only stores belonging to this client (key prefix match).
|
|
4829
|
-
const clientPrefix = `${clientId}::`;
|
|
4830
|
-
const clientStoreKeys = new Set();
|
|
4831
|
-
for ( const k of flagsThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4832
|
-
for ( const k of flagsPrev.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4833
|
-
for ( const k of recurThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
|
|
4834
|
-
|
|
4835
|
-
for ( const k of clientStoreKeys ) {
|
|
4836
|
-
const cur = flagsThis.get( k );
|
|
4837
|
-
const prev = flagsPrev.get( k );
|
|
4838
|
-
const recCur = recurThis.get( k );
|
|
4839
|
-
const recPrev = recurPrev.get( k );
|
|
4840
|
-
const storeRecurring = recCur?.count || 0;
|
|
4841
|
-
const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
|
|
4842
|
-
const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
|
|
4843
|
-
|
|
4844
|
-
if ( storeTotal > 0 ) storesFlagged += 1;
|
|
4845
|
-
if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
|
|
4846
|
-
totalFlags += storeTotal;
|
|
4847
|
-
prevTotalFlags += prevStoreTotal;
|
|
4848
|
-
questionFlags += cur?.questionFlag || 0;
|
|
4849
|
-
notSubmittedFlags += cur?.timeFlag || 0;
|
|
4850
|
-
runAIFlags += cur?.runAIFlag || 0;
|
|
4851
|
-
recurringFlags += storeRecurring;
|
|
4852
|
-
|
|
4853
|
-
( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
|
|
4854
|
-
( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
|
|
4855
|
-
|
|
4856
|
-
if ( storeTotal > 0 || prevStoreTotal > 0 ) {
|
|
4857
|
-
const w = computeWow( storeTotal, prevStoreTotal );
|
|
4858
|
-
excelRows.push( {
|
|
4859
|
-
storeId: ( cur?.store_id || prev?.store_id || k.slice( clientPrefix.length ) ),
|
|
4860
|
-
storeName: cur?.storeName || prev?.storeName || '',
|
|
4861
|
-
questionFlag: cur?.questionFlag || 0,
|
|
4862
|
-
timeFlag: cur?.timeFlag || 0,
|
|
4863
|
-
runAIFlag: cur?.runAIFlag || 0,
|
|
4864
|
-
recurringFlag: storeRecurring,
|
|
4865
|
-
totalFlags: storeTotal,
|
|
4866
|
-
prevTotal: prevStoreTotal,
|
|
4867
|
-
wow: w.value ? `${w.direction === 'up' ? '+' : '-'}${w.value}` : '0%',
|
|
4868
|
-
} );
|
|
4869
|
-
}
|
|
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;
|
|
4870
4998
|
}
|
|
4871
4999
|
|
|
4872
|
-
|
|
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
|
+
}
|
|
4873
5009
|
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
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
|
+
}
|
|
4885
5023
|
|
|
4886
|
-
|
|
4887
|
-
|
|
4888
|
-
|
|
4889
|
-
|
|
4890
|
-
|
|
4891
|
-
|
|
4892
|
-
|
|
4893
|
-
|
|
4894
|
-
|
|
4895
|
-
|
|
4896
|
-
|
|
4897
|
-
|
|
4898
|
-
|
|
4899
|
-
|
|
4900
|
-
|
|
4901
|
-
|
|
4902
|
-
|
|
4903
|
-
|
|
4904
|
-
|
|
4905
|
-
|
|
4906
|
-
|
|
4907
|
-
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
|
|
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
|
+
}
|
|
4912
5067
|
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
|
|
4917
|
-
|
|
4918
|
-
|
|
4919
|
-
|
|
4920
|
-
|
|
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})`;
|
|
4921
5158
|
|
|
4922
|
-
|
|
4923
|
-
|
|
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
|
+
}
|
|
4924
5165
|
} catch ( e ) {
|
|
4925
5166
|
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
4926
5167
|
}
|