tango-app-api-trax 3.8.25 → 3.8.27
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
CHANGED
|
@@ -4360,7 +4360,6 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4360
4360
|
],
|
|
4361
4361
|
},
|
|
4362
4362
|
}, { _id: 1, checkListName: 1, recurringFlag: 1, notifyFlags: 1, approver: 1, client_id: 1 } );
|
|
4363
|
-
console.log( JSON.stringify( checklistDetails ) );
|
|
4364
4363
|
if ( !checklistDetails.length ) {
|
|
4365
4364
|
return res.sendSuccess( 'No checklists configured for recurring flag' );
|
|
4366
4365
|
}
|
|
@@ -4390,6 +4389,20 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4390
4389
|
|
|
4391
4390
|
if ( !recipients.length ) return;
|
|
4392
4391
|
|
|
4392
|
+
// Resolve each recipient's allowed-store scope once per checklist:
|
|
4393
|
+
// allowed === null → full access (superadmin / non-client userType / external recipient not in users collection)
|
|
4394
|
+
// allowed === Set → restrict store-based rows to these storeIds
|
|
4395
|
+
// User-based tracker rows pass through regardless (no store binding).
|
|
4396
|
+
const recipientFilters = await Promise.all( recipients.map( async ( recipient ) => {
|
|
4397
|
+
const userDetails = await userService.findOne(
|
|
4398
|
+
{ email: recipient },
|
|
4399
|
+
{ email: 1, assignedStores: 1, userType: 1, role: 1, clientId: 1 },
|
|
4400
|
+
);
|
|
4401
|
+
// Unknown recipients (external approvers etc.) keep full access — preserves existing behavior.
|
|
4402
|
+
const allowed = userDetails ? await resolveUserAssignedStores( userDetails ) : null;
|
|
4403
|
+
return { recipient, allowed };
|
|
4404
|
+
} ) );
|
|
4405
|
+
|
|
4393
4406
|
// Read tracker rows where EITHER the sop streak or the runAI count has hit threshold and the
|
|
4394
4407
|
// current trigger hasn't been emailed yet (lastEmailDate vs the relevant flagged date).
|
|
4395
4408
|
const trackerRows = await recurringFlagTracker.find( {
|
|
@@ -4414,7 +4427,12 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4414
4427
|
// Determine which streak crossed threshold for this row — drives reset granularity below.
|
|
4415
4428
|
const sopFired = ( t.consecutiveCount || 0 ) >= threshold && t.lastEmailDate !== t.lastFlaggedDate;
|
|
4416
4429
|
const runAIFired = ( t.runAICount || 0 ) >= threshold && t.lastEmailDate !== t.lastRunAIFlaggedDate;
|
|
4417
|
-
|
|
4430
|
+
|
|
4431
|
+
let rowEmitted = false;
|
|
4432
|
+
for ( const { recipient, allowed } of recipientFilters ) {
|
|
4433
|
+
// Skip store-based rows outside the recipient's reachable stores. Full-access (null) and
|
|
4434
|
+
// user-based rows always pass through.
|
|
4435
|
+
if ( allowed !== null && !isUserBased && !allowed.has( t.store_id ) ) continue;
|
|
4418
4436
|
triggers.push( {
|
|
4419
4437
|
recipient,
|
|
4420
4438
|
clientId: t.client_id,
|
|
@@ -4438,14 +4456,19 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4438
4456
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4439
4457
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4440
4458
|
} );
|
|
4459
|
+
rowEmitted = true;
|
|
4460
|
+
}
|
|
4461
|
+
// Only reset rows that actually went into at least one recipient's email — otherwise a row
|
|
4462
|
+
// visible to nobody would silently zero its streak without an email being sent.
|
|
4463
|
+
if ( rowEmitted ) {
|
|
4464
|
+
trackerIdsToReset.push( {
|
|
4465
|
+
_id: t._id,
|
|
4466
|
+
lastFlaggedDate: t.lastFlaggedDate,
|
|
4467
|
+
lastRunAIFlaggedDate: t.lastRunAIFlaggedDate,
|
|
4468
|
+
sopFired,
|
|
4469
|
+
runAIFired,
|
|
4470
|
+
} );
|
|
4441
4471
|
}
|
|
4442
|
-
trackerIdsToReset.push( {
|
|
4443
|
-
_id: t._id,
|
|
4444
|
-
lastFlaggedDate: t.lastFlaggedDate,
|
|
4445
|
-
lastRunAIFlaggedDate: t.lastRunAIFlaggedDate,
|
|
4446
|
-
sopFired,
|
|
4447
|
-
runAIFired,
|
|
4448
|
-
} );
|
|
4449
4472
|
}
|
|
4450
4473
|
} ) );
|
|
4451
4474
|
|
|
@@ -4470,9 +4493,11 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4470
4493
|
await Promise.all( [ ...byRecipient.entries() ].map( async ( [ recipient, items ] ) => {
|
|
4471
4494
|
const subjects = new Set( items.map( ( i ) => i.subjectId ) );
|
|
4472
4495
|
const checklists = new Set( items.map( ( i ) => i.checklistId ) );
|
|
4473
|
-
console.log( checklists );
|
|
4474
4496
|
const isMultiStore = subjects.size > 1; // "isMultiStore" name retained for template back-compat
|
|
4475
4497
|
const isMultiChecklist = !isMultiStore && checklists.size > 1;
|
|
4498
|
+
// Sub-mode of multi-store when the recipient's flagged subjects all share a single checklist.
|
|
4499
|
+
// Drives a tighter email layout (no Checklist column, no "Total Checklists" line, checklist name in intro).
|
|
4500
|
+
const isMultiStoreSingleChecklist = isMultiStore && checklists.size === 1;
|
|
4476
4501
|
// Threshold for the message line — when grouping spans multiple checklists/subjects, take min threshold seen.
|
|
4477
4502
|
const thresholdShown = items.reduce( ( acc, it ) => Math.min( acc, it.days ), items[0].days );
|
|
4478
4503
|
|
|
@@ -4521,9 +4546,10 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4521
4546
|
checklistName: g.checklistName,
|
|
4522
4547
|
lastSubmittedBy: g.lastSubmittedBy,
|
|
4523
4548
|
lastSubmissionDate: g.lastSubmissionDate,
|
|
4524
|
-
days: g.questionCount, //
|
|
4549
|
+
days: g.questionCount, // legacy alias kept for back-compat with older template builds
|
|
4525
4550
|
flagCount: g.questionCount,
|
|
4526
4551
|
runAICount: g.runAICount,
|
|
4552
|
+
totalFlags: g.questionCount + g.runAICount,
|
|
4527
4553
|
} ) );
|
|
4528
4554
|
|
|
4529
4555
|
// Excel attachment keeps per-question detail (one row per flagged question).
|
|
@@ -4546,6 +4572,7 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4546
4572
|
threshold: thresholdShown,
|
|
4547
4573
|
isMultiStore,
|
|
4548
4574
|
isMultiChecklist,
|
|
4575
|
+
isMultiStoreSingleChecklist,
|
|
4549
4576
|
showTable: isMultiStore || isMultiChecklist,
|
|
4550
4577
|
hasAttachment,
|
|
4551
4578
|
domain: flagDomain,
|
|
@@ -4563,11 +4590,15 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4563
4590
|
totalChecklists: checklists.size,
|
|
4564
4591
|
totalFlags: items.length,
|
|
4565
4592
|
};
|
|
4593
|
+
if ( isMultiStoreSingleChecklist ) {
|
|
4594
|
+
// Show the single checklist name in the intro line for this sub-mode.
|
|
4595
|
+
data.checklistName = items[0].checklistName;
|
|
4596
|
+
}
|
|
4566
4597
|
} else if ( isMultiChecklist ) {
|
|
4567
4598
|
data.subjectName = items[0].subjectName;
|
|
4568
4599
|
data.storeName = items[0].subjectName;
|
|
4569
4600
|
} else {
|
|
4570
|
-
// Single mode: one (subject, checklist) —
|
|
4601
|
+
// Single mode: one (subject, checklist) — totalFlags = sop question flags + runAI flags.
|
|
4571
4602
|
const single = rows[0];
|
|
4572
4603
|
data.subjectName = single.subjectName;
|
|
4573
4604
|
data.storeName = single.subjectName;
|
|
@@ -4575,9 +4606,8 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4575
4606
|
data.lastSubmittedBy = single.lastSubmittedBy;
|
|
4576
4607
|
data.lastSubmissionDate = single.lastSubmissionDate;
|
|
4577
4608
|
data.flagCount = single.flagCount;
|
|
4578
|
-
data.flagCountPlural = single.flagCount > 1;
|
|
4579
4609
|
data.runAICount = single.runAICount;
|
|
4580
|
-
data.
|
|
4610
|
+
data.totalFlags = single.totalFlags;
|
|
4581
4611
|
}
|
|
4582
4612
|
|
|
4583
4613
|
const html = compiled( { data } );
|
|
@@ -4602,7 +4632,6 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4602
4632
|
logger.error( { functionName: 'recurringFlagAlert.buildExcel', error: e } );
|
|
4603
4633
|
}
|
|
4604
4634
|
}
|
|
4605
|
-
console.log( params.toEmail );
|
|
4606
4635
|
sendEmailWithSES( params.toEmail, params.mailSubject, params.htmlBody, params.attachment, params.sourceEmail );
|
|
4607
4636
|
sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
|
|
4608
4637
|
} ) );
|
|
@@ -4643,10 +4672,12 @@ function getLastWeekRange( ref = dayjs() ) {
|
|
|
4643
4672
|
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4644
4673
|
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4645
4674
|
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4646
|
-
|
|
4647
|
-
const
|
|
4648
|
-
const
|
|
4649
|
-
|
|
4675
|
+
// Current week (Mon..Sun) — primary range surfaced in the email.
|
|
4676
|
+
const weekStart = thisMonday;
|
|
4677
|
+
const weekEnd = thisMonday.add( 6, 'day' );
|
|
4678
|
+
// Previous full week (Mon..Sun) — used for WoW comparisons.
|
|
4679
|
+
const prevWeekStart = thisMonday.subtract( 7, 'day' );
|
|
4680
|
+
const prevWeekEnd = thisMonday.subtract( 1, 'day' );
|
|
4650
4681
|
return {
|
|
4651
4682
|
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4652
4683
|
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
@@ -4708,26 +4739,33 @@ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
|
4708
4739
|
}
|
|
4709
4740
|
|
|
4710
4741
|
async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
4742
|
+
// Counts how many distinct days within the week the store received a recurring-flag email.
|
|
4743
|
+
// Each tracker doc is per-question, so a single email covering N flagged questions adds the same date
|
|
4744
|
+
// to N tracker docs — we de-dupe by date per store via $addToSet.
|
|
4711
4745
|
const rows = await recurringFlagTracker.aggregate( [
|
|
4712
4746
|
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4713
4747
|
{ $project: {
|
|
4714
4748
|
client_id: 1,
|
|
4715
4749
|
store_id: 1,
|
|
4716
4750
|
storeName: 1,
|
|
4717
|
-
|
|
4718
|
-
$
|
|
4719
|
-
$
|
|
4720
|
-
|
|
4721
|
-
|
|
4722
|
-
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4723
|
-
},
|
|
4751
|
+
datesInWeek: {
|
|
4752
|
+
$filter: {
|
|
4753
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4754
|
+
as: 'd',
|
|
4755
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4724
4756
|
},
|
|
4725
4757
|
},
|
|
4726
4758
|
} },
|
|
4759
|
+
{ $unwind: '$datesInWeek' },
|
|
4727
4760
|
{ $group: {
|
|
4728
4761
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4729
4762
|
storeName: { $last: '$storeName' },
|
|
4730
|
-
|
|
4763
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4764
|
+
} },
|
|
4765
|
+
{ $project: {
|
|
4766
|
+
_id: 1,
|
|
4767
|
+
storeName: 1,
|
|
4768
|
+
count: { $size: '$uniqueDates' },
|
|
4731
4769
|
} },
|
|
4732
4770
|
] );
|
|
4733
4771
|
const map = new Map();
|
|
@@ -4742,180 +4780,382 @@ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
|
4742
4780
|
return map;
|
|
4743
4781
|
}
|
|
4744
4782
|
|
|
4745
|
-
function buildWeeklyWrapExcel( header,
|
|
4783
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4746
4784
|
const workbook = new ExcelJS.Workbook();
|
|
4747
4785
|
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4748
4786
|
sheet.addRow( [ header ] );
|
|
4749
4787
|
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4750
4788
|
sheet.addRow( [] );
|
|
4751
4789
|
sheet.columns = [
|
|
4752
|
-
{ header: 'Store ID', key: 'storeId', width: 18 },
|
|
4753
4790
|
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4791
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4792
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4754
4793
|
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4755
|
-
{ header: 'Not Submitted', key: 'timeFlag', width:
|
|
4756
|
-
{ header: 'Run AI Flags', key: 'runAIFlag', width:
|
|
4794
|
+
{ header: 'Not Submitted Flags', key: 'timeFlag', width: 20 },
|
|
4795
|
+
{ header: 'Run AI Flags', key: 'runAIFlag', width: 14 },
|
|
4757
4796
|
{ 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
4797
|
];
|
|
4762
4798
|
sheet.getRow( 3 ).font = { bold: true };
|
|
4763
4799
|
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4764
|
-
for ( const r of
|
|
4800
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4765
4801
|
return workbook.xlsx.writeBuffer();
|
|
4766
4802
|
}
|
|
4767
4803
|
|
|
4804
|
+
// Per-(store, checklist) flag aggregation for the weekly Excel attachment. Same source as
|
|
4805
|
+
// aggregateWeeklyFlagsByStore but additionally grouped by sourceCheckList_id so each row in the export
|
|
4806
|
+
// corresponds to one store × one checklist.
|
|
4807
|
+
async function aggregateWeeklyFlagsByStoreChecklist( weekStart, weekEnd ) {
|
|
4808
|
+
const rows = await processedchecklist.aggregate( [
|
|
4809
|
+
{ $match: { date_string: { $gte: weekStart, $lte: weekEnd }, isdeleted: { $ne: true } } },
|
|
4810
|
+
{ $group: {
|
|
4811
|
+
_id: { client_id: '$client_id', store_id: '$store_id', sourceCheckList_id: '$sourceCheckList_id' },
|
|
4812
|
+
storeName: { $last: '$storeName' },
|
|
4813
|
+
checkListName: { $last: '$checkListName' },
|
|
4814
|
+
questionFlag: { $sum: { $ifNull: [ '$questionFlag', 0 ] } },
|
|
4815
|
+
timeFlag: { $sum: { $ifNull: [ '$timeFlag', 0 ] } },
|
|
4816
|
+
runAIFlag: { $sum: { $ifNull: [ '$runAIFlag', 0 ] } },
|
|
4817
|
+
} },
|
|
4818
|
+
] );
|
|
4819
|
+
const map = new Map();
|
|
4820
|
+
for ( const r of rows ) {
|
|
4821
|
+
const key = `${r._id.client_id}::${r._id.store_id}::${r._id.sourceCheckList_id}`;
|
|
4822
|
+
map.set( key, {
|
|
4823
|
+
client_id: r._id.client_id,
|
|
4824
|
+
store_id: r._id.store_id,
|
|
4825
|
+
sourceCheckList_id: String( r._id.sourceCheckList_id || '' ),
|
|
4826
|
+
storeName: r.storeName,
|
|
4827
|
+
checkListName: r.checkListName,
|
|
4828
|
+
questionFlag: r.questionFlag || 0,
|
|
4829
|
+
timeFlag: r.timeFlag || 0,
|
|
4830
|
+
runAIFlag: r.runAIFlag || 0,
|
|
4831
|
+
} );
|
|
4832
|
+
}
|
|
4833
|
+
return map;
|
|
4834
|
+
}
|
|
4835
|
+
|
|
4836
|
+
// Per-(store, checklist) recurring count = distinct days the (store, checklist) was emailed in the week.
|
|
4837
|
+
async function aggregateWeeklyRecurringByStoreChecklist( weekStart, weekEnd ) {
|
|
4838
|
+
const rows = await recurringFlagTracker.aggregate( [
|
|
4839
|
+
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4840
|
+
{ $project: {
|
|
4841
|
+
client_id: 1,
|
|
4842
|
+
store_id: 1,
|
|
4843
|
+
storeName: 1,
|
|
4844
|
+
sourceCheckList_id: 1,
|
|
4845
|
+
checkListName: 1,
|
|
4846
|
+
datesInWeek: {
|
|
4847
|
+
$filter: {
|
|
4848
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4849
|
+
as: 'd',
|
|
4850
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4851
|
+
},
|
|
4852
|
+
},
|
|
4853
|
+
} },
|
|
4854
|
+
{ $unwind: '$datesInWeek' },
|
|
4855
|
+
{ $group: {
|
|
4856
|
+
_id: { client_id: '$client_id', store_id: '$store_id', sourceCheckList_id: '$sourceCheckList_id' },
|
|
4857
|
+
storeName: { $last: '$storeName' },
|
|
4858
|
+
checkListName: { $last: '$checkListName' },
|
|
4859
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4860
|
+
} },
|
|
4861
|
+
{ $project: {
|
|
4862
|
+
_id: 1,
|
|
4863
|
+
storeName: 1,
|
|
4864
|
+
checkListName: 1,
|
|
4865
|
+
count: { $size: '$uniqueDates' },
|
|
4866
|
+
} },
|
|
4867
|
+
] );
|
|
4868
|
+
const map = new Map();
|
|
4869
|
+
for ( const r of rows ) {
|
|
4870
|
+
const key = `${r._id.client_id}::${r._id.store_id}::${r._id.sourceCheckList_id}`;
|
|
4871
|
+
map.set( key, {
|
|
4872
|
+
client_id: r._id.client_id,
|
|
4873
|
+
store_id: r._id.store_id,
|
|
4874
|
+
sourceCheckList_id: String( r._id.sourceCheckList_id || '' ),
|
|
4875
|
+
storeName: r.storeName,
|
|
4876
|
+
checkListName: r.checkListName,
|
|
4877
|
+
count: r.count,
|
|
4878
|
+
} );
|
|
4879
|
+
}
|
|
4880
|
+
return map;
|
|
4881
|
+
}
|
|
4882
|
+
|
|
4883
|
+
// Resolves the set of storeIds a user can see. Returns:
|
|
4884
|
+
// null — full access (superadmin or non-client userType); caller should not filter
|
|
4885
|
+
// Set<storeId> — restricted access; iterate this user's reachable stores
|
|
4886
|
+
// Mirrors the existing pattern at internalTrax.controller.js ~L4192 (assignedStores + clusters where the
|
|
4887
|
+
// user is a Teamlead + teams the user leads + teams the user is a member of, all expanded to storeIds).
|
|
4888
|
+
async function resolveUserAssignedStores( userDetails ) {
|
|
4889
|
+
if ( !userDetails ) return new Set();
|
|
4890
|
+
if ( userDetails.userType !== 'client' || userDetails.role === 'superadmin' ) {
|
|
4891
|
+
return null;
|
|
4892
|
+
}
|
|
4893
|
+
const storeIds = new Set( ( userDetails.assignedStores || [] ).map( ( s ) => s?.storeId ).filter( Boolean ) );
|
|
4894
|
+
|
|
4895
|
+
const [ leadClusters, leadTeams ] = await Promise.all( [
|
|
4896
|
+
clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
|
|
4897
|
+
teamsServices.findteams( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
|
|
4898
|
+
] );
|
|
4899
|
+
|
|
4900
|
+
for ( const cluster of leadClusters || [] ) {
|
|
4901
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4902
|
+
}
|
|
4903
|
+
|
|
4904
|
+
// Teams the user leads — pull each member's assignedStores + clusters where THAT member is Teamlead.
|
|
4905
|
+
for ( const team of leadTeams || [] ) {
|
|
4906
|
+
for ( const member of team.users || [] ) {
|
|
4907
|
+
const memberDetails = await userService.findOne( { _id: member.userId } );
|
|
4908
|
+
if ( memberDetails?.assignedStores?.length ) {
|
|
4909
|
+
memberDetails.assignedStores.forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4910
|
+
}
|
|
4911
|
+
if ( memberDetails?.email ) {
|
|
4912
|
+
const memberClusters = await clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: memberDetails.email } } } );
|
|
4913
|
+
for ( const cluster of memberClusters || [] ) {
|
|
4914
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4915
|
+
}
|
|
4916
|
+
}
|
|
4917
|
+
}
|
|
4918
|
+
}
|
|
4919
|
+
|
|
4920
|
+
// Teams the user is a member of — pull clusters that reference those teams.
|
|
4921
|
+
const memberTeams = await teamsServices.findteams( { clientId: userDetails.clientId, users: { $elemMatch: { email: userDetails.email } } } );
|
|
4922
|
+
for ( const team of memberTeams || [] ) {
|
|
4923
|
+
const clusters = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
|
|
4924
|
+
for ( const cluster of clusters || [] ) {
|
|
4925
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4926
|
+
}
|
|
4927
|
+
}
|
|
4928
|
+
|
|
4929
|
+
// Teams the user leads — also pull clusters that reference those teams.
|
|
4930
|
+
for ( const team of leadTeams || [] ) {
|
|
4931
|
+
const clusters = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
|
|
4932
|
+
for ( const cluster of clusters || [] ) {
|
|
4933
|
+
( cluster.stores || [] ).forEach( ( s ) => s?.storeId && storeIds.add( s.storeId ) );
|
|
4934
|
+
}
|
|
4935
|
+
}
|
|
4936
|
+
|
|
4937
|
+
return storeIds;
|
|
4938
|
+
}
|
|
4939
|
+
|
|
4768
4940
|
export async function weeklyWrapAlert( req, res ) {
|
|
4769
4941
|
try {
|
|
4770
4942
|
const range = getLastWeekRange();
|
|
4771
4943
|
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4772
4944
|
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4773
4945
|
|
|
4774
|
-
//
|
|
4775
|
-
const
|
|
4946
|
+
// Only send to clients that opted in via weeklyFlagEmail. Build the allow-list once.
|
|
4947
|
+
const enabledClients = await clientService.find( { weeklyFlagEmail: true }, { clientId: 1 } );
|
|
4948
|
+
console.log( enabledClients );
|
|
4949
|
+
if ( !enabledClients.length ) {
|
|
4950
|
+
return res.sendSuccess( { message: 'No clients configured for weeklyFlagEmail', sent: [] } );
|
|
4951
|
+
}
|
|
4952
|
+
const enabledClientIds = new Set( enabledClients.map( ( c ) => String( c.clientId ) ) );
|
|
4776
4953
|
|
|
4777
|
-
// Aggregate flag data for both weeks across all stores in one shot.
|
|
4778
|
-
|
|
4954
|
+
// Aggregate flag data for both weeks across all stores in one shot. The per-(store, checklist)
|
|
4955
|
+
// maps drive the Excel attachment rows; the per-store maps drive the email body's totals + top picks.
|
|
4956
|
+
const [ flagsThis, flagsPrev, recurThis, recurPrev, flagsThisCL, recurThisCL ] = await Promise.all( [
|
|
4779
4957
|
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4780
4958
|
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4781
4959
|
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4782
4960
|
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4961
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4962
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4783
4963
|
] );
|
|
4784
4964
|
|
|
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
4965
|
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4800
4966
|
const compiled = handlebars.compile( fileContent );
|
|
4801
4967
|
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4802
4968
|
const sentSummary = [];
|
|
4803
4969
|
|
|
4804
|
-
// Build one digest per client present in the data
|
|
4970
|
+
// Build one digest per client present in the data, restricted to clients that opted in.
|
|
4805
4971
|
const clientIds = new Set();
|
|
4806
|
-
for ( const v of flagsThis.values() )
|
|
4807
|
-
|
|
4972
|
+
for ( const v of flagsThis.values() ) {
|
|
4973
|
+
if ( enabledClientIds.has( String( v.client_id ) ) ) clientIds.add( v.client_id );
|
|
4974
|
+
}
|
|
4975
|
+
for ( const v of recurThis.values() ) {
|
|
4976
|
+
if ( enabledClientIds.has( String( v.client_id ) ) ) clientIds.add( v.client_id );
|
|
4977
|
+
}
|
|
4808
4978
|
|
|
4809
4979
|
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4810
4980
|
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
|
-
}
|
|
4981
|
+
// Recipients = admin + superadmin users for this client. Each user's digest is scoped via
|
|
4982
|
+
// resolveUserAssignedStores — superadmin / tango users see everything, regular admins see only
|
|
4983
|
+
// stores they own (assignedStores + clusters they lead + teams they're in/lead).
|
|
4984
|
+
const adminUsers = await userService.find(
|
|
4985
|
+
{ clientId: String( clientId ), email: { $in: [ 'gopisjkg@gmail.com', 'sh9628hs@gmail.com' ] }, role: { $in: [ 'admin', 'superadmin' ] } },
|
|
4986
|
+
{ email: 1, assignedStores: 1, userName: 1, userType: 1, role: 1, clientId: 1 },
|
|
4987
|
+
);
|
|
4988
|
+
console.log( adminUsers );
|
|
4989
|
+
if ( !adminUsers.length ) {
|
|
4990
|
+
logger.info( `[weeklyWrapAlert] no admin/superadmin users for client ${clientId}, skipping` );
|
|
4991
|
+
return;
|
|
4865
4992
|
}
|
|
4866
4993
|
|
|
4867
|
-
|
|
4994
|
+
for ( const user of adminUsers ) {
|
|
4995
|
+
try {
|
|
4996
|
+
if ( !user?.email ) continue;
|
|
4997
|
+
const allowed = await resolveUserAssignedStores( user );
|
|
4998
|
+
// null = full access (superadmin / tango). Empty set = no reachable stores → skip.
|
|
4999
|
+
if ( allowed && allowed.size === 0 ) {
|
|
5000
|
+
logger.info( `[weeklyWrapAlert] user ${user.email} has no reachable stores, skipping` );
|
|
5001
|
+
continue;
|
|
5002
|
+
}
|
|
4868
5003
|
|
|
4869
|
-
|
|
4870
|
-
|
|
4871
|
-
|
|
4872
|
-
|
|
4873
|
-
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
5004
|
+
// Iterate stores belonging to this client; intersect with the user's allowed set when present.
|
|
5005
|
+
const clientPrefix = `${clientId}::`;
|
|
5006
|
+
const isAllowed = ( storeId ) => allowed === null || allowed.has( storeId );
|
|
5007
|
+
const clientStoreKeys = new Set();
|
|
5008
|
+
for ( const k of flagsThis.keys() ) {
|
|
5009
|
+
if ( k.startsWith( clientPrefix ) && isAllowed( k.slice( clientPrefix.length ) ) ) clientStoreKeys.add( k );
|
|
5010
|
+
}
|
|
5011
|
+
for ( const k of flagsPrev.keys() ) {
|
|
5012
|
+
if ( k.startsWith( clientPrefix ) && isAllowed( k.slice( clientPrefix.length ) ) ) clientStoreKeys.add( k );
|
|
5013
|
+
}
|
|
5014
|
+
for ( const k of recurThis.keys() ) {
|
|
5015
|
+
if ( k.startsWith( clientPrefix ) && isAllowed( k.slice( clientPrefix.length ) ) ) clientStoreKeys.add( k );
|
|
5016
|
+
}
|
|
4880
5017
|
|
|
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
|
-
|
|
5018
|
+
let storesFlagged = 0;
|
|
5019
|
+
let totalFlags = 0;
|
|
5020
|
+
let prevTotalFlags = 0;
|
|
5021
|
+
let prevStoresFlagged = 0;
|
|
5022
|
+
const flaggedChecklistsThis = new Set();
|
|
5023
|
+
const flaggedChecklistsPrev = new Set();
|
|
5024
|
+
let questionFlags = 0;
|
|
5025
|
+
let notSubmittedFlags = 0;
|
|
5026
|
+
let runAIFlags = 0;
|
|
5027
|
+
let recurringFlags = 0;
|
|
5028
|
+
const excelRows = [];
|
|
5029
|
+
// Track top flagged + top recurring within this user's assigned stores only.
|
|
5030
|
+
let topFlag = null;
|
|
5031
|
+
let topRecurring = null;
|
|
5032
|
+
|
|
5033
|
+
for ( const k of clientStoreKeys ) {
|
|
5034
|
+
const cur = flagsThis.get( k );
|
|
5035
|
+
const prev = flagsPrev.get( k );
|
|
5036
|
+
const recCur = recurThis.get( k );
|
|
5037
|
+
const recPrev = recurPrev.get( k );
|
|
5038
|
+
const storeRecurring = recCur?.count || 0;
|
|
5039
|
+
const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
|
|
5040
|
+
const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
|
|
5041
|
+
|
|
5042
|
+
if ( storeTotal > 0 ) storesFlagged += 1;
|
|
5043
|
+
if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
|
|
5044
|
+
totalFlags += storeTotal;
|
|
5045
|
+
prevTotalFlags += prevStoreTotal;
|
|
5046
|
+
questionFlags += cur?.questionFlag || 0;
|
|
5047
|
+
notSubmittedFlags += cur?.timeFlag || 0;
|
|
5048
|
+
runAIFlags += cur?.runAIFlag || 0;
|
|
5049
|
+
recurringFlags += storeRecurring;
|
|
5050
|
+
|
|
5051
|
+
( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
|
|
5052
|
+
( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
|
|
5053
|
+
|
|
5054
|
+
if ( cur && ( !topFlag || ( cur.totalFlags || 0 ) > topFlag.value ) ) {
|
|
5055
|
+
topFlag = { storeId: cur.store_id, storeName: cur.storeName, value: cur.totalFlags || 0 };
|
|
5056
|
+
}
|
|
5057
|
+
if ( recCur && ( !topRecurring || ( recCur.count || 0 ) > topRecurring.value ) ) {
|
|
5058
|
+
topRecurring = { storeId: recCur.store_id, storeName: recCur.storeName, value: recCur.count || 0 };
|
|
5059
|
+
}
|
|
5060
|
+
}
|
|
4907
5061
|
|
|
4908
|
-
|
|
4909
|
-
|
|
4910
|
-
|
|
4911
|
-
content: attachmentBuffer,
|
|
4912
|
-
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
4913
|
-
};
|
|
4914
|
-
const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
|
|
4915
|
-
const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
|
|
5062
|
+
if ( totalFlags === 0 && prevTotalFlags === 0 ) {
|
|
5063
|
+
continue; // user has nothing to report this week
|
|
5064
|
+
}
|
|
4916
5065
|
|
|
4917
|
-
|
|
4918
|
-
|
|
5066
|
+
// Build the Excel attachment as one row per (store, checklist) the user can see.
|
|
5067
|
+
// Pulls from the per-checklist flag aggregation + per-checklist recurring email count.
|
|
5068
|
+
const seenStoreChecklist = new Set();
|
|
5069
|
+
for ( const k of flagsThisCL.keys() ) {
|
|
5070
|
+
if ( !k.startsWith( clientPrefix ) ) continue;
|
|
5071
|
+
const cur = flagsThisCL.get( k );
|
|
5072
|
+
if ( !isAllowed( cur.store_id ) ) continue;
|
|
5073
|
+
const recCur = recurThisCL.get( k );
|
|
5074
|
+
const recurringCount = recCur?.count || 0;
|
|
5075
|
+
const flags = ( cur.questionFlag || 0 ) + ( cur.timeFlag || 0 ) + ( cur.runAIFlag || 0 ) + recurringCount;
|
|
5076
|
+
if ( flags === 0 ) continue;
|
|
5077
|
+
excelRows.push( {
|
|
5078
|
+
storeName: cur.storeName || '',
|
|
5079
|
+
checkListName: cur.checkListName || '',
|
|
5080
|
+
flags,
|
|
5081
|
+
questionFlag: cur.questionFlag || 0,
|
|
5082
|
+
timeFlag: cur.timeFlag || 0,
|
|
5083
|
+
runAIFlag: cur.runAIFlag || 0,
|
|
5084
|
+
recurringFlag: recurringCount,
|
|
5085
|
+
} );
|
|
5086
|
+
seenStoreChecklist.add( k );
|
|
5087
|
+
}
|
|
5088
|
+
// (store, checklist) pairs that hit recurring threshold but had no flag aggregation entry.
|
|
5089
|
+
for ( const k of recurThisCL.keys() ) {
|
|
5090
|
+
if ( !k.startsWith( clientPrefix ) ) continue;
|
|
5091
|
+
if ( seenStoreChecklist.has( k ) ) continue;
|
|
5092
|
+
const recCur = recurThisCL.get( k );
|
|
5093
|
+
if ( !isAllowed( recCur.store_id ) ) continue;
|
|
5094
|
+
if ( !recCur.count ) continue;
|
|
5095
|
+
excelRows.push( {
|
|
5096
|
+
storeName: recCur.storeName || '',
|
|
5097
|
+
checkListName: recCur.checkListName || '',
|
|
5098
|
+
flags: recCur.count,
|
|
5099
|
+
questionFlag: 0,
|
|
5100
|
+
timeFlag: 0,
|
|
5101
|
+
runAIFlag: 0,
|
|
5102
|
+
recurringFlag: recCur.count,
|
|
5103
|
+
} );
|
|
5104
|
+
}
|
|
5105
|
+
|
|
5106
|
+
let highestFlaggedStoreWow = { value: '', direction: 'up' };
|
|
5107
|
+
if ( topFlag ) {
|
|
5108
|
+
const prev = flagsPrev.get( `${clientId}::${topFlag.storeId}` );
|
|
5109
|
+
highestFlaggedStoreWow = computeWow( topFlag.value, prev?.totalFlags || 0 );
|
|
5110
|
+
}
|
|
5111
|
+
let highestRecurringStoreWow = { value: '', direction: 'up' };
|
|
5112
|
+
if ( topRecurring ) {
|
|
5113
|
+
const prev = recurPrev.get( `${clientId}::${topRecurring.storeId}` );
|
|
5114
|
+
highestRecurringStoreWow = computeWow( topRecurring.value, prev?.count || 0 );
|
|
5115
|
+
}
|
|
5116
|
+
|
|
5117
|
+
const excelHeader = `Weekly Summary ${startDateLabel} - ${endDateLabel}`;
|
|
5118
|
+
const buf = await buildWeeklyWrapExcel( excelHeader, excelRows );
|
|
5119
|
+
const attachmentBuffer = Buffer.from( buf );
|
|
5120
|
+
const sizeKb = Math.max( 1, Math.round( attachmentBuffer.length / 1024 ) );
|
|
5121
|
+
|
|
5122
|
+
const data = {
|
|
5123
|
+
startDate: startDateLabel,
|
|
5124
|
+
endDate: endDateLabel,
|
|
5125
|
+
storesFlagged,
|
|
5126
|
+
storesFlaggedWow: computeWow( storesFlagged, prevStoresFlagged ),
|
|
5127
|
+
totalFlags,
|
|
5128
|
+
totalFlagsWow: computeWow( totalFlags, prevTotalFlags ),
|
|
5129
|
+
checklistFlags: flaggedChecklistsThis.size,
|
|
5130
|
+
checklistFlagsWow: computeWow( flaggedChecklistsThis.size, flaggedChecklistsPrev.size ),
|
|
5131
|
+
questionFlags,
|
|
5132
|
+
notSubmittedFlags,
|
|
5133
|
+
runAIFlags,
|
|
5134
|
+
recurringFlags,
|
|
5135
|
+
highestFlaggedStore: topFlag?.storeName || topFlag?.storeId || '--',
|
|
5136
|
+
highestFlaggedStoreWow,
|
|
5137
|
+
highestRecurringStore: topRecurring?.storeName || topRecurring?.storeId || '--',
|
|
5138
|
+
highestRecurringStoreWow,
|
|
5139
|
+
attachmentName: `${excelHeader}.xlsx`,
|
|
5140
|
+
attachmentSize: `${sizeKb} KB`,
|
|
5141
|
+
domain: flagDomain,
|
|
5142
|
+
};
|
|
5143
|
+
|
|
5144
|
+
const html = compiled( { data } );
|
|
5145
|
+
const attachment = {
|
|
5146
|
+
filename: `${excelHeader}.xlsx`,
|
|
5147
|
+
content: attachmentBuffer,
|
|
5148
|
+
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
5149
|
+
};
|
|
5150
|
+
const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
|
|
5151
|
+
const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
|
|
5152
|
+
|
|
5153
|
+
sendEmailWithSES( user.email, subject, html, attachment, sourceEmail );
|
|
5154
|
+
sentSummary.push( { recipient: user.email, clientId, totalFlags } );
|
|
5155
|
+
} catch ( e ) {
|
|
5156
|
+
logger.error( { functionName: 'weeklyWrapAlert.user', clientId, email: user?.email, error: e } );
|
|
5157
|
+
}
|
|
5158
|
+
}
|
|
4919
5159
|
} catch ( e ) {
|
|
4920
5160
|
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
4921
5161
|
}
|
|
@@ -67,7 +67,9 @@
|
|
|
67
67
|
<div style="margin-top: 0px;margin-bottom: 0px;font-size: 16px;line-height: 28px;color: #82899a;">
|
|
68
68
|
<span style="font-weight: 400;color: #121A26;line-height: 140%;">
|
|
69
69
|
Hi,<br/>
|
|
70
|
-
{{#if data.
|
|
70
|
+
{{#if data.isMultiStoreSingleChecklist}}
|
|
71
|
+
Recurring flags has been detected across multiple {{data.subjectLabelPluralLower}} in recent <b>{{data.checklistName}}</b> on recent submissions, exceeding the configured threshold of {{data.threshold}} occurrences.
|
|
72
|
+
{{else if data.isMultiStore}}
|
|
71
73
|
Recurring flags has been detected across multiple {{data.subjectLabelPluralLower}} and across multiple checklists on recent submissions, exceeding the configured threshold of {{data.threshold}} occurrences.
|
|
72
74
|
{{else if data.isMultiChecklist}}
|
|
73
75
|
A Recurring flags has been identified for {{data.subjectLabelLower}} <b>{{data.subjectName}}</b> across multiple checklists on recent submissions, exceeding the configured threshold of {{data.threshold}} occurrences.
|
|
@@ -87,7 +89,9 @@
|
|
|
87
89
|
<b>Key Highlights:</b>
|
|
88
90
|
<ul style="margin:8px 0 0 0;padding-left:18px;">
|
|
89
91
|
<li>Total {{data.subjectLabelPlural}} with Recurring Flags: {{data.highlights.totalSubjects}}</li>
|
|
92
|
+
{{#unless data.isMultiStoreSingleChecklist}}
|
|
90
93
|
<li>Total Checklists with Recurring Flags: {{data.highlights.totalChecklists}}</li>
|
|
94
|
+
{{/unless}}
|
|
91
95
|
<li>Total Recurring Flags: {{data.highlights.totalFlags}}</li>
|
|
92
96
|
</ul>
|
|
93
97
|
</div>
|
|
@@ -141,12 +145,7 @@
|
|
|
141
145
|
<tr bgcolor="#ffffff">
|
|
142
146
|
<td class="flagText" style="padding-left:30px;line-height: 24px;">No of Flags :</td>
|
|
143
147
|
<td></td><td></td>
|
|
144
|
-
<td class="flagText">{{data.
|
|
145
|
-
</tr>
|
|
146
|
-
<tr bgcolor="#ffffff">
|
|
147
|
-
<td class="flagText" style="padding-left:30px;line-height: 24px;">Run AI Flags :</td>
|
|
148
|
-
<td></td><td></td>
|
|
149
|
-
<td class="flagText">{{data.runAICount}} Run AI Flag{{#if data.runAICountPlural}}s{{/if}}</td>
|
|
148
|
+
<td class="flagText">{{data.totalFlags}} (Question Flags: {{data.flagCount}}, Run AI Flags: {{data.runAICount}})</td>
|
|
150
149
|
</tr>
|
|
151
150
|
</table>
|
|
152
151
|
</td>
|
|
@@ -164,22 +163,20 @@
|
|
|
164
163
|
<thead>
|
|
165
164
|
<tr>
|
|
166
165
|
{{#if data.isMultiStore}}<th>{{data.subjectLabel}} Name</th>{{/if}}
|
|
167
|
-
<th>Checklist Name</th>
|
|
166
|
+
{{#unless data.isMultiStoreSingleChecklist}}<th>Checklist Name</th>{{/unless}}
|
|
168
167
|
<th>Last Submitted By</th>
|
|
169
168
|
<th>Last Submission Date</th>
|
|
170
169
|
<th>Total Recurring Flags</th>
|
|
171
|
-
<th>Run AI Flags</th>
|
|
172
170
|
</tr>
|
|
173
171
|
</thead>
|
|
174
172
|
<tbody>
|
|
175
173
|
{{#each data.rows}}
|
|
176
174
|
<tr>
|
|
177
175
|
{{#if ../data.isMultiStore}}<td>{{this.subjectName}}</td>{{/if}}
|
|
178
|
-
<td>{{this.checklistName}}</td>
|
|
176
|
+
{{#unless ../data.isMultiStoreSingleChecklist}}<td>{{this.checklistName}}</td>{{/unless}}
|
|
179
177
|
<td>{{this.lastSubmittedBy}}</td>
|
|
180
178
|
<td>{{this.lastSubmissionDate}}</td>
|
|
181
|
-
<td>{{this.
|
|
182
|
-
<td>{{this.runAICount}}</td>
|
|
179
|
+
<td>{{this.totalFlags}} (Question Flags: {{this.flagCount}}, Run AI Flags: {{this.runAICount}})</td>
|
|
183
180
|
</tr>
|
|
184
181
|
{{/each}}
|
|
185
182
|
</tbody>
|
|
@@ -70,6 +70,8 @@
|
|
|
70
70
|
.q-answer-text.flagged{color:#a32d2d}
|
|
71
71
|
.q-answer-media{margin-top:8px}
|
|
72
72
|
.q-answer-media img,.q-answer-media video,.q-answer-item td img{display:block;width:200px;height:180px;object-fit:cover;border-radius:6px;margin-bottom:6px}
|
|
73
|
+
.img-grid{display:flex;flex-wrap:wrap;gap:8px}
|
|
74
|
+
.img-grid img{margin-bottom:0}
|
|
73
75
|
.q-answer-link{font-size:12px;color:#0085D2;text-decoration:underline;word-break:break-all}
|
|
74
76
|
.q-answer-caption{font-size:11px;color:#666;margin-bottom:4px}
|
|
75
77
|
.q-answer-remarks{font-size:11px;color:#666;margin-top:6px;white-space:pre-line}
|
|
@@ -188,9 +190,11 @@
|
|
|
188
190
|
{{#if this.multiQuestionReferenceImage.length}}
|
|
189
191
|
<div class="q-answer-media">
|
|
190
192
|
<div class="q-answer-caption">Question Reference Images</div>
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
193
|
+
<div class="img-grid">
|
|
194
|
+
{{#each this.multiQuestionReferenceImage}}
|
|
195
|
+
<img src="{{this}}" alt="Reference Image" />
|
|
196
|
+
{{/each}}
|
|
197
|
+
</div>
|
|
194
198
|
</div>
|
|
195
199
|
{{/if}}
|
|
196
200
|
{{!-- <span class="q-ans {{#if this.isYes}}ans-yes{{else}}{{#if this.isNo}}ans-no{{/if}}{{/if}}">{{#if this.isYes}}✓ Yes{{else}}{{#if this.isNo}}✗ No{{else}}{{this.answerDisplay}}{{/if}}{{/if}}</span> --}}
|
|
@@ -198,25 +202,6 @@
|
|
|
198
202
|
<div class="q-answer-list">
|
|
199
203
|
{{#each this.userAnswer}}
|
|
200
204
|
<div class="q-answer-item">
|
|
201
|
-
{{#neq ../answerType 'image/video'}}
|
|
202
|
-
{{#neq ../answerType 'multipleImage'}}
|
|
203
|
-
{{#if this.multiReferenceImage.length}}
|
|
204
|
-
<div class="q-answer-media">
|
|
205
|
-
<div class="q-answer-caption">Reference Images</div>
|
|
206
|
-
{{#each this.multiReferenceImage}}
|
|
207
|
-
<img src="{{this}}" alt="Reference Image" />
|
|
208
|
-
{{/each}}
|
|
209
|
-
</div>
|
|
210
|
-
{{else}}
|
|
211
|
-
{{#if this.referenceImage}}
|
|
212
|
-
<div class="q-answer-media">
|
|
213
|
-
<div class="q-answer-caption">Reference Image</div>
|
|
214
|
-
<img src="{{this.referenceImage}}" alt="Reference Image" />
|
|
215
|
-
</div>
|
|
216
|
-
{{/if}}
|
|
217
|
-
{{/if}}
|
|
218
|
-
{{/neq}}
|
|
219
|
-
{{/neq}}
|
|
220
205
|
{{#eq this.answerType 'text'}}
|
|
221
206
|
{{#if this.answer}}
|
|
222
207
|
<div class="q-answer-text {{#if this.sopFlag}}flagged{{/if}}">{{this.answer}}</div>
|
|
@@ -231,8 +216,29 @@
|
|
|
231
216
|
{{/if}}
|
|
232
217
|
{{/eq}}
|
|
233
218
|
|
|
234
|
-
<table style="width:100%;margin-top:8px"><tr>
|
|
235
|
-
<td style="vertical-align:top">
|
|
219
|
+
<table style="width:100%;margin-top:8px;table-layout:fixed"><tr>
|
|
220
|
+
<td style="width:50%;vertical-align:top;padding-right:8px">
|
|
221
|
+
{{#neq ../answerType 'image/video'}}
|
|
222
|
+
{{#neq ../answerType 'multipleImage'}}
|
|
223
|
+
{{#if this.multiReferenceImage.length}}
|
|
224
|
+
<div class="q-answer-media">
|
|
225
|
+
<div class="q-answer-caption">Reference Images</div>
|
|
226
|
+
{{#each this.multiReferenceImage}}
|
|
227
|
+
<img src="{{this}}" alt="Reference Image" />
|
|
228
|
+
{{/each}}
|
|
229
|
+
</div>
|
|
230
|
+
{{else}}
|
|
231
|
+
{{#if this.referenceImage}}
|
|
232
|
+
<div class="q-answer-media">
|
|
233
|
+
<div class="q-answer-caption">Reference Image</div>
|
|
234
|
+
<img src="{{this.referenceImage}}" alt="Reference Image" />
|
|
235
|
+
</div>
|
|
236
|
+
{{/if}}
|
|
237
|
+
{{/if}}
|
|
238
|
+
{{/neq}}
|
|
239
|
+
{{/neq}}
|
|
240
|
+
</td>
|
|
241
|
+
<td style="width:50%;vertical-align:top;padding-left:8px">
|
|
236
242
|
{{#eq this.answerType 'image'}}
|
|
237
243
|
{{#if this.answer}}
|
|
238
244
|
<div class="q-answer-caption">Uploaded Image</div>
|
|
@@ -169,13 +169,15 @@ function buildQuestionAnswerEntries( question ) {
|
|
|
169
169
|
|
|
170
170
|
const rawMultiRefSources = [
|
|
171
171
|
userAnswer?.multiReferenceImage,
|
|
172
|
-
matchedAnswer?.multiReferenceImage,
|
|
173
|
-
question?.answers?.[0]?.multiReferenceImage,
|
|
172
|
+
// matchedAnswer?.multiReferenceImage,
|
|
173
|
+
// question?.answers?.[0]?.multiReferenceImage,
|
|
174
174
|
];
|
|
175
175
|
let multiReferenceImage = [];
|
|
176
176
|
for ( const src of rawMultiRefSources ) {
|
|
177
177
|
const flat = flattenImageRefs( src );
|
|
178
|
-
if ( flat.length ) {
|
|
178
|
+
if ( flat.length ) {
|
|
179
|
+
multiReferenceImage = flat; break;
|
|
180
|
+
}
|
|
179
181
|
}
|
|
180
182
|
const validationImage = flattenImageRefs( userAnswer?.validationImage );
|
|
181
183
|
|
|
@@ -920,7 +922,6 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
|
|
|
920
922
|
q.multiQuestionReferenceImage = q.multiQuestionReferenceImage.map( ( ele ) => resolveUrl( ele ) );
|
|
921
923
|
}
|
|
922
924
|
q.userAnswer?.forEach( ( ua ) => {
|
|
923
|
-
|
|
924
925
|
if ( ua.multiReferenceImage?.length ) {
|
|
925
926
|
ua.multiReferenceImage = ua.multiReferenceImage.map( ( ele ) => resolveUrl( ele ) );
|
|
926
927
|
}
|