tango-app-api-trax 3.7.92 → 3.7.94

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.7.92",
3
+ "version": "3.7.94",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -30,8 +30,16 @@ import handlebars from './handlebar-helper.js';
30
30
  import { buildVisitChecklistTemplateData, getCompiledVisitChecklistTemplate, createImageCache, resolveTemplateUrls } from '../utils/visitChecklistPdf.utils.js';
31
31
  import archiver from 'archiver';
32
32
  import puppeteer from 'puppeteer';
33
+ import { getBrowser as getBrowserInstance } from '../utils/browserPool.utils.js';
33
34
  import fs from 'fs';
34
35
  import path from 'path';
36
+ import { fileURLToPath as toPath } from 'url';
37
+
38
+ const __ctrlDir = path.dirname( toPath( import.meta.url ) );
39
+ const tangoyeLogoSvg = fs.readFileSync(
40
+ path.join( __ctrlDir, '../hbs/partials/tangoye-footer-logo.hbs' ),
41
+ 'utf8',
42
+ ).replaceAll( '{{gid}}', 'ft' );
35
43
 
36
44
 
37
45
  const ObjectId = mongoose.Types.ObjectId;
@@ -2711,9 +2719,9 @@ export async function getRunAIList( req, res ) {
2711
2719
  if ( qn.answers.some( ( ele ) => ele.runAI ) ) {
2712
2720
  let checkList = runAIList.findIndex( ( run ) => run.checklist.toString() == check._id.toString() );
2713
2721
  if ( checkList == -1 ) {
2714
- runAIList.push( { checklist: check._id, questions: [ qn.uniqueqid ] } );
2722
+ runAIList.push( { checklist: check._id, checklistName: check.checkListName, questions: [ { name: qn.qname, qid: qn.uniqueqid } ] } );
2715
2723
  } else {
2716
- runAIList[checkList].questions.push( qn.uniqueqid );
2724
+ runAIList[checkList].questions.push( { name: qn.qname, qid: qn.uniqueqid } );
2717
2725
  }
2718
2726
  }
2719
2727
  } );
@@ -3454,6 +3462,7 @@ async function getBrandInfo( clientId ) {
3454
3462
  }
3455
3463
 
