tango-app-api-trax 3.7.91 → 3.7.93

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.91",
3
+ "version": "3.7.93",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -18,7 +18,7 @@ import timeZone from 'dayjs/plugin/timezone.js';
18
18
  import utc from 'dayjs/plugin/utc.js';
19
19
  import { logger } from 'tango-app-api-middleware';
20
20
  import mongoose from 'mongoose';
21
- import { sendPushNotification, sendAiPushNotification, sendEmailWithSES, signedUrl, fileUpload } from 'tango-app-api-middleware';
21
+ import { sendPushNotification, sendAiPushNotification, sendEmailWithSES, signedUrl, fileUpload, getOpenSearchData } from 'tango-app-api-middleware';
22
22
  // import * as planoService from '../services/planogram.service.js';
23
23
  import * as clusterServices from '../services/cluster.service.js';
24
24
  import * as teamsServices from '../services/teams.service.js';
@@ -30,6 +30,7 @@ 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';
35
36
 
@@ -2711,9 +2712,9 @@ export async function getRunAIList( req, res ) {
2711
2712
  if ( qn.answers.some( ( ele ) => ele.runAI ) ) {
2712
2713
  let checkList = runAIList.findIndex( ( run ) => run.checklist.toString() == check._id.toString() );
2713
2714
  if ( checkList == -1 ) {
2714
- runAIList.push( { checklist: check._id, questions: [ qn.uniqueqid ] } );
2715
+ runAIList.push( { checklist: check._id, checklistName: check.checkListName, questions: [ { name: qn.qname, qid: qn.uniqueqid } ] } );
2715
2716
  } else {
2716
- runAIList[checkList].questions.push( qn.uniqueqid );
2717
+ runAIList[checkList].questions.push( { name: qn.qname, qid: qn.uniqueqid } );
2717
2718
  }
2718
2719
  }
2719
2720
  } );
@@ -3454,9 +3455,232 @@ async function getBrandInfo( clientId ) {
3454
3455
  }
3455
3456
 
