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
|
@@ -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
|
|
3466
|
-
|
|
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
|
-
|
|
3482
|
-
|
|
3483
|
-
|
|
3484
|
-
|
|
3485
|
-
|
|
3486
|
-
|
|
3487
|
-
|
|
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
|
-
|
|
3496
|
-
|
|
3497
|
-
|
|
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
|
-
|
|
3497
|
+
if ( aiDetails?.statusCode != 200 || !aiDetails?.body?.hits?.hits.length ) {
|
|
3498
|
+
return res.sendError( 'Checklist not found', 404 );
|
|
3499
|
+
}
|
|
3501
3500
|
|
|
3502
|
-
|
|
3501
|
+
const doc = { ...aiDetails.body.hits.hits[0]._source };
|
|
3503
3502
|
|
|
3504
|
-
|
|
3505
|
-
|
|
3506
|
-
|
|
3507
|
-
|
|
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
|
-
|
|
3511
|
-
|
|
3512
|
-
|
|
3513
|
-
|
|
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
|
-
|
|
3528
|
-
|
|
3529
|
-
|
|
3530
|
-
);
|
|
3516
|
+
if ( complianceData?.data?.length ) {
|
|
3517
|
+
doc['historyData'] = complianceData.data;
|
|
3518
|
+
}
|
|
3531
3519
|
|
|
3532
|
-
|
|
3533
|
-
|
|
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
|
-
|
|
3531
|
+
const templateData = buildVisitChecklistTemplateData( doc, brandInfo );
|
|
3532
|
+
const resolvedData = resolveTemplateUrls( templateData, cdnBase );
|
|
3533
|
+
await imageCache.resolveAllImages( resolvedData );
|
|
3536
3534
|
|
|
3537
|
-
|
|
3538
|
-
await page.setContent( html, {
|
|
3539
|
-
waitUntil: 'networkidle0',
|
|
3540
|
-
timeout: 30000,
|
|
3541
|
-
} );
|
|
3535
|
+
const html = pdfTemplate( resolvedData );
|
|
3542
3536
|
|
|
3543
|
-
|
|
3544
|
-
|
|
3545
|
-
|
|
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
|
-
|
|
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
|
-
|
|
3551
|
-
|
|
3552
|
-
|
|
3553
|
-
|
|
3554
|
-
|
|
3555
|
-
|
|
3556
|
-
|
|
3557
|
-
|
|
3558
|
-
|
|
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
|
-
|
|
3562
|
-
|
|
3563
|
-
|
|
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
|
-
|
|
3567
|
-
|
|
3568
|
-
|
|
3584
|
+
const finalBuffer = Buffer.isBuffer( pdfBuffer ) ?
|
|
3585
|
+
pdfBuffer :
|
|
3586
|
+
Buffer.from( pdfBuffer );
|
|
3569
3587
|
|
|
3570
|
-
|
|
3571
|
-
|
|
3572
|
-
|
|
3588
|
+
const pdfName = `${safeName(
|
|
3589
|
+
doc.store_id + '_' + ( doc.storeName || 'store' ),
|
|
3590
|
+
)}.pdf`;
|
|
3573
3591
|
|
|
3574
|
-
|
|
3575
|
-
|
|
3576
|
-
|
|
3577
|
-
|
|
3578
|
-
|
|
3592
|
+
res.set( {
|
|
3593
|
+
'Content-Type': 'application/pdf',
|
|
3594
|
+
'Content-Disposition': `attachment; filename="${pdfName}"`,
|
|
3595
|
+
'Content-Length': finalBuffer.length,
|
|
3596
|
+
} );
|
|
3579
3597
|
|
|
3580
|
-
|
|
3581
|
-
} finally {
|
|
3582
|
-
await browser.close();
|
|
3583
|
-
}
|
|
3598
|
+
return res.send( finalBuffer );
|
|
3584
3599
|
} catch ( e ) {
|
|
3585
|
-
|
|
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(
|
|
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;
|
|
11
|
-
.cover-wrap{position:relative;width:794px;height:
|
|
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:
|
|
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:
|
|
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:
|
|
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:
|
|
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
|
|
54
|
-
.q-row{break-inside:
|
|
55
|
-
.q-answer-item{break-inside:
|
|
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:
|
|
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
|
|
90
|
-
<path opacity="0.8" d="
|
|
91
|
-
<path opacity="0.7" d="
|
|
92
|
-
<path opacity="0.7" d="
|
|
93
|
-
<path opacity="0.7" d="
|
|
94
|
-
<path opacity="0.6" d="
|
|
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
|
-
{{!--
|
|
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
|
-
.
|
|
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
|
-
|
|
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 );
|