3456
3464
  export const downloadInsertPdf = async ( req, res ) => {
3465
+ let page = null;
3457
3466
  try {
3458
3467
  if ( !req.body.checklistId ) {
3459
3468
  return res.sendError( 'Checklist Id is required', 400 );
@@ -3462,129 +3471,138 @@ export const downloadInsertPdf = async ( req, res ) => {
3462
3471
  const pdfTemplate = getCompiledVisitChecklistTemplate();
3463
3472
  const imageCache = createImageCache();
3464
3473
 
3465
- const browser = await puppeteer.launch( {
3466
- headless: 'new',
3467
- args: [ '--no-sandbox', '--disable-dev-shm-usage' ],
3468
- } );
3469
-
3470
- try {
3471
- const page = await browser.newPage();
3472
-
3473
- await page.setViewport( {
3474
- width: 1280,
3475
- height: 1800,
3476
- } );
3477
-
3478
- const safeName = ( str ) =>
3479
- ( str || '' ).toString().replace( /[<>:"/\\|?*]+/g, '_' );
3474
+ const safeName = ( str ) =>
3475
+ ( str || '' ).toString().replace( /[<>:"/\\|?*]+/g, '_' );
3480
3476
 
3481
- let query = {
3482
- query: {
3483
- bool: {
3484
- must: [
3485
- {
3486
- term: {
3487
- _id: req.body.checklistId,
3488
- },
3477
+ const query = {
3478
+ query: {
3479
+ bool: {
3480
+ must: [
3481
+ {
3482
+ term: {
3483
+ _id: req.body.checklistId,
3489
3484
  },
3490
- ],
3491
- },
3485
+ },
3486
+ ],
3492
3487
  },
3493
- };
3488
+ },
3489
+ };
3494
3490
 
3495
- let aiDetails = await getOpenSearchData( JSON.parse( process.env.OPENSEARCH ).traxIndex, query );
3496
- if ( aiDetails?.statusCode != 200 || !aiDetails?.body?.hits?.hits.length ) {
3497
- return res.sendError( 'Checklist not found', 404 );
3498
- }
3491
+ // 1) Launch browser page + fetch OpenSearch data in parallel
3492
+ const [ browser, aiDetails ] = await Promise.all( [
3493
+ getBrowserInstance(),
3494
+ getOpenSearchData( JSON.parse( process.env.OPENSEARCH ).traxIndex, query ),
3495
+ ] );
3499
3496
 
3500
- const doc = { ...aiDetails.body.hits.hits[0]._source };
3497
+ if ( aiDetails?.statusCode != 200 || !aiDetails?.body?.hits?.hits.length ) {
3498
+ return res.sendError( 'Checklist not found', 404 );
3499
+ }
3501
3500
 
3502
- const brandInfo = await getBrandInfo( doc.client_id );
3501
+ const doc = { ...aiDetails.body.hits.hits[0]._source };
3503
3502
 
3504
- const detectionPayload = {
3505
- 'storeId': [ doc.store_id ],
3506
- 'userEmail': doc.userEmail,
3507
- 'sourceChecklist_id': doc.sourceCheckList_id,
3508
- };
3503
+ // 2) Fetch brandInfo + compliance data in parallel
3504
+ const complianceURL = JSON.parse( process.env.LAMBDAURL ).complianceHistory;
3505
+ const detectionPayload = {
3506
+ 'storeId': [ doc.store_id ],
3507
+ 'userEmail': doc.userEmail,
3508
+ 'sourceChecklist_id': doc.sourceCheckList_id,
3509
+ };
3509
3510
 
3510
- let complianceURL = JSON.parse( process.env.LAMBDAURL ).complianceHistory;
3511
- const complianceData = await LamdaServiceCall( complianceURL, detectionPayload );
3512
- if ( complianceData?.data.length ) {
3513
- doc['historyData'] = complianceData.data;
3514
- }
3515
- // CDN fix
3516
- ( doc.questionAnswers || [] ).forEach( ( section ) => {
3517
- ( section.questions || [] ).forEach( ( question ) => {
3518
- ( question.userAnswer || [] ).forEach( ( answer ) => {
3519
- if ( answer?.referenceImage?.trim() ) {
3520
- answer.referenceImage =
3521
- cdnBase + answer.referenceImage;
3522
- }
3523
- } );
3524
- } );
3525
- } );
3511
+ const [ brandInfo, complianceData ] = await Promise.all( [
3512
+ getBrandInfo( doc.client_id ),
3513
+ LamdaServiceCall( complianceURL, detectionPayload ),
3514
+ ] );
3526
3515
 
3527
- const templateData = buildVisitChecklistTemplateData(
3528
- doc,
3529
- brandInfo,
3530
- );
3516
+ if ( complianceData?.data?.length ) {
3517
+ doc['historyData'] = complianceData.data;
3518
+ }
3531
3519
 
3532
- const resolvedData = resolveTemplateUrls( templateData, cdnBase );
3533
- await imageCache.resolveAllImages( resolvedData );
3520
+ // CDN fix
3521
+ ( doc.questionAnswers || [] ).forEach( ( section ) => {
3522
+ ( section.questions || [] ).forEach( ( question ) => {
3523
+ ( question.userAnswer || [] ).forEach( ( answer ) => {
3524
+ if ( answer?.referenceImage?.trim() ) {
3525
+ answer.referenceImage = cdnBase + answer.referenceImage;
3526
+ }
3527
+ } );
3528
+ } );
3529
+ } );
3534
3530
 
3535
- const html = pdfTemplate( resolvedData );
3531
+ const templateData = buildVisitChecklistTemplateData( doc, brandInfo );
3532
+ const resolvedData = resolveTemplateUrls( templateData, cdnBase );
3533
+ await imageCache.resolveAllImages( resolvedData );
3536
3534
 
3537
- try {
3538
- await page.setContent( html, {
3539
- waitUntil: 'networkidle0',
3540
- timeout: 30000,
3541
- } );
3535
+ const html = pdfTemplate( resolvedData );
3542
3536
 
3543
- await page.emulateMediaType( 'screen' );
3544
- } catch ( err ) {
3545
- logger.error( { functionName: 'setContent failed:', error: err } );
3546
- }
3537
+ // 3) Create page and render — images are already base64 so use domcontentloaded
3538
+ page = await browser.newPage();
3539
+ await page.setViewport( { width: 1280, height: 1800 } );
3547
3540
 
3548
- let pdfBuffer;
3541
+ try {
3542
+ await page.setContent( html, {
3543
+ waitUntil: 'domcontentloaded',
3544
+ timeout: 15000,
3545
+ } );
3546
+ await page.emulateMediaType( 'screen' );
3547
+ } catch ( err ) {
3548
+ logger.error( { functionName: 'downloadInsertPdf', message: 'setContent failed', error: err } );
3549
+ }
3549
3550
 
3550
- try {
3551
- pdfBuffer = await page.pdf( {
3552
- format: 'A4',
3553
- printBackground: true,
3554
- margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
3555
- } );
3556
- } catch ( err ) {
3557
- logger.error( { functionName: 'downloadInsertPdfOld', message: 'PDF generation failed', error: err } );
3558
- return res.sendError( 'PDF generation failed', 500 );
3559
- }
3551
+ let pdfBuffer;
3552
+ try {
3553
+ pdfBuffer = await page.pdf( {
3554
+ format: 'A4',
3555
+ printBackground: true,
3556
+ preferCSSPageSize: true,
3557
+ displayHeaderFooter: true,
3558
+ headerTemplate: '<span></span>',
3559
+ footerTemplate: [
3560
+ '<div style="width:100%;padding:0 10mm;font-size:10px;',
3561
+ 'font-family:Arial,sans-serif;display:flex;',
3562
+ 'justify-content:space-between;align-items:center;',
3563
+ 'border-top:1px solid #d9d9d9;padding-top:4px;">',
3564
+ '<span style="color:#999;">Page ',
3565
+ '<span class="pageNumber"></span>',
3566
+ ' of <span class="totalPages"></span></span>',
3567
+ '<span style="display:flex;align-items:center;gap:6px;">',
3568
+ '<span style="color:#666;font-weight:400;font-size:10px;">',
3569
+ 'Generated by</span>',
3570
+ tangoyeLogoSvg,
3571
+ '</span></div>',
3572
+ ].join( '' ),
3573
+ } );
3574
+ } catch ( err ) {
3575
+ logger.error( { functionName: 'downloadInsertPdf', message: 'PDF generation failed', error: err } );
3576
+ return res.sendError( 'PDF generation failed', 500 );
3577
+ }
3560
3578
 
3561
- if ( !pdfBuffer || pdfBuffer.length === 0 ) {
3562
- logger.error( { functionName: 'downloadInsertPdfOld', message: 'Empty PDF', docId: doc._id } );
3563
- return res.sendError( 'PDF generation resulted in empty buffer', 500 );
3564
- }
3579
+ if ( !pdfBuffer || pdfBuffer.length === 0 ) {
3580
+ logger.error( { functionName: 'downloadInsertPdf', message: 'Empty PDF', docId: doc._id } );
3581
+ return res.sendError( 'PDF generation resulted in empty buffer', 500 );
3582
+ }
3565
3583
 
3566
- const finalBuffer = Buffer.isBuffer( pdfBuffer ) ?
3567
- pdfBuffer :
3568
- Buffer.from( pdfBuffer );
3584
+ const finalBuffer = Buffer.isBuffer( pdfBuffer ) ?
3585
+ pdfBuffer :
3586
+ Buffer.from( pdfBuffer );
3569
3587
 
3570
- const pdfName = `${safeName(
3571
- doc.storeName || doc.store_id || doc._id || 'store',
3572
- )}.pdf`;
3588
+ const pdfName = `${safeName(
3589
+ doc.store_id + '_' + ( doc.storeName || 'store' ),
3590
+ )}.pdf`;
3573
3591
 
3574
- res.set( {
3575
- 'Content-Type': 'application/pdf',
3576
- 'Content-Disposition': `attachment; filename="${pdfName}"`,
3577
- 'Content-Length': finalBuffer.length,
3578
- } );
3592
+ res.set( {
3593
+ 'Content-Type': 'application/pdf',
3594
+ 'Content-Disposition': `attachment; filename="${pdfName}"`,
3595
+ 'Content-Length': finalBuffer.length,
3596
+ } );
3579
3597
 
3580
- return res.send( finalBuffer );
3581
- } finally {
3582
- await browser.close();
3583
- }
3598
+ return res.send( finalBuffer );
3584
3599
  } catch ( e ) {
3585
- console.log( e );
3586
- logger.error( { functionName: 'downloadInsertPdfOld', error: e } );
3600
+ logger.error( { functionName: 'downloadInsertPdf', error: e } );
3587
3601
  return res.sendError( e, 500 );
3602
+ } finally {
3603
+ if ( page ) {
3604
+ await page.close().catch( () => {} );
3605
+ }
3588
3606
  }
3589
3607
  };
3590
3608
 
@@ -3595,10 +3613,89 @@ export async function checklistAutoMailList( req, res ) {
3595
3613
  '$expr': {
3596
3614
  $gt: [ { $size: { $ifNull: [ '$autoEmail.type', [] ] } }, 0 ],
3597
3615
  },
3598
- }, { _id: 1 } );
3616
+ }, { _id: 1, autoEmail: 1 } );
3617
+
3618
+ let result = [];
3619
+
3620
+ await Promise.all( checklistInfoList.map( async ( ele ) => {
3621
+ for ( let email of ele?.autoEmail?.users ) {
3622
+ let stores = [];
3623
+ let userDetails = await userService.findOne( { email: email.value } );
3624
+ if ( userDetails ) {
3625
+ if ( userDetails.userType === 'client' && userDetails.role !== 'superadmin' ) {
3626
+ let storeIds = new Set( userDetails.assignedStores.map( ( store ) => store.storeId ) );
3627
+
3628
+ // Fetch clusters and teams in parallel
3629
+ const [ clustersList, teamsList ] = await Promise.all( [
3630
+ clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
3631
+ teamsServices.findteams( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
3632
+ ] );
3633
+
3634
+ // Process clusters
3635
+ if ( clustersList.length > 0 ) {
3636
+ for ( let cluster of clustersList ) {
3637
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3638
+ }
3639
+ }
3640
+
3641
+ // Process teams
3642
+ if ( teamsList.length > 0 ) {
3643
+ for ( let team of teamsList ) {
3644
+ for ( let user of team.users ) {
3645
+ let findUser = await userService.findOne( { _id: user.userId } );
3646
+ if ( findUser && findUser.assignedStores?.length > 0 ) {
3647
+ findUser.assignedStores.forEach( ( store ) => storeIds.add( store.storeId ) );
3648
+ }
3649
+
3650
+ // Fetch clusters for the user
3651
+ let userClustersList = await clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: findUser.email } } } );
3652
+ if ( userClustersList.length > 0 ) {
3653
+ for ( let cluster of userClustersList ) {
3654
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3655
+ }
3656
+ }
3657
+ }
3658
+ }
3659
+ }
3660
+ let TeamMember = await teamsServices.findteams( { clientId: userDetails.clientId, users: { $elemMatch: { email: userDetails.email } } } );
3661
+ if ( TeamMember&&TeamMember.length>0 ) {
3662
+ for ( let team of TeamMember ) {
3663
+ let clusterList = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
3664
+ if ( clusterList.length > 0 ) {
3665
+ for ( let cluster of clusterList ) {
3666
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3667
+ }
3668
+ }
3669
+ }
3670
+ }
3671
+ let TeamLeader = await teamsServices.findteams( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } );
3672
+ if ( TeamLeader&&TeamLeader.length>0 ) {
3673
+ for ( let team of TeamLeader ) {
3674
+ let clusterList = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
3675
+ if ( clusterList.length > 0 ) {
3676
+ for ( let cluster of clusterList ) {
3677
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3678
+ }
3679
+ }
3680
+ }
3681
+ }
3682
+ // Convert Set back to Array if needed
3683
+ stores = Array.from( storeIds );
3684
+ }
3685
+ let data = {
3686
+ id: ele._id,
3687
+ email: email?.value,
3688
+ stores,
3689
+ role: userDetails.role,
3690
+ };
3691
+ result.push( data );
3692
+ }
3693
+ }
3694
+ } ) );
3599
3695
 
3600
- return res.sendSuccess( checklistInfoList?.map( ( ele ) => ele?._id ) );
3696
+ return res.sendSuccess( result );
3601
3697
  } catch ( e ) {
3698
+ console.log( e );
3602
3699
  logger.error( { functionName: 'checklistAutoMailList', error: e } );
3603
3700
  return res.sendError( e, 500 );
3604
3701
  }
