tango-app-api-trax 3.8.26 → 3.8.28

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
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-trax",
3
- "version": "3.8.26",
3
+ "version": "3.8.28",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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
- for ( const recipient of recipients ) {
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
- const weekStart = thisMonday.subtract( 7, 'day' );
4652
- const weekEnd = thisMonday.subtract( 1, 'day' );
4653
- const prevWeekStart = weekStart.subtract( 7, 'day' );
4654
- const prevWeekEnd = weekStart.subtract( 1, 'day' );
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
- firedInWeek: {
4723
- $size: {
4724
- $filter: {
4725
- input: { $ifNull: [ '$emailHistory', [] ] },
4726
- as: 'd',
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
- count: { $sum: '$firedInWeek' },
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, perStoreRows ) {
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: 16 },
4761
- { header: 'Run AI Flags', key: 'runAIFlag', width: 16 },
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 perStoreRows ) sheet.addRow( r );
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
- // TEMP: static recipient for dev. Replace with client-config lookup later.
4780
- const STATIC_RECIPIENT = 'gopisjkg@gmail.com';
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
- const [ flagsThis, flagsPrev, recurThis, recurPrev ] = await Promise.all( [
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 and email the static recipient.
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() ) clientIds.add( v.client_id );
4812
- for ( const v of recurThis.values() ) clientIds.add( v.client_id );
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
- let storesFlagged = 0;
4817
- let totalFlags = 0;
4818
- let prevTotalFlags = 0;
4819
- let prevStoresFlagged = 0;
4820
- const flaggedChecklistsThis = new Set();
4821
- const flaggedChecklistsPrev = new Set();
4822
- let questionFlags = 0;
4823
- let notSubmittedFlags = 0;
4824
- let runAIFlags = 0;
4825
- let recurringFlags = 0;
4826
- const excelRows = [];
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
- if ( totalFlags === 0 && prevTotalFlags === 0 ) return; // skip silent clients
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
- const top = topByClient.get( clientId ) || { topFlag: null, topRecurring: null };
4875
- let highestFlaggedStoreWow = { value: '', direction: 'up' };
4876
- if ( top.topFlag ) {
4877
- const prev = flagsPrev.get( `${clientId}::${top.topFlag.storeId}` );
4878
- highestFlaggedStoreWow = computeWow( top.topFlag.value, prev?.totalFlags || 0 );
4879
- }
4880
- let highestRecurringStoreWow = { value: '', direction: 'up' };
4881
- if ( top.topRecurring ) {
4882
- const prev = recurPrev.get( `${clientId}::${top.topRecurring.storeId}` );
4883
- highestRecurringStoreWow = computeWow( top.topRecurring.value, prev?.count || 0 );
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
- const excelHeader = `Weekly Summary ${startDateLabel} - ${endDateLabel}`;
4887
- const buf = await buildWeeklyWrapExcel( excelHeader, excelRows );
4888
- const attachmentBuffer = Buffer.from( buf );
4889
- const sizeKb = Math.max( 1, Math.round( attachmentBuffer.length / 1024 ) );
4890
-
4891
- const data = {
4892
- startDate: startDateLabel,
4893
- endDate: endDateLabel,
4894
- storesFlagged,
4895
- storesFlaggedWow: computeWow( storesFlagged, prevStoresFlagged ),
4896
- totalFlags,
4897
- totalFlagsWow: computeWow( totalFlags, prevTotalFlags ),
4898
- checklistFlags: flaggedChecklistsThis.size,
4899
- checklistFlagsWow: computeWow( flaggedChecklistsThis.size, flaggedChecklistsPrev.size ),
4900
- questionFlags,
4901
- notSubmittedFlags,
4902
- runAIFlags,
4903
- recurringFlags,
4904
- highestFlaggedStore: top.topFlag?.storeName || top.topFlag?.storeId || '--',
4905
- highestFlaggedStoreWow,
4906
- highestRecurringStore: top.topRecurring?.storeName || top.topRecurring?.storeId || '--',
4907
- highestRecurringStoreWow,
4908
- attachmentName: `${excelHeader}.xlsx`,
4909
- attachmentSize: `${sizeKb} KB`,
4910
- domain: flagDomain,
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
- const html = compiled( { data } );
4914
- const attachment = {
4915
- filename: `${excelHeader}.xlsx`,
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
- sendEmailWithSES( STATIC_RECIPIENT, subject, html, attachment, sourceEmail );
4923
- sentSummary.push( { recipient: STATIC_RECIPIENT, clientId, totalFlags } );
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
  }
@@ -467,7 +467,7 @@ export async function startTask( req, res ) {
467
467
  } );
468
468
  const getUpdatedTask = await processedTask.aggregate( findQuery );
469
469
 
470
-
470
+ let cdnurl = JSON.parse( process.env.CDNURL );
471
471
  if ( !( task.checkListFrom && task.checkListFrom == 'api' ) ) {
472
472
  // const bucket = JSON.parse( process.env.BUCKET );
473
473
  await Promise.all(