3456
3457
  export const downloadInsertPdf = async ( req, res ) => {
3458
+ let page = null;
3457
3459
  try {
3458
- res.sendSuccess( 'PDF generation started' );
3460
+ if ( !req.body.checklistId ) {
3461
+ return res.sendError( 'Checklist Id is required', 400 );
3462
+ }
3463
+ const cdnBase = JSON.parse( process.env.CDNURL )?.TraxAnswerCDN || '';
3464
+ const pdfTemplate = getCompiledVisitChecklistTemplate();
3465
+ const imageCache = createImageCache();
3466
+
3467
+ const safeName = ( str ) =>
3468
+ ( str || '' ).toString().replace( /[<>:"/\\|?*]+/g, '_' );
3469
+
3470
+ const query = {
3471
+ query: {
3472
+ bool: {
3473
+ must: [
3474
+ {
3475
+ term: {
3476
+ _id: req.body.checklistId,
3477
+ },
3478
+ },
3479
+ ],
3480
+ },
3481
+ },
3482
+ };
3483
+
3484
+ // 1) Launch browser page + fetch OpenSearch data in parallel
3485
+ const [ browser, aiDetails ] = await Promise.all( [
3486
+ getBrowserInstance(),
3487
+ getOpenSearchData( JSON.parse( process.env.OPENSEARCH ).traxIndex, query ),
3488
+ ] );
3489
+
3490
+ if ( aiDetails?.statusCode != 200 || !aiDetails?.body?.hits?.hits.length ) {
3491
+ return res.sendError( 'Checklist not found', 404 );
3492
+ }
3459
3493
 
3494
+ const doc = { ...aiDetails.body.hits.hits[0]._source };
3495
+
3496
+ // 2) Fetch brandInfo + compliance data in parallel
3497
+ const complianceURL = JSON.parse( process.env.LAMBDAURL ).complianceHistory;
3498
+ const detectionPayload = {
3499
+ 'storeId': [ doc.store_id ],
3500
+ 'userEmail': doc.userEmail,
3501
+ 'sourceChecklist_id': doc.sourceCheckList_id,
3502
+ };
3503
+
3504
+ const [ brandInfo, complianceData ] = await Promise.all( [
3505
+ getBrandInfo( doc.client_id ),
3506
+ LamdaServiceCall( complianceURL, detectionPayload ),
3507
+ ] );
3508
+
3509
+ if ( complianceData?.data?.length ) {
3510
+ doc['historyData'] = complianceData.data;
3511
+ }
3512
+
3513
+ // CDN fix
3514
+ ( doc.questionAnswers || [] ).forEach( ( section ) => {
3515
+ ( section.questions || [] ).forEach( ( question ) => {
3516
+ ( question.userAnswer || [] ).forEach( ( answer ) => {
3517
+ if ( answer?.referenceImage?.trim() ) {
3518
+ answer.referenceImage = cdnBase + answer.referenceImage;
3519
+ }
3520
+ } );
3521
+ } );
3522
+ } );
3523
+
3524
+ const templateData = buildVisitChecklistTemplateData( doc, brandInfo );
3525
+ const resolvedData = resolveTemplateUrls( templateData, cdnBase );
3526
+ await imageCache.resolveAllImages( resolvedData );
3527
+
3528
+ const html = pdfTemplate( resolvedData );
3529
+
3530
+ // 3) Create page and render — images are already base64 so use domcontentloaded
3531
+ page = await browser.newPage();
3532
+ await page.setViewport( { width: 1280, height: 1800 } );
3533
+
3534
+ try {
3535
+ await page.setContent( html, {
3536
+ waitUntil: 'domcontentloaded',
3537
+ timeout: 15000,
3538
+ } );
3539
+ await page.emulateMediaType( 'screen' );
3540
+ } catch ( err ) {
3541
+ logger.error( { functionName: 'downloadInsertPdf', message: 'setContent failed', error: err } );
3542
+ }
3543
+
3544
+ let pdfBuffer;
3545
+ try {
3546
+ pdfBuffer = await page.pdf( {
3547
+ format: 'A4',
3548
+ printBackground: true,
3549
+ margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
3550
+ } );
3551
+ } catch ( err ) {
3552
+ logger.error( { functionName: 'downloadInsertPdf', message: 'PDF generation failed', error: err } );
3553
+ return res.sendError( 'PDF generation failed', 500 );
3554
+ }
3555
+
3556
+ if ( !pdfBuffer || pdfBuffer.length === 0 ) {
3557
+ logger.error( { functionName: 'downloadInsertPdf', message: 'Empty PDF', docId: doc._id } );
3558
+ return res.sendError( 'PDF generation resulted in empty buffer', 500 );
3559
+ }
3560
+
3561
+ const finalBuffer = Buffer.isBuffer( pdfBuffer ) ?
3562
+ pdfBuffer :
3563
+ Buffer.from( pdfBuffer );
3564
+
3565
+ const pdfName = `${safeName(
3566
+ doc.store_id + '_' + ( doc.storeName || 'store' ),
3567
+ )}.pdf`;
3568
+
3569
+ res.set( {
3570
+ 'Content-Type': 'application/pdf',
3571
+ 'Content-Disposition': `attachment; filename="${pdfName}"`,
3572
+ 'Content-Length': finalBuffer.length,
3573
+ } );
3574
+
3575
+ return res.send( finalBuffer );
3576
+ } catch ( e ) {
3577
+ logger.error( { functionName: 'downloadInsertPdf', error: e } );
3578
+ return res.sendError( e, 500 );
3579
+ } finally {
3580
+ if ( page ) {
3581
+ await page.close().catch( () => {} );
3582
+ }
3583
+ }
3584
+ };
3585
+
3586
+ export async function checklistAutoMailList( req, res ) {
3587
+ try {
3588
+ const checklistInfoList = await CLconfig.find( {
3589
+ 'autoEmail.type': { $exists: true },
3590
+ '$expr': {
3591
+ $gt: [ { $size: { $ifNull: [ '$autoEmail.type', [] ] } }, 0 ],
3592
+ },
3593
+ }, { _id: 1, autoEmail: 1 } );
3594
+
3595
+ let result = [];
3596
+
3597
+ await Promise.all( checklistInfoList.map( async ( ele ) => {
3598
+ for ( let email of ele?.autoEmail?.users ) {
3599
+ let stores = [];
3600
+ let userDetails = await userService.findOne( { email: email.value } );
3601
+ if ( userDetails ) {
3602
+ if ( userDetails.userType === 'client' && userDetails.role !== 'superadmin' ) {
3603
+ let storeIds = new Set( userDetails.assignedStores.map( ( store ) => store.storeId ) );
3604
+
3605
+ // Fetch clusters and teams in parallel
3606
+ const [ clustersList, teamsList ] = await Promise.all( [
3607
+ clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
3608
+ teamsServices.findteams( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } ),
3609
+ ] );
3610
+
3611
+ // Process clusters
3612
+ if ( clustersList.length > 0 ) {
3613
+ for ( let cluster of clustersList ) {
3614
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3615
+ }
3616
+ }
3617
+
3618
+ // Process teams
3619
+ if ( teamsList.length > 0 ) {
3620
+ for ( let team of teamsList ) {
3621
+ for ( let user of team.users ) {
3622
+ let findUser = await userService.findOne( { _id: user.userId } );
3623
+ if ( findUser && findUser.assignedStores?.length > 0 ) {
3624
+ findUser.assignedStores.forEach( ( store ) => storeIds.add( store.storeId ) );
3625
+ }
3626
+
3627
+ // Fetch clusters for the user
3628
+ let userClustersList = await clusterServices.findcluster( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: findUser.email } } } );
3629
+ if ( userClustersList.length > 0 ) {
3630
+ for ( let cluster of userClustersList ) {
3631
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3632
+ }
3633
+ }
3634
+ }
3635
+ }
3636
+ }
3637
+ let TeamMember = await teamsServices.findteams( { clientId: userDetails.clientId, users: { $elemMatch: { email: userDetails.email } } } );
3638
+ if ( TeamMember&&TeamMember.length>0 ) {
3639
+ for ( let team of TeamMember ) {
3640
+ let clusterList = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
3641
+ if ( clusterList.length > 0 ) {
3642
+ for ( let cluster of clusterList ) {
3643
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3644
+ }
3645
+ }
3646
+ }
3647
+ }
3648
+ let TeamLeader = await teamsServices.findteams( { clientId: userDetails.clientId, Teamlead: { $elemMatch: { email: userDetails.email } } } );
3649
+ if ( TeamLeader&&TeamLeader.length>0 ) {
3650
+ for ( let team of TeamLeader ) {
3651
+ let clusterList = await clusterServices.findcluster( { clientId: userDetails.clientId, teams: { $elemMatch: { name: team.teamName } } } );
3652
+ if ( clusterList.length > 0 ) {
3653
+ for ( let cluster of clusterList ) {
3654
+ cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
3655
+ }
3656
+ }
3657
+ }
3658
+ }
3659
+ // Convert Set back to Array if needed
3660
+ stores = Array.from( storeIds );
3661
+ }
3662
+ let data = {
3663
+ id: ele._id,
3664
+ email: email?.value,
3665
+ stores,
3666
+ role: userDetails.role,
3667
+ };
3668
+ result.push( data );
3669
+ }
3670
+ }
3671
+ } ) );
3672
+
3673
+ return res.sendSuccess( result );
3674
+ } catch ( e ) {
3675
+ console.log( e );
3676
+ logger.error( { functionName: 'checklistAutoMailList', error: e } );
3677
+ return res.sendError( e, 500 );
3678
+ }
3679
+ }
3680
+
3681
+
3682
+ export const downloadInsertPdfOld = async ( req, res ) => {
3683
+ try {
3460
3684
  setImmediate( async () => {
3461
3685
  try {
3462
3686
  const cdnBase = JSON.parse( process.env.CDNURL )?.TraxAnswerCDN || '';
@@ -3596,11 +3820,10 @@ export const downloadInsertPdf = async ( req, res ) => {
3596
3820
 
3597
3821
  try {
3598
3822
  await page.setContent( html, {
3599
- waitUntil: 'domcontentloaded',
3600
- timeout: 0,
3823
+ waitUntil: 'networkidle0',
3824
+ timeout: 30000,
3601
3825
  } );
3602
3826
 
3603
- await new Promise( ( r ) => setTimeout( r, 300 ) );
3604
3827
  await page.emulateMediaType( 'screen' );
3605
3828
  } catch ( err ) {
3606
3829
  logger.error( { functionName: 'setContent failed:', error: err } );
@@ -38,6 +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
+ .get( '/checklistAutoMailList', isAllowedInternalAPIHandler, internalController.checklistAutoMailList )
41
42
  ;
42
43
 
43
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
+