@@ -7,15 +7,15 @@
7
7
  <style>
8
8
  *{box-sizing:border-box;margin:0;padding:0}
9
9
  body{font-family:Arial,Helvetica,sans-serif;background:#fff;padding:0}
10
- .page{width:794px;min-height:1123px;background:#fff;padding:0;overflow:hidden;position:relative;page-break-after:always}
11
- .cover-wrap{position:relative;width:794px;height:1123px;background:#fff;overflow:hidden;font-family:Arial,Helvetica,sans-serif}
10
+ .page{width:794px;background:#fff;padding:0;overflow:hidden;position:relative;page-break-after:always}
11
+ .cover-wrap{position:relative;width:794px;height:284mm;background:#fff;overflow:hidden;font-family:Arial,Helvetica,sans-serif}
12
12
  /* Cover — match brand PDF (cyan title, right geometry, grey footer rule) */
13
13
  /* Right-edge artwork: flush right; height 1043px leaves ~80px for footer band on 1123px cover */
14
- .cover-deco{position:absolute;top:0;right:0;left:auto;width:493px;height:1043px;pointer-events:none;z-index:0}
14
+ .cover-deco{position:absolute;top:0;right:0;bottom:0;left:auto;width:450px;pointer-events:none;z-index:0;}
15
15
  .cover-brand{position:absolute;top:48px;left:48px;display:flex;align-items:center;gap:10px;z-index:1}
16
16
  .cover-brand-name{font-size:22px;font-weight:600;color:#1a1a1a;letter-spacing:.02em;text-transform:lowercase}
17
17
  .cover-title-block{position:absolute;top:188px;left:48px;max-width:440px;z-index:1}
18
- .cover-title-line1,.cover-title-line2{font-size:54px;font-weight:600;color:#00AEEF;line-height:1.06;letter-spacing:-.5px}
18
+ .cover-title-line1,.cover-title-line2{font-size:40px;font-weight:600;color:#00AEEF;line-height:1.15;letter-spacing:-.5px;word-wrap:break-word;overflow-wrap:break-word}
19
19
  .cover-meta-block{position:absolute;top:418px;left:48px;z-index:1}
20
20
  .cover-ref{font-size:22px;font-weight:700;color:#1a1a1a;letter-spacing:.02em}
21
21
  .cover-datetime{font-size:15px;color:#1a1a1a;font-weight:400;margin-top:10px}
@@ -28,12 +28,12 @@
28
28
  .cover-footer-page{font-size:11px;color:#999}
29
29
  .cover-footer-gen{display:flex;align-items:center;gap:8px;font-size:12px;color:#00AEEF;font-weight:600}
30
30
  /* Score page */
31
- .score-page{padding:40px}
31
+ .score-page{margin-bottom:40px;padding-bottom:20px;border-bottom:1px solid #e0e0e0}
32
32
  .score-hero{text-align:center;margin-bottom:36px}
33
33
  .score-pct{font-size:72px;font-weight:700;color:#00AEEF;line-height:1}
34
34
  .score-sub{font-size:18px;color:#444;margin-top:8px}
35
35
  .score-date{font-size:14px;color:#888;margin-top:4px}
36
- .section-title{font-size:16px;font-weight:700;color:#1a1a2e;margin-bottom:16px;padding-bottom:8px;border-bottom:2px solid #00AEEF}
36
+ .section-title{font-size:16px;font-weight:700;color:#1a1a2e;margin-bottom:30px;padding-bottom:10px;border-bottom:2px solid #00AEEF}
37
37
  .history-bars{display:flex;align-items:flex-end;gap:8px;height:160px;margin-bottom:36px}
38
38
  .bar-wrap{flex:1;display:flex;flex-direction:column;align-items:center;gap:4px}
39
39
  .bar{width:100%;background:#00AEEF;border-radius:4px 4px 0 0;display:flex;align-items:flex-start;justify-content:center}
@@ -50,9 +50,9 @@
50
50
  .pct-mid{background:#faeeda;color:#854f0b}
51
51
  .pct-lo{background:#fcebeb;color:#a32d2d}
52
52
  /* Detail pages */
53
- .detail-page{padding:32px 40px;break-inside:auto}
54
- .q-row{break-inside:avoid}
55
- .q-answer-item{break-inside:avoid}
53
+ .detail-page{padding:32px 40px}
54
+ .q-row{break-inside:auto}
55
+ .q-answer-item{break-inside:auto}
56
56
  .dp-header{display:flex;justify-content:space-between;align-items:center;margin-bottom:20px;padding-bottom:12px;border-bottom:2px solid #00AEEF}
57
57
  .dp-header h2{font-size:18px;font-weight:700;color:#1a1a2e}
58
58
  .dp-score{font-size:15px;font-weight:700;color:#000000}
@@ -79,19 +79,20 @@
79
79
  .footer-gen-by{color:#666;font-weight:400}
80
80
  .cover-footer-gen .footer-gen-by{color:#1a1a1a}
81
81
  .footer-tangoye-logo{display:block;flex-shrink:0}
82
- @page{size:A4;margin:0}
82
+ @page{size:A4;margin:5mm 0mm 15mm 0mm}
83
+ @page:first{margin:0}
83
84
  </style>
84
85
  </head>
85
86
  <body>
86
87
 
87
88
  {{!-- PAGE 1: COVER --}}
88
89
  <div class="page cover-wrap">
89
- <svg class="cover-deco" width="493" height="1043" viewBox="0 0 398 842" fill="none" xmlns="http://www.w3.org/2000/svg" preserveAspectRatio="xMaxYMin meet" aria-hidden="true">
90
- <path opacity="0.8" d="M397.199 842V586L334.199 292L222.199 842H397.199Z" fill="#99DAFF"/>
91
- <path opacity="0.7" d="M125.199 0H142.544L397.199 204.045V380L125.199 0Z" fill="#51C1FF"/>
92
- <path opacity="0.7" d="M84.7003 -0.00271425L125.197 -0.00675424L334.347 292.188L298.195 469L84.7003 -0.00271425Z" fill="#99DAFF"/>
93
- <path opacity="0.7" d="M397.199 360.707L334.199 292L397.199 586V360.707Z" fill="#6BCAFF"/>
94
- <path opacity="0.6" d="M397.199 220L125.199 0H397.199V220Z" fill="#009BF3"/>
90
+ <svg xmlns="http://www.w3.org/2000/svg" class="cover-deco" viewBox="0 0 650 2000" fill="none">
91
+ <path opacity="0.8" d="M711.5 2000V1391.92L568.063 693.586L313.062 2000H711.5Z" fill="#99DAFF" />
92
+ <path opacity="0.7" d="M92.2148 0.0078125H131.706L711.501 484.677V902.622L92.2148 0.0078125Z" fill="#51C1FF" />
93
+ <path opacity="0.7" d="M0 0.00959645L92.203 0L568.392 694.051L486.082 1114.03L0 0.00959645Z" fill="#99DAFF" />
94
+ <path opacity="0.7" d="M711.5 856.786L568.062 693.586L711.5 1391.92V856.786Z" fill="#6BCAFF" />
95
+ <path opacity="0.6" d="M711.501 522.574L92.2148 0.0078125H711.501V522.574Z" fill="#009BF3" />
95
96
  </svg>
96
97
 
97
98
  <div class="cover-brand">
@@ -115,25 +116,25 @@
115
116
  <div class="cover-sum-row"><span class="cover-sum-label">Country</span><span class="cover-sum-colon">:</span><span class="cover-sum-val">{{country}}</span></div>
116
117
  </div>
117
118
 
118
- <div class="cover-footer">
119
+ {{!-- <div class="cover-footer">
119
120
  <span class="cover-footer-page"></span>
120
121
  <div class="cover-footer-gen">
121
122
  <span class="footer-gen-by">Generated by</span>
122
123
  {{> tangoyeFooterLogo gid="cover"}}
123
124
  </div>
124
- </div>
125
+ </div> --}}
125
126
  </div>
126
127
 
127
- {{!-- PAGE 2: SCORE SUMMARY --}}
128
+ {{!-- SCORE SUMMARY + DETAIL (continuous flow) --}}
129
+ <div class="detail-page">
128
130
  {{#if hasCompliancePage}}
129
- <div class="page">
130
131
  <div class="score-page">
131
132
  <div class="score-hero">
132
133
  <div class="score-pct">{{totalPercentage}}%</div>
133
134
  <div class="score-sub">Total: <strong>{{totalScore}}</strong> out of <strong>{{maxScore}}</strong></div>
134
135
  <div class="score-date">{{reportDate}}</div>
135
136
  </div>
136
-
137
+
137
138
  {{#if historyData}}
138
139
  <div class="section-title">History — Last 7 Days</div>
139
140
  <div style="display:flex;flex-direction:column;margin-bottom:36px">
@@ -148,7 +149,7 @@
148
149
  <div class="bar-axis"></div>
149
150
  </div>
150
151
  {{/if}}
151
-
152
+
152
153
  <div class="section-title">Section Wise Insights</div>
153
154
  <table class="sw-table">
154
155
  <thead><tr><th>Sections</th><th>Target Score</th><th>Actual Scrore</th><th>%</th></tr></thead>
@@ -164,18 +165,8 @@
164
165
  </tbody>
165
166
  </table>
166
167
  </div>
167
- {{!-- <div class="page-footer">
168
- <span>Page 2 of {{totalPages}}</span>
169
- <span class="footer-brand"><span class="footer-gen-by">Generated by</span>{{> tangoyeFooterLogo gid="p2"}}</span>
170
- </div>
171
- </div> --}}
172
168
  {{/if}}
173
-
174
- {{!-- PAGES 3+: DETAIL - Question sections --}}
175
- {{#each detailPageGroups}}
176
- <div class="page">
177
- <div class="detail-page">
178
- {{#each this.sections}}
169
+ {{#each sections}}
179
170
  <div class="dp-header" {{#unless @first}}style="margin-top:20px"{{/unless}}><h2>{{this.sectionName}}</h2>{{#if this.maxScore}}<span class="dp-score">{{this.currentScore}}/{{this.maxScore}}</span>{{/if}}</div>
180
171
  <div class="sec-questions">
181
172
  {{#each this.questions}}
@@ -247,22 +238,7 @@
247
238
  {{/each}}
248
239
  </div>
249
240
  {{/each}}
250
-
251
- {{!-- {{#if this.isLastGroup}}
252
- {{#each ../flags}}
253
- <div style="margin-top:12px;padding:12px;background:#faeeda;border-radius:8px;border-left:4px solid #ef9f27">
254
- <strong style="font-size:13px;color:#854f0b">⚠ Flag: {{this.sectionName}} — {{this.qname}}</strong>
255
- <p style="font-size:12px;color:#854f0b;margin-top:4px">Q{{this.qno}} ({{this.sectionName}}): "{{this.qname}}" — Answered: <strong>{{this.answer}}</strong>. Action required.</p>
256
- </div>
257
- {{/each}}
258
- {{/if}} --}}
259
- </div>
260
- {{!-- <div class="page-footer">
261
- <span>Page {{this.pageNumber}} of {{../totalPages}}</span>
262
- <span class="footer-brand"><span class="footer-gen-by">Generated by</span>{{> tangoyeFooterLogo gid=(strConcat 'd' @index)}}</span>
263
- </div> --}}
264
241
  </div>
265
- {{/each}}
266
242
 
267
243
  </body>
268
244
  </html>
@@ -38,7 +38,7 @@ internalTraxRouter
38
38
  .post( '/posblock', isAllowedInternalAPIHandler, internalController.getStoreTaskDetails )
39
39
  .post( '/runAIFlag', isAllowedInternalAPIHandler, internalController.runAIFlag )
40
40
  .post( '/downloadInsertPdf', isAllowedInternalAPIHandler, internalController.downloadInsertPdf )
41
- .post( '/checklistAutoMailList', isAllowedInternalAPIHandler, internalController.checklistAutoMailList )
41
+ .get( '/checklistAutoMailList', isAllowedInternalAPIHandler, internalController.checklistAutoMailList )
42
42
  ;
43
43
 
44
44
 
@@ -0,0 +1,72 @@
1
+ import puppeteer from 'puppeteer';
2
+
3
+
4
+ const LAUNCH_ARGS = {
5
+
6
+ headless: 'new',
7
+
8
+ args: [ '--no-sandbox', '--disable-dev-shm-usage', '--disable-gpu' ],
9
+
10
+ };
11
+
12
+
13
+ let browserInstance = null;
14
+
15
+ let launchPromise = null;
16
+
17
+
18
+ async function launchBrowser() {
19
+ const browser = await puppeteer.launch( LAUNCH_ARGS );
20
+
21
+ browser.on( 'disconnected', () => {
22
+ if ( browserInstance === browser ) {
23
+ browserInstance = null;
24
+
25
+ launchPromise = null;
26
+ }
27
+ } );
28
+
29
+ return browser;
30
+ }
31
+
32
+
33
+ /**
34
+
35
+ * Returns a shared browser instance, launching one if needed.
36
+
37
+ * Callers must close their pages, NOT the browser.
38
+
39
+ */
40
+
41
+ export async function getBrowser() {
42
+ if ( browserInstance ) {
43
+ try {
44
+ // verify browser is still alive
45
+
46
+ await browserInstance.version();
47
+
48
+ return browserInstance;
49
+ } catch {
50
+ browserInstance = null;
51
+
52
+ launchPromise = null;
53
+ }
54
+ }
55
+
56
+
57
+ if ( !launchPromise ) {
58
+ launchPromise = launchBrowser().then( ( b ) => {
59
+ browserInstance = b;
60
+
61
+ return b;
62
+ } ).catch( ( err ) => {
63
+ launchPromise = null;
64
+
65
+ throw err;
66
+ } );
67
+ }
68
+
69
+
70
+ return launchPromise;
71
+ }
72
+
@@ -214,7 +214,19 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
214
214
  const max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
215
215
 
216
216
 
217
- const score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
217
+ let score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
218
+
219
+
220
+ if ( q.answerType == 'image' && q.compliance && q.userAnswer?.[0]?.runAIData ) {
221
+ let find = q.userAnswer?.[0]?.runAIData?.find( ( run ) => run?.featureName == 'Matched/Not Matched' );
222
+ if ( find ) {
223
+ if ( find?.value == 'True' ) {
224
+ score = q?.answers?.[0]?.matchedCount;
225
+ } else {
226
+ score = q?.answers?.[0]?.notMatchedCount;
227
+ }
228
+ }
229
+ }
218
230
 
219
231
 
220
232
  sectionScore += score;
@@ -491,6 +503,8 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
491
503
 
492
504
  detailPageGroups,
493
505
 
506
+ sections: detailPageGroups.flatMap( ( g ) => g.sections ),
507
+
494
508
  flags,
495
509
 
496
510
  };
@@ -629,6 +643,8 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
629
643
 
630
644
  detailPageGroups,
631
645
 
646
+ sections: detailPageGroups.flatMap( ( g ) => g.sections ),
647
+
632
648
  flags,
633
649
 
634
650
  complianceCount: hasCompliancePage,
@@ -753,6 +769,8 @@ export function createImageCache() {
753
769
  group.sections?.forEach( collectFromSection );
754
770
  } );
755
771
 
772
+ resolvedData.sections?.forEach( collectFromSection );
773
+
756
774
 
757
775
  // Fetch all unique URLs in parallel (max 20 concurrent)
758
776
 
@@ -798,6 +816,8 @@ export function createImageCache() {
798
816
  group.sections?.forEach( replaceInSection );
799
817
  } );
800
818
 
819
+ resolvedData.sections?.forEach( replaceInSection );
820
+
801
821
 
802
822
  return resolvedData;
803
823
  }
@@ -862,6 +882,8 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
862
882
  group.sections?.forEach( resolveQuestionMedia );
863
883
  } );
864
884
 
885
+ resolvedData.sections?.forEach( resolveQuestionMedia );
886
+
865
887
 
866
888
  if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
867
889
  resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
@@ -977,6 +999,7 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
977
999
  resolvedData.detailPageGroups?.forEach( ( group ) => {
978
1000
  group.sections?.forEach( resolveQuestionMedia );
979
1001
  } );
1002
+ resolvedData.sections?.forEach( resolveQuestionMedia );
980
1003
 
981
1004
  if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
982
1005
  resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );