tango-app-api-trax 3.8.26 → 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
|
@@ -4389,6 +4389,20 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4389
4389
|
|
|
4390
4390
|
if ( !recipients.length ) return;
|
|
4391
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
|
+
|
|
4392
4406
|
// Read tracker rows where EITHER the sop streak or the runAI count has hit threshold and the
|
|
4393
4407
|
// current trigger hasn't been emailed yet (lastEmailDate vs the relevant flagged date).
|
|
4394
4408
|
const trackerRows = await recurringFlagTracker.find( {
|
|
@@ -4413,7 +4427,12 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4413
4427
|
// Determine which streak crossed threshold for this row — drives reset granularity below.
|
|
4414
4428
|
const sopFired = ( t.consecutiveCount || 0 ) >= threshold && t.lastEmailDate !== t.lastFlaggedDate;
|
|
4415
4429
|
const runAIFired = ( t.runAICount || 0 ) >= threshold && t.lastEmailDate !== t.lastRunAIFlaggedDate;
|
|
4416
|
-
|
|
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;
|
|
4417
4436
|
triggers.push( {
|
|
4418
4437
|
recipient,
|
|
4419
4438
|
clientId: t.client_id,
|
|
@@ -4437,14 +4456,19 @@ export async function recurringFlagAlert( req, res ) {
|
|
|
4437
4456
|
lastSubmittedBy: t.lastSubmittedBy || '--',
|
|
4438
4457
|
lastSubmissionDate: t.lastSubmissionDate || t.lastFlaggedDate || '',
|
|
4439
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
|
+
} );
|
|
4440
4471
|
}
|
|
4441
|
-
trackerIdsToReset.push( {
|
|
4442
|
-
_id: t._id,
|
|
4443
|
-
lastFlaggedDate: t.lastFlaggedDate,
|
|
4444
|
-
lastRunAIFlaggedDate: t.lastRunAIFlaggedDate,
|
|
4445
|
-
sopFired,
|
|
4446
|
-
runAIFired,
|
|
4447
|
-
} );
|
|
4448
4472
|
}
|
|
4449
4473
|
} ) );
|
|
4450
4474
|
|
|
@@ -4648,10 +4672,12 @@ function getLastWeekRange( ref = dayjs() ) {
|
|
|
4648
4672
|
const dow = today.day(); // 0 Sun ... 6 Sat
|
|
4649
4673
|
const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
|
|
4650
4674
|
const thisMonday = today.subtract( daysToThisMonday, 'day' );
|
|
4651
|
-
|
|
4652
|
-
const
|
|
4653
|
-
const
|
|
4654
|
-
|
|
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' );
|
|
4655
4681
|
return {
|
|
4656
4682
|
weekStart: weekStart.format( 'YYYY-MM-DD' ),
|
|
4657
4683
|
weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
|
|
@@ -4713,26 +4739,33 @@ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
|
|
|
4713
4739
|
}
|
|
4714
4740
|
|
|
4715
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.
|
|
4716
4745
|
const rows = await recurringFlagTracker.aggregate( [
|
|
4717
4746
|
{ $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
|
|
4718
4747
|
{ $project: {
|
|
4719
4748
|
client_id: 1,
|
|
4720
4749
|
store_id: 1,
|
|
4721
4750
|
storeName: 1,
|
|
4722
|
-
|
|
4723
|
-
$
|
|
4724
|
-
$
|
|
4725
|
-
|
|
4726
|
-
|
|
4727
|
-
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4728
|
-
},
|
|
4751
|
+
datesInWeek: {
|
|
4752
|
+
$filter: {
|
|
4753
|
+
input: { $ifNull: [ '$emailHistory', [] ] },
|
|
4754
|
+
as: 'd',
|
|
4755
|
+
cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
|
|
4729
4756
|
},
|
|
4730
4757
|
},
|
|
4731
4758
|
} },
|
|
4759
|
+
{ $unwind: '$datesInWeek' },
|
|
4732
4760
|
{ $group: {
|
|
4733
4761
|
_id: { client_id: '$client_id', store_id: '$store_id' },
|
|
4734
4762
|
storeName: { $last: '$storeName' },
|
|
4735
|
-
|
|
4763
|
+
uniqueDates: { $addToSet: '$datesInWeek' },
|
|
4764
|
+
} },
|
|
4765
|
+
{ $project: {
|
|
4766
|
+
_id: 1,
|
|
4767
|
+
storeName: 1,
|
|
4768
|
+
count: { $size: '$uniqueDates' },
|
|
4736
4769
|
} },
|
|
4737
4770
|
] );
|
|
4738
4771
|
const map = new Map();
|
|
@@ -4747,180 +4780,382 @@ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
|
|
|
4747
4780
|
return map;
|
|
4748
4781
|
}
|
|
4749
4782
|
|
|
4750
|
-
function buildWeeklyWrapExcel( header,
|
|
4783
|
+
function buildWeeklyWrapExcel( header, perStoreChecklistRows ) {
|
|
4751
4784
|
const workbook = new ExcelJS.Workbook();
|
|
4752
4785
|
const sheet = workbook.addWorksheet( 'Weekly Summary' );
|
|
4753
4786
|
sheet.addRow( [ header ] );
|
|
4754
4787
|
sheet.getRow( 1 ).font = { bold: true, size: 14 };
|
|
4755
4788
|
sheet.addRow( [] );
|
|
4756
4789
|
sheet.columns = [
|
|
4757
|
-
{ header: 'Store ID', key: 'storeId', width: 18 },
|
|
4758
4790
|
{ header: 'Store Name', key: 'storeName', width: 28 },
|
|
4791
|
+
{ header: 'Checklist Name', key: 'checkListName', width: 32 },
|
|
4792
|
+
{ header: 'Flags', key: 'flags', width: 12 },
|
|
4759
4793
|
{ header: 'Question Flags', key: 'questionFlag', width: 16 },
|
|
4760
|
-
{ header: 'Not Submitted', key: 'timeFlag', width:
|
|
4761
|
-
{ 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 },
|
|
4762
4796
|
{ 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
4797
|
];
|
|
4767
4798
|
sheet.getRow( 3 ).font = { bold: true };
|
|
4768
4799
|
sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
|
|
4769
|
-
for ( const r of
|
|
4800
|
+
for ( const r of perStoreChecklistRows ) sheet.addRow( r );
|
|
4770
4801
|
return workbook.xlsx.writeBuffer();
|
|
4771
4802
|
}
|
|
4772
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
|
+
|
|
4773
4940
|
export async function weeklyWrapAlert( req, res ) {
|
|
4774
4941
|
try {
|
|
4775
4942
|
const range = getLastWeekRange();
|
|
4776
4943
|
const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
|
|
4777
4944
|
const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
|
|
4778
4945
|
|
|
4779
|
-
//
|
|
4780
|
-
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 ) ) );
|
|
4781
4953
|
|
|
4782
|
-
// Aggregate flag data for both weeks across all stores in one shot.
|
|
4783
|
-
|
|
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( [
|
|
4784
4957
|
aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
|
|
4785
4958
|
aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4786
4959
|
aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
|
|
4787
4960
|
aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
|
|
4961
|
+
aggregateWeeklyFlagsByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4962
|
+
aggregateWeeklyRecurringByStoreChecklist( range.weekStart, range.weekEnd ),
|
|
4788
4963
|
] );
|
|
4789
4964
|
|
|
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
4965
|
const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
|
|
4805
4966
|
const compiled = handlebars.compile( fileContent );
|
|
4806
4967
|
const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
|
|
4807
4968
|
const sentSummary = [];
|
|
4808
4969
|
|
|
4809
|
-
// 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.
|
|
4810
4971
|
const clientIds = new Set();
|
|
4811
|
-
for ( const v of flagsThis.values() )
|
|
4812
|
-
|
|
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
|
+
}
|
|
4813
4978
|
|
|
4814
4979
|
await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
|
|
4815
4980
|
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
|
-
}
|
|
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;
|
|
4870
4992
|
}
|
|
4871
4993
|
|
|
4872
|
-
|
|
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
|
+
}
|
|
4873
5003
|
|
|
4874
|
-
|
|
4875
|
-
|
|
4876
|
-
|
|
4877
|
-
|
|
4878
|
-
|
|
4879
|
-
|
|
4880
|
-
|
|
4881
|
-
|
|
4882
|
-
|
|
4883
|
-
|
|
4884
|
-
|
|
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
|
+
}
|
|
4885
5017
|
|
|
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
|
-
|
|
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
|
+
}
|
|
4912
5061
|
|
|
4913
|
-
|
|
4914
|
-
|
|
4915
|
-
|
|
4916
|
-
content: attachmentBuffer,
|
|
4917
|
-
contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
|
|
4918
|
-
};
|
|
4919
|
-
const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
|
|
4920
|
-
const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
|
|
5062
|
+
if ( totalFlags === 0 && prevTotalFlags === 0 ) {
|
|
5063
|
+
continue; // user has nothing to report this week
|
|
5064
|
+
}
|
|
4921
5065
|
|
|
4922
|
-
|
|
4923
|
-
|
|
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
|
+
}
|
|
4924
5159
|
} catch ( e ) {
|
|
4925
5160
|
logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
|
|
4926
5161
|
}
|