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
|
@@ -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
|
-
|
|
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: '
|
|
3600
|
-
timeout:
|
|
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
|
+
|