tango-app-api-trax 3.8.21 → 3.8.22

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.21",
3
+ "version": "3.8.22",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -29,7 +29,7 @@
29
29
  "nodemon": "^3.1.4",
30
30
  "path": "^0.12.7",
31
31
  "puppeteer": "^24.39.1",
32
- "tango-api-schema": "^2.5.85",
32
+ "tango-api-schema": "^2.5.87",
33
33
  "tango-app-api-middleware": "^3.5.2",
34
34
  "url": "^0.11.4",
35
35
  "winston": "^3.13.1",
@@ -2119,7 +2119,6 @@ export async function getPDFCSVChecklistDetails( req, res ) {
2119
2119
  }
2120
2120
  }
2121
2121
 
2122
-
2123
2122
  export async function AiPushNotificationAlert( req, res ) {
2124
2123
  try {
2125
2124
  // console.log( req.body );
@@ -2176,7 +2175,6 @@ export async function liveAiPushNotificationAlert( req, res ) {
2176
2175
  }
2177
2176
  }
2178
2177
 
2179
-
2180
2178
  export async function taskPushNotification( req, res ) {
2181
2179
  try {
2182
2180
  let query = [ {
@@ -4249,12 +4247,12 @@ export async function recurringFlagAlert( req, res ) {
4249
4247
  const checklistDetails = await CLconfig.find( {
4250
4248
  publish: true,
4251
4249
  $expr: {
4252
- $gt: [
4253
- { $size: { $cond: [ { $isArray: '$recurringFlag.users' }, '$recurringFlag.users', [] ] } },
4254
- 0,
4250
+ $or: [
4251
+ { $gt: [ { $size: { $cond: [ { $isArray: '$recurringFlag.users' }, '$recurringFlag.users', [] ] } }, 0 ] },
4252
+ { $gt: [ { $size: { $cond: [ { $isArray: '$recurringFlag.notifyType' }, '$recurringFlag.notifyType', [] ] } }, 0 ] },
4255
4253
  ],
4256
4254
  },
4257
- }, { _id: 1, checkListName: 1, recurringFlag: 1, approver: 1, client_id: 1 } );
4255
+ }, { _id: 1, checkListName: 1, recurringFlag: 1, notifyFlags: 1, approver: 1, client_id: 1 } );
4258
4256
 
4259
4257
  if ( !checklistDetails.length ) {
4260
4258
  return res.sendSuccess( 'No checklists configured for recurring flag' );
@@ -4270,10 +4268,19 @@ export async function recurringFlagAlert( req, res ) {
4270
4268
  const users = cl?.recurringFlag?.users || [];
4271
4269
 
4272
4270
  let recipients = [];
4273
- if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
4274
- recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
4271
+ if ( notifyType.includes( 'sameAsNotify' ) ) {
4272
+ const nfType = cl?.notifyFlags?.notifyType || [];
4273
+ const nfUsers = cl?.notifyFlags?.users || [];
4274
+ if ( nfType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
4275
+ recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
4276
+ }
4277
+ recipients = [ ...recipients, ...nfUsers.map( ( u ) => u?.value ).filter( Boolean ) ];
4278
+ } else {
4279
+ if ( notifyType.includes( 'approver' ) && Array.isArray( cl.approver ) ) {
4280
+ recipients = cl.approver.map( ( a ) => a?.value ).filter( Boolean );
4281
+ }
4282
+ recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
4275
4283
  }
4276
- recipients = [ ...recipients, ...users.map( ( u ) => u?.value ).filter( Boolean ) ];
4277
4284
  recipients = [ ...new Set( recipients ) ];
4278
4285
 
4279
4286
  if ( !recipients.length ) return;
@@ -4404,12 +4411,15 @@ export async function recurringFlagAlert( req, res ) {
4404
4411
  sentSummary.push( { recipient, count: items.length, mode: isMultiStore ? 'multi-store' : ( isMultiChecklist ? 'multi-checklist' : 'single' ) } );
4405
4412
  } ) );
4406
4413
 
4407
- // Reset counters and stamp lastEmailDate for all triggered tracker rows so the next streak starts fresh.
4414
+ // Reset counters, stamp lastEmailDate, and append the fire date to emailHistory (capped at last 60).
4408
4415
  if ( trackerIdsToReset.length ) {
4409
4416
  const resetOps = trackerIdsToReset.map( ( { _id, lastFlaggedDate } ) => ( {
4410
4417
  updateOne: {
4411
4418
  filter: { _id },
4412
- update: { $set: { consecutiveCount: 0, lastEmailDate: lastFlaggedDate } },
4419
+ update: {
4420
+ $set: { consecutiveCount: 0, lastEmailDate: lastFlaggedDate },
4421
+ $push: { emailHistory: { $each: [ lastFlaggedDate ], $slice: -60 } },
4422
+ },
4413
4423
  },
4414
4424
  } ) );
4415
4425
  await recurringFlagTracker.bulkWrite( resetOps );
@@ -4422,41 +4432,299 @@ export async function recurringFlagAlert( req, res ) {
4422
4432
  }
4423
4433
  }
4424
4434
 
4435
+ function getLastWeekRange( ref = dayjs() ) {
4436
+ const today = ref.startOf( 'day' );
4437
+ const dow = today.day(); // 0 Sun ... 6 Sat
4438
+ const daysToThisMonday = ( dow + 6 ) % 7; // Mon=0, Tue=1, ..., Sun=6
4439
+ const thisMonday = today.subtract( daysToThisMonday, 'day' );
4440
+ const weekStart = thisMonday.subtract( 7, 'day' );
4441
+ const weekEnd = thisMonday.subtract( 1, 'day' );
4442
+ const prevWeekStart = weekStart.subtract( 7, 'day' );
4443
+ const prevWeekEnd = weekStart.subtract( 1, 'day' );
4444
+ return {
4445
+ weekStart: weekStart.format( 'YYYY-MM-DD' ),
4446
+ weekEnd: weekEnd.format( 'YYYY-MM-DD' ),
4447
+ prevWeekStart: prevWeekStart.format( 'YYYY-MM-DD' ),
4448
+ prevWeekEnd: prevWeekEnd.format( 'YYYY-MM-DD' ),
4449
+ };
4450
+ }
4451
+
4452
+ function computeWow( current, previous ) {
4453
+ if ( !previous ) {
4454
+ if ( current ) return { value: '100%', direction: 'up' };
4455
+ return { value: '', direction: 'up' };
4456
+ }
4457
+ const delta = current - previous;
4458
+ if ( delta === 0 ) return { value: '0%', direction: 'up' };
4459
+ const pct = Math.min( 100, Math.round( ( Math.abs( delta ) / previous ) * 100 ) );
4460
+ return { value: `${pct}%`, direction: delta > 0 ? 'up' : 'down' };
4461
+ }
4462
+
4463
+ async function aggregateWeeklyFlagsByStore( weekStart, weekEnd ) {
4464
+ const rows = await processedchecklist.aggregate( [
4465
+ { $match: { date_string: { $gte: weekStart, $lte: weekEnd }, isdeleted: { $ne: true } } },
4466
+ { $group: {
4467
+ _id: { client_id: '$client_id', store_id: '$store_id' },
4468
+ storeName: { $last: '$storeName' },
4469
+ questionFlag: { $sum: { $ifNull: [ '$questionFlag', 0 ] } },
4470
+ timeFlag: { $sum: { $ifNull: [ '$timeFlag', 0 ] } },
4471
+ runAIFlag: { $sum: { $ifNull: [ '$runAIFlag', 0 ] } },
4472
+ flaggedChecklistIds: { $addToSet: {
4473
+ $cond: [
4474
+ { $or: [
4475
+ { $gt: [ { $ifNull: [ '$questionFlag', 0 ] }, 0 ] },
4476
+ { $gt: [ { $ifNull: [ '$timeFlag', 0 ] }, 0 ] },
4477
+ { $gt: [ { $ifNull: [ '$runAIFlag', 0 ] }, 0 ] },
4478
+ ] },
4479
+ '$sourceCheckList_id',
4480
+ null,
4481
+ ],
4482
+ } },
4483
+ } },
4484
+ ] );
4485
+ const map = new Map();
4486
+ for ( const r of rows ) {
4487
+ const key = `${r._id.client_id}::${r._id.store_id}`;
4488
+ const flaggedChecklists = ( r.flaggedChecklistIds || [] ).filter( ( id ) => id ).map( ( id ) => String( id ) );
4489
+ const totalFlags = ( r.questionFlag || 0 ) + ( r.timeFlag || 0 ) + ( r.runAIFlag || 0 );
4490
+ map.set( key, {
4491
+ client_id: r._id.client_id,
4492
+ store_id: r._id.store_id,
4493
+ storeName: r.storeName,
4494
+ questionFlag: r.questionFlag || 0,
4495
+ timeFlag: r.timeFlag || 0,
4496
+ runAIFlag: r.runAIFlag || 0,
4497
+ flaggedChecklists,
4498
+ totalFlags,
4499
+ } );
4500
+ }
4501
+ return map;
4502
+ }
4503
+
4504
+ async function aggregateWeeklyRecurringByStore( weekStart, weekEnd ) {
4505
+ const rows = await recurringFlagTracker.aggregate( [
4506
+ { $match: { emailHistory: { $elemMatch: { $gte: weekStart, $lte: weekEnd } } } },
4507
+ { $project: {
4508
+ client_id: 1,
4509
+ store_id: 1,
4510
+ storeName: 1,
4511
+ firedInWeek: {
4512
+ $size: {
4513
+ $filter: {
4514
+ input: { $ifNull: [ '$emailHistory', [] ] },
4515
+ as: 'd',
4516
+ cond: { $and: [ { $gte: [ '$$d', weekStart ] }, { $lte: [ '$$d', weekEnd ] } ] },
4517
+ },
4518
+ },
4519
+ },
4520
+ } },
4521
+ { $group: {
4522
+ _id: { client_id: '$client_id', store_id: '$store_id' },
4523
+ storeName: { $last: '$storeName' },
4524
+ count: { $sum: '$firedInWeek' },
4525
+ } },
4526
+ ] );
4527
+ const map = new Map();
4528
+ for ( const r of rows ) {
4529
+ map.set( `${r._id.client_id}::${r._id.store_id}`, {
4530
+ client_id: r._id.client_id,
4531
+ store_id: r._id.store_id,
4532
+ storeName: r.storeName,
4533
+ count: r.count,
4534
+ } );
4535
+ }
4536
+ return map;
4537
+ }
4538
+
4539
+ function buildWeeklyWrapExcel( header, perStoreRows ) {
4540
+ const workbook = new ExcelJS.Workbook();
4541
+ const sheet = workbook.addWorksheet( 'Weekly Summary' );
4542
+ sheet.addRow( [ header ] );
4543
+ sheet.getRow( 1 ).font = { bold: true, size: 14 };
4544
+ sheet.addRow( [] );
4545
+ sheet.columns = [
4546
+ { header: 'Store ID', key: 'storeId', width: 18 },
4547
+ { header: 'Store Name', key: 'storeName', width: 28 },
4548
+ { header: 'Question Flags', key: 'questionFlag', width: 16 },
4549
+ { header: 'Not Submitted', key: 'timeFlag', width: 16 },
4550
+ { header: 'Run AI Flags', key: 'runAIFlag', width: 16 },
4551
+ { header: 'Recurring Flags', key: 'recurringFlag', width: 18 },
4552
+ { header: 'Total Flags', key: 'totalFlags', width: 14 },
4553
+ { header: 'Last Week Total', key: 'prevTotal', width: 18 },
4554
+ { header: 'WoW %', key: 'wow', width: 12 },
4555
+ ];
4556
+ sheet.getRow( 3 ).font = { bold: true };
4557
+ sheet.getRow( 3 ).fill = { type: 'pattern', pattern: 'solid', fgColor: { argb: 'FFF1F5F9' } };
4558
+ for ( const r of perStoreRows ) sheet.addRow( r );
4559
+ return workbook.xlsx.writeBuffer();
4560
+ }
4561
+
4562
+ export async function weeklyWrapAlert( req, res ) {
4563
+ try {
4564
+ const range = getLastWeekRange();
4565
+ const startDateLabel = dayjs( range.weekStart ).format( 'DD/MM/YY' );
4566
+ const endDateLabel = dayjs( range.weekEnd ).format( 'DD/MM/YY' );
4567
+
4568
+ // TEMP: static recipient for dev. Replace with client-config lookup later.
4569
+ const STATIC_RECIPIENT = 'sh9628hs@gmail.com';
4570
+
4571
+ // Aggregate flag data for both weeks across all stores in one shot.
4572
+ const [ flagsThis, flagsPrev, recurThis, recurPrev ] = await Promise.all( [
4573
+ aggregateWeeklyFlagsByStore( range.weekStart, range.weekEnd ),
4574
+ aggregateWeeklyFlagsByStore( range.prevWeekStart, range.prevWeekEnd ),
4575
+ aggregateWeeklyRecurringByStore( range.weekStart, range.weekEnd ),
4576
+ aggregateWeeklyRecurringByStore( range.prevWeekStart, range.prevWeekEnd ),
4577
+ ] );
4578
+
4579
+ // Compute per-client highest flagged store and highest recurring flagged store (this week).
4580
+ const topByClient = new Map(); // clientId -> { topFlag, topRecurring }
4581
+ const upsertTop = ( client, key, candidate ) => {
4582
+ if ( !topByClient.has( client ) ) topByClient.set( client, { topFlag: null, topRecurring: null } );
4583
+ const entry = topByClient.get( client );
4584
+ if ( !entry[key] || candidate.value > entry[key].value ) entry[key] = candidate;
4585
+ };
4586
+ for ( const v of flagsThis.values() ) {
4587
+ upsertTop( v.client_id, 'topFlag', { storeId: v.store_id, storeName: v.storeName, value: v.totalFlags } );
4588
+ }
4589
+ for ( const v of recurThis.values() ) {
4590
+ upsertTop( v.client_id, 'topRecurring', { storeId: v.store_id, storeName: v.storeName, value: v.count } );
4591
+ }
4592
+
4593
+ const fileContent = fs.readFileSync( path.resolve( path.dirname( '' ) ) + '/src/hbs/weeklyWrap.hbs', 'utf8' );
4594
+ const compiled = handlebars.compile( fileContent );
4595
+ const flagDomain = `${JSON.parse( process.env.URL ).domain}/manage/trax/dashboard`;
4596
+ const sentSummary = [];
4597
+
4598
+ // Build one digest per client present in the data and email the static recipient.
4599
+ const clientIds = new Set();
4600
+ for ( const v of flagsThis.values() ) clientIds.add( v.client_id );
4601
+ for ( const v of recurThis.values() ) clientIds.add( v.client_id );
4602
+
4603
+ await Promise.all( [ ...clientIds ].map( async ( clientId ) => {
4604
+ try {
4605
+ let storesFlagged = 0;
4606
+ let totalFlags = 0;
4607
+ let prevTotalFlags = 0;
4608
+ let prevStoresFlagged = 0;
4609
+ const flaggedChecklistsThis = new Set();
4610
+ const flaggedChecklistsPrev = new Set();
4611
+ let questionFlags = 0;
4612
+ let notSubmittedFlags = 0;
4613
+ let runAIFlags = 0;
4614
+ let recurringFlags = 0;
4615
+ const excelRows = [];
4616
+
4617
+ // Iterate only stores belonging to this client (key prefix match).
4618
+ const clientPrefix = `${clientId}::`;
4619
+ const clientStoreKeys = new Set();
4620
+ for ( const k of flagsThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
4621
+ for ( const k of flagsPrev.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
4622
+ for ( const k of recurThis.keys() ) if ( k.startsWith( clientPrefix ) ) clientStoreKeys.add( k );
4623
+
4624
+ for ( const k of clientStoreKeys ) {
4625
+ const cur = flagsThis.get( k );
4626
+ const prev = flagsPrev.get( k );
4627
+ const recCur = recurThis.get( k );
4628
+ const recPrev = recurPrev.get( k );
4629
+ const storeRecurring = recCur?.count || 0;
4630
+ const storeTotal = ( cur?.totalFlags || 0 ) + storeRecurring;
4631
+ const prevStoreTotal = ( prev?.totalFlags || 0 ) + ( recPrev?.count || 0 );
4632
+
4633
+ if ( storeTotal > 0 ) storesFlagged += 1;
4634
+ if ( prevStoreTotal > 0 ) prevStoresFlagged += 1;
4635
+ totalFlags += storeTotal;
4636
+ prevTotalFlags += prevStoreTotal;
4637
+ questionFlags += cur?.questionFlag || 0;
4638
+ notSubmittedFlags += cur?.timeFlag || 0;
4639
+ runAIFlags += cur?.runAIFlag || 0;
4640
+ recurringFlags += storeRecurring;
4641
+
4642
+ ( cur?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsThis.add( id ) );
4643
+ ( prev?.flaggedChecklists || [] ).forEach( ( id ) => flaggedChecklistsPrev.add( id ) );
4644
+
4645
+ if ( storeTotal > 0 || prevStoreTotal > 0 ) {
4646
+ const w = computeWow( storeTotal, prevStoreTotal );
4647
+ excelRows.push( {
4648
+ storeId: ( cur?.store_id || prev?.store_id || k.slice( clientPrefix.length ) ),
4649
+ storeName: cur?.storeName || prev?.storeName || '',
4650
+ questionFlag: cur?.questionFlag || 0,
4651
+ timeFlag: cur?.timeFlag || 0,
4652
+ runAIFlag: cur?.runAIFlag || 0,
4653
+ recurringFlag: storeRecurring,
4654
+ totalFlags: storeTotal,
4655
+ prevTotal: prevStoreTotal,
4656
+ wow: w.value ? `${w.direction === 'up' ? '+' : '-'}${w.value}` : '0%',
4657
+ } );
4658
+ }
4659
+ }
4660
+
4661
+ if ( totalFlags === 0 && prevTotalFlags === 0 ) return; // skip silent clients
4662
+
4663
+ const top = topByClient.get( clientId ) || { topFlag: null, topRecurring: null };
4664
+ let highestFlaggedStoreWow = { value: '', direction: 'up' };
4665
+ if ( top.topFlag ) {
4666
+ const prev = flagsPrev.get( `${clientId}::${top.topFlag.storeId}` );
4667
+ highestFlaggedStoreWow = computeWow( top.topFlag.value, prev?.totalFlags || 0 );
4668
+ }
4669
+ let highestRecurringStoreWow = { value: '', direction: 'up' };
4670
+ if ( top.topRecurring ) {
4671
+ const prev = recurPrev.get( `${clientId}::${top.topRecurring.storeId}` );
4672
+ highestRecurringStoreWow = computeWow( top.topRecurring.value, prev?.count || 0 );
4673
+ }
4674
+
4675
+ const excelHeader = `Weekly Summary ${startDateLabel} - ${endDateLabel}`;
4676
+ const buf = await buildWeeklyWrapExcel( excelHeader, excelRows );
4677
+ const attachmentBuffer = Buffer.from( buf );
4678
+ const sizeKb = Math.max( 1, Math.round( attachmentBuffer.length / 1024 ) );
4679
+
4680
+ const data = {
4681
+ startDate: startDateLabel,
4682
+ endDate: endDateLabel,
4683
+ storesFlagged,
4684
+ storesFlaggedWow: computeWow( storesFlagged, prevStoresFlagged ),
4685
+ totalFlags,
4686
+ totalFlagsWow: computeWow( totalFlags, prevTotalFlags ),
4687
+ checklistFlags: flaggedChecklistsThis.size,
4688
+ checklistFlagsWow: computeWow( flaggedChecklistsThis.size, flaggedChecklistsPrev.size ),
4689
+ questionFlags,
4690
+ notSubmittedFlags,
4691
+ runAIFlags,
4692
+ recurringFlags,
4693
+ highestFlaggedStore: top.topFlag?.storeName || top.topFlag?.storeId || '--',
4694
+ highestFlaggedStoreWow,
4695
+ highestRecurringStore: top.topRecurring?.storeName || top.topRecurring?.storeId || '--',
4696
+ highestRecurringStoreWow,
4697
+ attachmentName: `${excelHeader}.xlsx`,
4698
+ attachmentSize: `${sizeKb} KB`,
4699
+ domain: flagDomain,
4700
+ };
4701
+
4702
+ const html = compiled( { data } );
4703
+ const attachment = {
4704
+ filename: `${excelHeader}.xlsx`,
4705
+ content: attachmentBuffer,
4706
+ contentType: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet',
4707
+ };
4708
+ const sourceEmail = JSON.parse( process.env.SES ).adminEmail;
4709
+ const subject = `TangoEye | Weekly Wrap (${startDateLabel} - ${endDateLabel})`;
4710
+
4711
+ sendEmailWithSES( STATIC_RECIPIENT, subject, html, attachment, sourceEmail );
4712
+ sentSummary.push( { recipient: STATIC_RECIPIENT, clientId, totalFlags } );
4713
+ } catch ( e ) {
4714
+ logger.error( { functionName: 'weeklyWrapAlert.client', clientId, error: e } );
4715
+ }
4716
+ } ) );
4717
+
4718
+ return res.sendSuccess( { message: 'Weekly wrap dispatched', range, sent: sentSummary } );
4719
+ } catch ( e ) {
4720
+ logger.error( { functionName: 'weeklyWrapAlert', error: e } );
4721
+ return res.sendError( e, 500 );
4722
+ }
4723
+ }
4724
+
4425
4725
  export async function updateStoreLatLong( req, res ) {
4426
4726
  try {
4427
- const defaultStores = [
4428
- { 'storeName': 'OFLBTM', 'lat': 12.9059052, 'long': 77.6057203 },
4429
- { 'storeName': 'OFLRRN', 'lat': 12.9096852, 'long': 77.5135813 },
4430
- { 'storeName': 'OFLGUN', 'lat': 12.9286282, 'long': 77.738055 },
4431
- { 'storeName': 'OFLJPN', 'lat': 12.8915891, 'long': 77.5776564 },
4432
- { 'storeName': 'OFLAECS', 'lat': 12.9638564, 'long': 77.7125467 },
4433
- { 'storeName': 'Seegehalli', 'lat': 13.0083064, 'long': 77.7588426 },
4434
- { 'storeName': 'OFLJPNS', 'lat': 12.8695409, 'long': 77.5820004 },
4435
- { 'storeName': 'OFLHAR', 'lat': 12.9136122, 'long': 77.6649999 },
4436
- { 'storeName': 'OFLKSNR', 'lat': 13.0051196, 'long': 77.6601987 },
4437
- { 'storeName': 'Hosa Road', 'lat': 12.8792369, 'long': 77.6721843 },
4438
- { 'storeName': 'OFLDEV', 'lat': 12.8942057, 'long': 77.6019696 },
4439
- { 'storeName': 'OFLAYN', 'lat': 12.958161, 'long': 77.570055 },
4440
- { 'storeName': 'OFLKAG', 'lat': 12.9845196, 'long': 77.6758945 },
4441
- { 'storeName': 'OFLBVR', 'lat': 12.9858428, 'long': 77.5424725 },
4442
- { 'storeName': 'OFLMTK', 'lat': 13.0279313, 'long': 77.5587934 },
4443
- { 'storeName': 'OFLMAG', 'lat': 12.9837929, 'long': 77.5325324 },
4444
- { 'storeName': 'OFLBEL', 'lat': 13.0370029, 'long': 77.5622911 },
4445
- { 'storeName': 'OFLKDU', 'lat': 12.8835726, 'long': 77.517709 },
4446
- { 'storeName': 'OFLAMLI', 'lat': 13.0685032, 'long': 77.5972815 },
4447
- { 'storeName': 'OFLSAN', 'lat': 19.0603798, 'long': 73.0041633 },
4448
- { 'storeName': 'OFLLOK', 'lat': 19.1471544, 'long': 72.5405682 },
4449
- { 'storeName': 'OFLTSTG', 'lat': 19.2471119, 'long': 72.9769107 },
4450
- { 'storeName': 'OFLKHGRS', 'lat': 19.0583611, 'long': 73.0584353 },
4451
- { 'storeName': 'OFLMAL', 'lat': 12.9673877, 'long': 77.499519 },
4452
- { 'storeName': 'OFLBAG', 'lat': 13.1218427, 'long': 77.6234015 },
4453
- { 'storeName': 'OFLYEL', 'lat': 13.114551, 'long': 77.5401312 },
4454
- { 'storeName': 'OFLTCP', 'lat': 13.0240695, 'long': 77.6928401 },
4455
- { 'storeName': 'OFLECTN', 'lat': 12.8185795, 'long': 77.6520784 },
4456
- { 'storeName': 'OFLHBL', 'lat': 13.0559812, 'long': 77.593941 },
4457
- { 'storeName': 'OFLBWR', 'lat': 12.9691559, 'long': 77.739599 },
4458
- { 'storeName': 'OFLHSR', 'lat': 12.9183666, 'long': 77.5793797 },
4459
- ];
4727
+ const defaultStores = [];
4460
4728
 
4461
4729
  const list = Array.isArray( req.body?.stores ) && req.body.stores.length ? req.body.stores : defaultStores;
4462
4730
 
@@ -234,15 +234,15 @@ export async function startChecklist( req, res ) {
234
234
  },
235
235
  } );
236
236
  let getupdatedchecklist = await processedchecklist.aggregate( findQuery );
237
- let bucket = JSON.parse( process.env.BUCKET );
237
+ // let bucket = JSON.parse( process.env.BUCKET );
238
238
  getupdatedchecklist[0].questionAnswers.forEach( ( section ) => {
239
239
  section.questions.forEach( async ( question ) => {
240
240
  if ( question.questionReferenceImage && question.questionReferenceImage!='' ) {
241
- question.questionReferenceImage = await signedUrl( { Bucket: bucket.sop, file_path: decodeURIComponent( question.questionReferenceImage ) } );
241
+ question.questionReferenceImage = `${cdnurl.TraxAnswerCDN}${question.questionReferenceImage}`;
242
242
  }
243
243
  question.answers.forEach( async ( answer ) => {
244
244
  if ( answer.referenceImage != '' ) {
245
- answer.referenceImage = await signedUrl( { Bucket: bucket.sop, file_path: decodeURIComponent( answer.referenceImage ) } );
245
+ answer.referenceImage = `${cdnurl.TraxAnswerCDN}${answer.referenceImage}`;
246
246
  }
247
247
  } );
248
248
  } );
@@ -469,17 +469,14 @@ export async function startTask( req, res ) {
469
469
 
470
470
 
471
471
  if ( !( task.checkListFrom && task.checkListFrom == 'api' ) ) {
472
- const bucket = JSON.parse( process.env.BUCKET );
472
+ // const bucket = JSON.parse( process.env.BUCKET );
473
473
  await Promise.all(
474
474
  getUpdatedTask[0].questionAnswers.map( ( section ) =>
475
475
  section.questions.map( async ( question ) => {
476
476
  if ( question.questionReferenceImage.length > 0 ) {
477
477
  question.questionReferenceImage = await Promise.all(
478
478
  question.questionReferenceImage.map( async ( image ) => {
479
- return await signedUrl( {
480
- Bucket: bucket.sop,
481
- file_path: decodeURIComponent( image ),
482
- } );
479
+ return `${cdnurl.TraxAnswerCDN}${image}`;
483
480
  } ),
484
481
  );
485
482
  }
@@ -489,10 +486,7 @@ export async function startTask( req, res ) {
489
486
  if ( answer.referenceImage.length > 0 ) {
490
487
  answer.referenceImage = await Promise.all(
491
488
  answer.referenceImage.map( async ( image ) => {
492
- return await signedUrl( {
493
- Bucket: bucket.sop,
494
- file_path: decodeURIComponent( image ),
495
- } );
489
+ return `${cdnurl.TraxAnswerCDN}${image}`;
496
490
  } ),
497
491
  );
498
492
  }
@@ -2103,7 +2097,7 @@ async function updateRecurringFlagTracker( processedChecklist, questionAnswers,
2103
2097
  ops.push( {
2104
2098
  updateOne: {
2105
2099
  filter: { ...trackerKeyBase, section_id: sectionId, qno },
2106
- update: { $set: { consecutiveCount: 0, lastFlaggedDate: today } },
2100
+ update: { $set: { lastFlaggedDate: today } },
2107
2101
  },
2108
2102
  } );
2109
2103
  }
@@ -3579,7 +3573,7 @@ export async function questionList( req, res ) {
3579
3573
  return res.sendError( 'Check List Got Edited Please Fill Again', 422 );
3580
3574
  } else {
3581
3575
  logger.info( `v5 => Checklist Continue => store Name: ${getchecklist[0].storeName}, User Email: ${getchecklist[0].userEmail}, Checklist Name: ${getchecklist[0].checkListName}` );
3582
- let bucket = JSON.parse( process.env.BUCKET );
3576
+ // let bucket = JSON.parse( process.env.BUCKET );
3583
3577
  let cdnurl = JSON.parse( process.env.CDNURL );
3584
3578
  for ( let [ secIndex, section ] of getchecklist[0].questionAnswers.entries() ) {
3585
3579
  for ( let [ questionIndex, question ] of section.questions.entries() ) {
@@ -3605,7 +3599,7 @@ export async function questionList( req, res ) {
3605
3599
  let checkvalidation = null;
3606
3600
  if ( answer.validationAnswer && answer.validationAnswer !='' ) {
3607
3601
  if ( answer.validationType != 'Descriptive Answer' ) {
3608
- checkvalidation = await signedUrl( { file_path: decodeURIComponent( answer.validationAnswer ), Bucket: bucket.sop } );
3602
+ checkvalidation = `${cdnurl.TraxAnswerCDN}${answer.validationAnswer}`;
3609
3603
  } else {
3610
3604
  checkvalidation = answer.validationAnswer;
3611
3605
  }
@@ -3626,23 +3620,23 @@ export async function questionList( req, res ) {
3626
3620
  answer.referenceImage= `${cdnurl.TraxAnswerCDN}${answer.referenceImage}`;
3627
3621
  }
3628
3622
  if ( ( answer.validationType == 'Capture Image' || answer.validationType == 'Capture Video' ) && answer.validationAnswer && answer.validationAnswer != '' ) {
3629
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].validationAnswer = await signedUrl( { file_path: decodeURIComponent( answer.validationAnswer ), Bucket: bucket.sop } );
3623
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].validationAnswer = `${cdnurl.TraxAnswerCDN}${answer.validationAnswer}`;
3630
3624
  }
3631
3625
  }
3632
3626
  if ( question?.userAnswer ) {
3633
3627
  for ( let [ userAnsIndex, userAns ] of question.userAnswer.entries() ) {
3634
3628
  if ( ( userAns.validationType == 'Capture Image' || userAns.validationType == 'Capture Video' ) && userAns.validationAnswer && userAns.validationAnswer != '' ) {
3635
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].validationAnswer = await signedUrl( { file_path: decodeURIComponent( userAns.validationAnswer ), Bucket: bucket.sop } );
3629
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].validationAnswer = `${cdnurl.TraxAnswerCDN}${userAns.validationAnswer}`;
3636
3630
  }
3637
3631
  if ( userAns.validationType == 'Capture Multiple Image with description' ) {
3638
3632
  let imageAnswers = userAns.validationImage;
3639
3633
  userAns.validationImage = [];
3640
3634
  for ( let image of imageAnswers ) {
3641
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].validationImage.push( await signedUrl( { file_path: decodeURIComponent( image ), Bucket: bucket.sop } ) );
3635
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].validationImage.push( `${cdnurl.TraxAnswerCDN}${image}` );
3642
3636
  }
3643
3637
  }
3644
3638
  if ( [ 'image', 'descriptiveImage', 'video' ].includes( question.answerType ) && userAns.answer != '' ) {
3645
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].answer = await signedUrl( { file_path: decodeURIComponent( userAns.answer ), Bucket: bucket.sop } );
3639
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].answer =`${cdnurl.TraxAnswerCDN}${userAns.answer}`;
3646
3640
  }
3647
3641
  if ( userAns.referenceImage != '' ) {
3648
3642
  // getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].referenceImage = await signedUrl( { file_path: decodeURIComponent( userAns.referenceImage ), Bucket: bucket.sop } );
@@ -3949,7 +3943,7 @@ export async function taskQuestionList( req, res ) {
3949
3943
  return res.sendError( 'Task Got Edited Please Fill Again', 422 );
3950
3944
  } else {
3951
3945
  logger.info( `v5 => Task Continue => store Name: ${getchecklist[0].storeName}, User Email: ${getchecklist[0].userEmail}, Task Name: ${getchecklist[0].checkListName}` );
3952
- let bucket = JSON.parse( process.env.BUCKET );
3946
+ // let bucket = JSON.parse( process.env.BUCKET );
3953
3947
  let cdnurl = JSON.parse( process.env.CDNURL );
3954
3948
  for ( let [ secIndex, section ] of getchecklist[0].questionAnswers.entries() ) {
3955
3949
  for ( let [ questionIndex, question ] of section.questions.entries() ) {
@@ -3960,13 +3954,10 @@ export async function taskQuestionList( req, res ) {
3960
3954
  if ( Array.isArray( questionReferenceImage ) ) {
3961
3955
  questionReferenceImage = questionReferenceImage.filter( ( item ) => item != '' );
3962
3956
  getchecklist[0].questionAnswers[secIndex].questions[questionIndex].questionReferenceImage = await Promise.all(
3963
- questionReferenceImage.map( async ( image ) => image ? await signedUrl( { file_path: decodeURIComponent( image ), Bucket: bucket.sop } ) : '' ),
3957
+ questionReferenceImage.map( async ( image ) => image ? `${cdnurl.TraxAnswerCDN}${image}` : '' ),
3964
3958
  );
3965
3959
  } else if ( questionReferenceImage !== '' ) {
3966
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].questionReferenceImage = await signedUrl( {
3967
- file_path: decodeURIComponent( questionReferenceImage ),
3968
- Bucket: bucket.sop,
3969
- } );
3960
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].questionReferenceImage = `${cdnurl.TraxAnswerCDN}${questionReferenceImage}`;
3970
3961
  }
3971
3962
  }
3972
3963
 
@@ -3977,13 +3968,10 @@ export async function taskQuestionList( req, res ) {
3977
3968
  if ( Array.isArray( referenceImage ) ) {
3978
3969
  referenceImage = referenceImage.filter( ( item ) => item != '' );
3979
3970
  getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].referenceImage = await Promise.all(
3980
- referenceImage.map( async ( image ) => image ? await signedUrl( { file_path: decodeURIComponent( image ), Bucket: bucket.sop } ) : '' ),
3971
+ referenceImage.map( async ( image ) => image ? `${cdnurl.TraxAnswerCDN}${image}` : '' ),
3981
3972
  );
3982
3973
  } else if ( referenceImage !== '' ) {
3983
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].referenceImage = await signedUrl( {
3984
- file_path: decodeURIComponent( referenceImage ),
3985
- Bucket: bucket.sop,
3986
- } );
3974
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].referenceImage = `${cdnurl.TraxAnswerCDN}${referenceImage}`;
3987
3975
  }
3988
3976
  }
3989
3977
 
@@ -3991,7 +3979,7 @@ export async function taskQuestionList( req, res ) {
3991
3979
  let checkvalidation = null;
3992
3980
  if ( answer.validationAnswer && answer.validationAnswer != '' ) {
3993
3981
  if ( answer.validationType != 'Descriptive Answer' ) {
3994
- checkvalidation = await signedUrl( { file_path: decodeURIComponent( answer.validationAnswer ), Bucket: bucket.sop } );
3982
+ checkvalidation = `${cdnurl.TraxAnswerCDN}${answer.validationAnswer}`;
3995
3983
  } else {
3996
3984
  checkvalidation = answer.validationAnswer;
3997
3985
  }
@@ -4000,10 +3988,7 @@ export async function taskQuestionList( req, res ) {
4000
3988
  getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].index = ansIndex;
4001
3989
  }
4002
3990
  if ( ( answer.validationType == 'Capture Image' || answer.validationType == 'Capture Video' ) && answer.validationAnswer && answer.validationAnswer != '' ) {
4003
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].validationAnswer = await signedUrl( {
4004
- file_path: decodeURIComponent( answer.validationAnswer ),
4005
- Bucket: bucket.sop,
4006
- } );
3991
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].answers[ansIndex].validationAnswer = `${cdnurl.TraxAnswerCDN}${answer.validationAnswer}`;
4007
3992
  }
4008
3993
  }
4009
3994
 
@@ -4014,25 +3999,16 @@ export async function taskQuestionList( req, res ) {
4014
3999
  getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].linearType = question.answers[0].linearType;
4015
4000
  }
4016
4001
  if ( ( userAns.validationType == 'Capture Image' || userAns.validationType == 'Capture Video' ) && userAns.validationAnswer && userAns.validationAnswer != '' ) {
4017
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].validationAnswer = await signedUrl( {
4018
- file_path: decodeURIComponent( userAns.validationAnswer ),
4019
- Bucket: bucket.sop,
4020
- } );
4002
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].validationAnswer = `${cdnurl.TraxAnswerCDN}${userAns.validationAnswer}`;
4021
4003
  }
4022
4004
  if ( [ 'image', 'descriptiveImage', 'video' ].includes( question.answerType ) && rawAnswer != '' ) {
4023
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].answer = await signedUrl( {
4024
- file_path: decodeURIComponent( rawAnswer ),
4025
- Bucket: bucket.sop,
4026
- } );
4005
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].answer = `${cdnurl.TraxAnswerCDN}${rawAnswer}`;
4027
4006
  }
4028
4007
  if ( question.answerType == 'image/video' && rawAnswer != '' ) {
4029
4008
  getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].answer = `${cdnurl.TraxAnswerCDN}${rawAnswer}`;
4030
4009
  }
4031
4010
  if ( userAns.referenceImage != '' ) {
4032
- getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].referenceImage = await signedUrl( {
4033
- file_path: decodeURIComponent( userAns.referenceImage ),
4034
- Bucket: bucket.sop,
4035
- } );
4011
+ getchecklist[0].questionAnswers[secIndex].questions[questionIndex].userAnswer[userAnsIndex].referenceImage = `${cdnurl.TraxAnswerCDN}${userAns.referenceImage}`;
4036
4012
  }
4037
4013
  if ( question.answerType == 'multiplechoicemultiple' || ( question.answerType == 'dropdown' && question.allowMultiple ) ) {
4038
4014
  let ansIndex = Multianswer.findIndex( ( item ) => item.answer == rawAnswer );
@@ -206,12 +206,12 @@
206
206
  {{#if userAnswer.length}}
207
207
  <span class="question">{{qno}}.</span>&nbsp;<span class="question">{{qname}}</span>
208
208
  {{#neq questionReferenceImage ''}}
209
- <div class="Reference"><span>Reference Image</span><br>
209
+ <div class="Reference"><span>Question Reference Image</span><br>
210
210
  <img src="{{questionReferenceImage}}" width="200" height="180" />
211
211
  </div>
212
212
  {{/neq}}
213
213
  {{#if multiQuestionReferenceImage.length}}
214
- <div class="Reference"><span>Reference Images</span><br>
214
+ <div class="Reference"><span>Question Reference Image</span><br>
215
215
  {{#each multiQuestionReferenceImage}}
216
216
  <img src="{{this}}" width="200" height="180" />
217
217
  {{/each}}
@@ -274,15 +274,15 @@
274
274
  {{!-- {{#each ../answers}} --}}
275
275
  <table style="width:100%">
276
276
  <tr>
277
- {{#neq referenceImage ''}}
277
+ {{#if multiReferenceImage.length}}
278
278
  <td style="width:50%">
279
- {{#each multiReferenceImage}}
280
- <div class="Reference"><span>Reference Image</span><br>
279
+ <div class="Reference"><span>Reference Image</span><br>
280
+ {{#each multiReferenceImage}}
281
281
  <img src="{{this}}" width="200" height="180" />
282
- </div>
283
- {{/each}}
282
+ {{/each}}
283
+ </div>
284
284
  </td>
285
- {{/neq}}
285
+ {{/if}}
286
286
  <td style="width:50%">
287
287
  <div class="Reference"><span>Uploaded Image</span><br>
288
288
  <img src="{{answer}}" width="200" height="180" />
@@ -296,15 +296,15 @@
296
296
  {{!-- {{#each ../answers}} --}}
297
297
  <table style="width:100%">
298
298
  <tr>
299
- {{#neq referenceImage ''}}
299
+ {{#if multiReferenceImage.length}}
300
300
  <td style="width:50%">
301
- {{#each multiReferenceImage}}
302
301
  <div class="Reference"><span>Reference Image</span><br>
302
+ {{#each multiReferenceImage}}
303
303
  <img src="{{this}}" width="200" height="180" />
304
+ {{/each}}
304
305
  </div>
305
- {{/each}}
306
306
  </td>
307
- {{/neq}}
307
+ {{/if}}
308
308
  <td style="width:50%">
309
309
  <div class="Reference"><span>Uploaded Image</span><br>
310
310
  <img src="{{answer}}" width="200" height="180" />
@@ -318,13 +318,15 @@
318
318
  {{!-- {{#each ../answers}} --}}
319
319
  <table style="width:100%">
320
320
  <tr>
321
- {{#neq referenceImage ''}}
321
+ {{#if multiReferenceImage.length}}
322
322
  <td style="width:50%">
323
323
  <div class="Reference"><span>Reference Image</span><br>
324
- <img src="{{referenceImage}}" width="200" height="180" />
324
+ {{#each multiReferenceImage}}
325
+ <img src="{{this}}" width="200" height="180" />
326
+ {{/each}}
325
327
  </div>
326
328
  </td>
327
- {{/neq}}
329
+ {{/if}}
328
330
  {{#eq answerType 'image'}}
329
331
  <td style="width:50%">
330
332
  <div class="Reference"><span>Uploaded Image</span><br>
@@ -345,13 +347,15 @@
345
347
  {{!-- {{#each ../answers}} --}}
346
348
  <table style="width:100%">
347
349
  <tr>
348
- {{#neq referenceImage ''}}
350
+ {{#if multiReferenceImage.length}}
349
351
  <td style="width:50%">
350
352
  <div class="Reference"><span>Reference Image</span><br>
351
- <img src="{{referenceImage}}" width="200" height="180" />
353
+ {{#each multiReferenceImage}}
354
+ <img src="{{this}}" width="200" height="180" />
355
+ {{/each}}
352
356
  </div>
353
357
  </td>
354
- {{/neq}}
358
+ {{/if}}
355
359
  <td style="width:50%">
356
360
  <div class="Reference"><span>Uploaded Image</span><br>
357
361
  <img src="{{answer}}" width="200" height="180" />
@@ -181,13 +181,13 @@
181
181
  </div>
182
182
  {{#if this.questionReferenceImage}}
183
183
  <div class="q-answer-media">
184
- <div class="q-answer-caption">Reference Image</div>
184
+ <div class="q-answer-caption">Question Reference Image</div>
185
185
  <img src="{{this.questionReferenceImage}}" alt="Reference Image" />
186
186
  </div>
187
187
  {{/if}}
188
188
  {{#if this.multiQuestionReferenceImage.length}}
189
189
  <div class="q-answer-media">
190
- <div class="q-answer-caption">Reference Images</div>
190
+ <div class="q-answer-caption">Question Reference Images</div>
191
191
  {{#each this.multiQuestionReferenceImage}}
192
192
  <img src="{{this}}" alt="Reference Image" />
193
193
  {{/each}}
@@ -0,0 +1,218 @@
1
+ <!DOCTYPE html>
2
+ <html>
3
+ <head>
4
+ <meta charset="utf-8">
5
+ <meta http-equiv="x-ua-compatible" content="ie=edge">
6
+ <title>Weekly Wrap</title>
7
+ <meta name="viewport" content="width=device-width, initial-scale=1">
8
+ <style type="text/css">
9
+ body { font-family: "Inter", sans-serif !important; margin:0; padding:0; background:#dbe5ea; }
10
+ table { border-collapse: collapse; }
11
+ a { color:#1a82e2; }
12
+ .card {
13
+ background:#F4F8FB;
14
+ border-radius:8px;
15
+ padding:20px 22px;
16
+ }
17
+ .cardLabel { font-size:13px; color:#475569; margin-bottom:6px; }
18
+ .cardValue { font-size:22px; font-weight:700; color:#0F172A; }
19
+ .wow { font-size:12px; font-weight:600; margin-left:6px; }
20
+ .wow.up { color:#16A34A; }
21
+ .wow.down { color:#DC2626; }
22
+ .wowLabel { font-size:11px; color:#94A3B8; margin-left:4px; }
23
+ .sectionTitle { font-size:16px; font-weight:700; color:#0F172A; margin:18px 0 10px; }
24
+ .breakdownRow { font-size:14px; color:#1E293B; line-height:24px; }
25
+ .insightCard {
26
+ background:#EFF7FF;
27
+ border-radius:8px;
28
+ padding:20px 22px;
29
+ }
30
+ .attach {
31
+ background:#F1F5F9;
32
+ border-radius:8px;
33
+ padding:12px 14px;
34
+ display:flex;
35
+ align-items:center;
36
+ gap:10px;
37
+ font-size:13px;
38
+ color:#0F172A;
39
+ }
40
+ .ctaBtn {
41
+ display:inline-block;
42
+ background:#00A3FF;
43
+ color:#FFFFFF !important;
44
+ padding:14px 36px;
45
+ font-weight:600;
46
+ font-size:15px;
47
+ border-radius:8px;
48
+ text-decoration:none;
49
+ }
50
+ .footerText { font-size:12px; color:#475569; line-height:18px; }
51
+ </style>
52
+ </head>
53
+ <body>
54
+ <table width="100%" cellpadding="0" cellspacing="0" style="background:#dbe5ea;padding:24px 10px;">
55
+ <tr>
56
+ <td align="center" style="padding-top:10px;padding-bottom:10px">
57
+ <table width="100%" cellpadding="0" cellspacing="0" style="max-width:680px;background:#FFFFFF;border:1px dashed #93B6CC;border-radius:10px;overflow:hidden;">
58
+
59
+ {{!-- Header --}}
60
+ <tr>
61
+ <td style="padding:20px 24px 0 24px;">
62
+ <img src="https://devtangoretail-api.tangoeye.ai/logo.png" width="160" alt="TangoEye"/>
63
+ </td>
64
+ </tr>
65
+
66
+ {{!-- Banner --}}
67
+ <tr>
68
+ <td style="padding:14px 24px 0 24px;">
69
+ <div style="position:relative;background:#1F2A37;border-radius:10px;color:#FFFFFF;padding:36px 20px;text-align:center;">
70
+ <div style="font-size:22px;font-weight:700;">Weekly Wrap</div>
71
+ <div style="font-size:13px;color:#CBD5E1;margin-top:6px;">
72
+ Your Store's Flags Insights for {{data.startDate}} - {{data.endDate}}
73
+ </div>
74
+ </div>
75
+ </td>
76
+ </tr>
77
+
78
+ {{!-- Greeting --}}
79
+ <tr>
80
+ <td style="padding:18px 24px 0 24px;font-size:14px;color:#1E293B;line-height:22px;">
81
+ Hi Team,<br/>
82
+ Here is your weekly checklist insights report, highlighting key observations, recurring issues, and areas that may require attention to improve store performance and compliance.
83
+ </td>
84
+ </tr>
85
+
86
+ {{!-- Weekly Overview --}}
87
+ <tr>
88
+ <td style="padding:8px 24px 0 24px;">
89
+ <div class="sectionTitle">Weekly Overview</div>
90
+ <table width="100%" cellpadding="0" cellspacing="8">
91
+ <tr>
92
+ <td width="30%" style="padding-right:10px">
93
+ <div class="card">
94
+ <div class="cardLabel">Stores Flagged</div>
95
+ <div>
96
+ <span class="cardValue">{{data.storesFlagged}}</span>
97
+ {{#if data.storesFlaggedWow.value}}
98
+ <span class="wow {{data.storesFlaggedWow.direction}}">{{#eq data.storesFlaggedWow.direction "up"}}&#x2197;{{else}}&#x2198;{{/eq}} {{data.storesFlaggedWow.value}}</span>
99
+ <span class="wowLabel">WoW</span>
100
+ {{/if}}
101
+ </div>
102
+ </div>
103
+ </td>
104
+ <td width="30%" style="padding-right:10px">
105
+ <div class="card">
106
+ <div class="cardLabel">Total Flags</div>
107
+ <div>
108
+ <span class="cardValue">{{data.totalFlags}}</span>
109
+ {{#if data.totalFlagsWow.value}}
110
+ <span class="wow {{data.totalFlagsWow.direction}}">{{#eq data.totalFlagsWow.direction "up"}}&#x2197;{{else}}&#x2198;{{/eq}} {{data.totalFlagsWow.value}}</span>
111
+ <span class="wowLabel">WoW</span>
112
+ {{/if}}
113
+ </div>
114
+ </div>
115
+ </td>
116
+ <td width="30%" style="padding-right:10px">
117
+ <div class="card">
118
+ <div class="cardLabel">Checklist Flags</div>
119
+ <div>
120
+ <span class="cardValue">{{data.checklistFlags}}</span>
121
+ {{#if data.checklistFlagsWow.value}}
122
+ <span class="wow {{data.checklistFlagsWow.direction}}">{{#eq data.checklistFlagsWow.direction "up"}}&#x2197;{{else}}&#x2198;{{/eq}} {{data.checklistFlagsWow.value}}</span>
123
+ <span class="wowLabel">WoW</span>
124
+ {{/if}}
125
+ </div>
126
+ </div>
127
+ </td>
128
+ </tr>
129
+ </table>
130
+ </td>
131
+ </tr>
132
+
133
+ {{!-- Flags Breakdown --}}
134
+ <tr>
135
+ <td style="padding:8px 24px 0 24px;">
136
+ <div class="sectionTitle">Flags Breakdown</div>
137
+ <div class="breakdownRow">
138
+ Question Flags : {{data.questionFlags}} &nbsp;&nbsp;
139
+ Not Submitted Flags : {{data.notSubmittedFlags}} &nbsp;&nbsp;
140
+ Run AI Flags : {{data.runAIFlags}} &nbsp;&nbsp;
141
+ Recurring Flags : {{data.recurringFlags}}
142
+ </div>
143
+ </td>
144
+ </tr>
145
+
146
+ {{!-- Key Insights --}}
147
+ <tr>
148
+ <td style="padding:8px 24px 0 24px;">
149
+ <div class="sectionTitle">Key Insights</div>
150
+ <table width="100%" cellpadding="0" cellspacing="8">
151
+ <tr>
152
+ <td width="40%" style="padding-right:10px">
153
+ <div class="insightCard">
154
+ <div class="cardLabel">Highest Flagged Store</div>
155
+ <div>
156
+ <span class="cardValue">{{data.highestFlaggedStore}}</span>
157
+ {{#if data.highestFlaggedStoreWow.value}}
158
+ <span class="wow {{data.highestFlaggedStoreWow.direction}}">{{#eq data.highestFlaggedStoreWow.direction "up"}}&#x2197;{{else}}&#x2198;{{/eq}} {{data.highestFlaggedStoreWow.value}}</span>
159
+ <span class="wowLabel">WoW</span>
160
+ {{/if}}
161
+ </div>
162
+ </div>
163
+ </td>
164
+ <td width="40%">
165
+ <div class="insightCard">
166
+ <div class="cardLabel">Highest Recurring Flagged Store</div>
167
+ <div>
168
+ <span class="cardValue">{{data.highestRecurringStore}}</span>
169
+ {{#if data.highestRecurringStoreWow.value}}
170
+ <span class="wow {{data.highestRecurringStoreWow.direction}}">{{#eq data.highestRecurringStoreWow.direction "up"}}&#x2197;{{else}}&#x2198;{{/eq}} {{data.highestRecurringStoreWow.value}}</span>
171
+ <span class="wowLabel">WoW</span>
172
+ {{/if}}
173
+ </div>
174
+ </div>
175
+ </td>
176
+ </tr>
177
+ </table>
178
+ </td>
179
+ </tr>
180
+
181
+ {{!-- Attachment hint --}}
182
+ <tr>
183
+ <td style="padding:18px 24px 0 24px;">
184
+ <div style="color:#1D2939;font-family: Inter;font-weight: 400;font-style: Regular;font-size: 14px;leading-trim: NONE;line-height: 100%;letter-spacing: 0.2px;">
185
+ Refer to the attached report for the weekly summary of the stores assigned to you.
186
+ </div>
187
+ <div style="color:#384860;font-family: Inter;font-weight: 400;font-style: Regular;font-size: 10px;leading-trim: NONE;line-height: 150%;letter-spacing: 0.2px;padding-top:10px">
188
+ If you have any questions or need assistance, please reach out to us at support@tangotech.co.in.
189
+ </div>
190
+ {{!-- <div class="attach">
191
+ <span style="display:inline-block;width:22px;height:22px;background:#21A366;color:#FFFFFF;border-radius:4px;text-align:center;line-height:22px;font-size:12px;font-weight:700;">X</span>
192
+ <span><b>{{data.attachmentName}}</b>{{#if data.attachmentSize}} &nbsp; <span style="color:#94A3B8;">{{data.attachmentSize}}</span>{{/if}}</span>
193
+ </div> --}}
194
+ </td>
195
+ </tr>
196
+
197
+ {{!-- CTA --}}
198
+ <tr>
199
+ <td style="padding:18px 24px 8px 24px;text-align:center;">
200
+ <a href="{{data.domain}}" class="ctaBtn">View Detailed Insights</a>
201
+ </td>
202
+ </tr>
203
+
204
+ {{!-- Footer --}}
205
+ <tr>
206
+ <td style="padding:18px 24px 24px 24px;border-top:1px solid #E2E8F0;">
207
+ <div class="footerText">
208
+ &copy;2024 Tango IT Solutions India Pvt. Ltd. All rights reserved.<br/>
209
+ Old Number 12 and New, 39, Haddows Rd, Thousand Lights West, Nungambakkam, Chennai, Tamil Nadu 600006
210
+ </div>
211
+ </td>
212
+ </tr>
213
+ </table>
214
+ </td>
215
+ </tr>
216
+ </table>
217
+ </body>
218
+ </html>
@@ -38,6 +38,7 @@ internalTraxRouter
38
38
  .post( '/posblock', isAllowedInternalAPIHandler, internalController.getStoreTaskDetails )
39
39
  .post( '/runAIFlag', isAllowedInternalAPIHandler, internalController.runAIFlag )
40
40
  .post( '/recurringFlag', isAllowedInternalAPIHandler, internalController.recurringFlagAlert )
41
+ .post( '/weeklyWrap', isAllowedInternalAPIHandler, internalController.weeklyWrapAlert )
41
42
  .post( '/downloadInsertPdf', isAllowedInternalAPIHandler, internalController.downloadInsertPdf )
42
43
  .get( '/checklistAutoMailList', isAllowedInternalAPIHandler, internalController.checklistAutoMailList )
43
44
  .post( '/sendAIEmailList', isAllowedInternalAPIHandler, internalController.sendAIEmailList )