tango-app-api-trax 3.7.87 → 3.7.89

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.
@@ -38,8 +38,8 @@ function toPlainObject( doc ) {
38
38
  * @param {string} [checklistName] Full checklist name from API or DB
39
39
  * @return {{ titleLine1: string, titleLine2: string }} titleLine1 and titleLine2 for the PDF cover
40
40
  */
41
- function splitCoverTitle( checklistName ) {
42
- const n = ( checklistName || '' ).trim();
41
+ function splitCoverTitle( checklistName ) {
42
+ const n = ( checklistName || '' ).trim();
43
43
 
44
44
  if ( !n ) {
45
45
  return { titleLine1: 'AOM', titleLine2: 'Visit Checklist' };
@@ -50,122 +50,122 @@ function splitCoverTitle( checklistName ) {
50
50
  if ( m ) {
51
51
  return { titleLine1: m[1].trim() || 'AOM', titleLine2: 'Visit Checklist' };
52
52
  }
53
-
54
- return { titleLine1: n, titleLine2: 'Visit Checklist' };
55
- }
56
-
57
- function getMatchedAnswerForUserAnswer( question, userAnswer ) {
58
- if ( !userAnswer || !Array.isArray( question?.answers ) ) return null;
59
-
60
- return question.answers.find( ( answer ) => {
61
- if ( userAnswer.no !== undefined && answer.index === userAnswer.no ) {
62
- return true;
63
- }
64
-
65
- if ( userAnswer.index !== undefined && answer.index === userAnswer.index ) {
66
- return true;
67
- }
68
-
69
- if ( userAnswer.answeroptionNumber !== undefined && answer.answeroptionNumber === userAnswer.answeroptionNumber ) {
70
- return true;
71
- }
72
-
73
- return answer.answer === userAnswer.answer;
74
- } ) || null;
75
- }
76
-
77
- function isMeaningfulUserAnswer( userAnswer = {} ) {
78
- return Boolean(
79
- userAnswer &&
80
- (
81
- ( typeof userAnswer.answer === 'string' && userAnswer.answer.trim() ) ||
82
- ( typeof userAnswer.validationAnswer === 'string' && userAnswer.validationAnswer.trim() ) ||
83
- ( typeof userAnswer.referenceImage === 'string' && userAnswer.referenceImage.trim() )
84
- ),
85
- );
86
- }
87
-
88
- function getSourceUserAnswers( question ) {
89
- const fromUserAnswer = Array.isArray( question?.userAnswer ) ? question.userAnswer : [];
90
- const fromMultiAnswer = Array.isArray( question?.Multianswer ) ? question.Multianswer : [];
91
- const isMultiAnswerQuestion = question?.answerType === 'multiplechoicemultiple' ||
92
- question?.answerType === 'multipleImage' ||
93
- question?.answerType === 'image/video' ||
94
- ( question?.answerType === 'dropdown' && question?.allowMultiple );
95
-
96
- if ( isMultiAnswerQuestion ) {
97
- return [ ...fromUserAnswer, ...fromMultiAnswer ];
98
- }
99
-
100
- return fromUserAnswer;
101
- }
102
-
103
- function getUserAnswerKey( userAnswer = {}, index = 0 ) {
104
- return [
105
- userAnswer.no ?? '',
106
- userAnswer.index ?? '',
107
- userAnswer.answeroptionNumber ?? '',
108
- userAnswer.answer ?? '',
109
- userAnswer.validationAnswer ?? '',
110
- userAnswer.referenceImage ?? '',
111
- index,
112
- ].join( '|' );
113
- }
114
-
115
- function getMediaDisplayType( questionAnswerType, userAnswer = {} ) {
116
- if ( questionAnswerType === 'video' ) {
117
- return userAnswer.answerType === 'image' ? 'image' : 'video';
118
- }
119
- if ( [ 'image', 'descriptiveImage', 'multipleImage' ].includes( questionAnswerType ) ) {
120
- return userAnswer.answerType === 'video' ? 'video' : 'image';
121
- }
122
- if ( questionAnswerType === 'image/video' ) {
123
- return userAnswer.answerType === 'video' ? 'video' : 'image';
124
- }
125
-
126
- return 'text';
127
- }
128
-
129
- function getValidationDisplayType( validationType ) {
130
- if ( validationType === 'Capture Image' ) return 'image';
131
- if ( validationType === 'Capture Video' ) return 'video';
132
- return 'text';
133
- }
134
-
135
- function buildQuestionAnswerEntries( question ) {
136
- const rawUserAnswers = getSourceUserAnswers( question );
137
- const uniqueUserAnswers = [];
138
- const seenUserAnswers = new Set();
139
-
140
- rawUserAnswers.forEach( ( userAnswer, index ) => {
141
- if ( !isMeaningfulUserAnswer( userAnswer ) ) return;
142
-
143
- const key = getUserAnswerKey( userAnswer, index );
144
- if ( seenUserAnswers.has( key ) ) return;
145
-
146
- seenUserAnswers.add( key );
147
- uniqueUserAnswers.push( userAnswer );
148
- } );
149
-
150
- return uniqueUserAnswers.map( ( userAnswer ) => {
151
- const matchedAnswer = getMatchedAnswerForUserAnswer( question, userAnswer );
152
- const validation = matchedAnswer?.validation ?? userAnswer?.validation ?? false;
153
- const validationType = matchedAnswer?.validationType || userAnswer?.validationType || '';
154
- const validationAnswer = matchedAnswer?.validationAnswer || userAnswer?.validationAnswer || '';
155
-
156
- return {
157
- answer: userAnswer?.answer || '',
158
- answerType: getMediaDisplayType( question?.answerType, userAnswer ),
159
- referenceImage: userAnswer?.referenceImage || matchedAnswer?.referenceImage || '',
160
- remarks: userAnswer?.remarks || '',
161
- sopFlag: userAnswer?.sopFlag ?? matchedAnswer?.sopFlag ?? false,
162
- validation,
163
- validationType,
164
- validationAnswer,
165
- validationDisplayType: getValidationDisplayType( validationType ),
166
- };
167
- } );
168
- }
53
+
54
+ return { titleLine1: n, titleLine2: 'Visit Checklist' };
55
+ }
56
+
57
+ function getMatchedAnswerForUserAnswer( question, userAnswer ) {
58
+ if ( !userAnswer || !Array.isArray( question?.answers ) ) return null;
59
+
60
+ return question.answers.find( ( answer ) => {
61
+ if ( userAnswer.no !== undefined && answer.index === userAnswer.no ) {
62
+ return true;
63
+ }
64
+
65
+ if ( userAnswer.index !== undefined && answer.index === userAnswer.index ) {
66
+ return true;
67
+ }
68
+
69
+ if ( userAnswer.answeroptionNumber !== undefined && answer.answeroptionNumber === userAnswer.answeroptionNumber ) {
70
+ return true;
71
+ }
72
+
73
+ return answer.answer === userAnswer.answer;
74
+ } ) || null;
75
+ }
76
+
77
+ function isMeaningfulUserAnswer( userAnswer = {} ) {
78
+ return Boolean(
79
+ userAnswer &&
80
+ (
81
+ ( typeof userAnswer.answer === 'string' && userAnswer.answer.trim() ) ||
82
+ ( typeof userAnswer.validationAnswer === 'string' && userAnswer.validationAnswer.trim() ) ||
83
+ ( typeof userAnswer.referenceImage === 'string' && userAnswer.referenceImage.trim() )
84
+ ),
85
+ );
86
+ }
87
+
88
+ function getSourceUserAnswers( question ) {
89
+ const fromUserAnswer = Array.isArray( question?.userAnswer ) ? question.userAnswer : [];
90
+ const fromMultiAnswer = Array.isArray( question?.Multianswer ) ? question.Multianswer : [];
91
+ const isMultiAnswerQuestion = question?.answerType === 'multiplechoicemultiple' ||
92
+ question?.answerType === 'multipleImage' ||
93
+ question?.answerType === 'image/video' ||
94
+ ( question?.answerType === 'dropdown' && question?.allowMultiple );
95
+
96
+ if ( isMultiAnswerQuestion ) {
97
+ return [ ...fromUserAnswer, ...fromMultiAnswer ];
98
+ }
99
+
100
+ return fromUserAnswer;
101
+ }
102
+
103
+ function getUserAnswerKey( userAnswer = {}, index = 0 ) {
104
+ return [
105
+ userAnswer.no ?? '',
106
+ userAnswer.index ?? '',
107
+ userAnswer.answeroptionNumber ?? '',
108
+ userAnswer.answer ?? '',
109
+ userAnswer.validationAnswer ?? '',
110
+ userAnswer.referenceImage ?? '',
111
+ index,
112
+ ].join( '|' );
113
+ }
114
+
115
+ function getMediaDisplayType( questionAnswerType, userAnswer = {} ) {
116
+ if ( questionAnswerType === 'video' ) {
117
+ return userAnswer.answerType === 'image' ? 'image' : 'video';
118
+ }
119
+ if ( [ 'image', 'descriptiveImage', 'multipleImage' ].includes( questionAnswerType ) ) {
120
+ return userAnswer.answerType === 'video' ? 'video' : 'image';
121
+ }
122
+ if ( questionAnswerType === 'image/video' ) {
123
+ return userAnswer.answerType === 'video' ? 'video' : 'image';
124
+ }
125
+
126
+ return 'text';
127
+ }
128
+
129
+ function getValidationDisplayType( validationType ) {
130
+ if ( validationType === 'Capture Image' ) return 'image';
131
+ if ( validationType === 'Capture Video' ) return 'video';
132
+ return 'text';
133
+ }
134
+
135
+ function buildQuestionAnswerEntries( question ) {
136
+ const rawUserAnswers = getSourceUserAnswers( question );
137
+ const uniqueUserAnswers = [];
138
+ const seenUserAnswers = new Set();
139
+
140
+ rawUserAnswers.forEach( ( userAnswer, index ) => {
141
+ if ( !isMeaningfulUserAnswer( userAnswer ) ) return;
142
+
143
+ const key = getUserAnswerKey( userAnswer, index );
144
+ if ( seenUserAnswers.has( key ) ) return;
145
+
146
+ seenUserAnswers.add( key );
147
+ uniqueUserAnswers.push( userAnswer );
148
+ } );
149
+
150
+ return uniqueUserAnswers.map( ( userAnswer ) => {
151
+ const matchedAnswer = getMatchedAnswerForUserAnswer( question, userAnswer );
152
+ const validation = matchedAnswer?.validation ?? userAnswer?.validation ?? false;
153
+ const validationType = matchedAnswer?.validationType || userAnswer?.validationType || '';
154
+ const validationAnswer = matchedAnswer?.validationAnswer || userAnswer?.validationAnswer || '';
155
+
156
+ return {
157
+ answer: userAnswer?.answer || '',
158
+ answerType: getMediaDisplayType( question?.answerType, userAnswer ),
159
+ referenceImage: userAnswer?.referenceImage || matchedAnswer?.referenceImage || '',
160
+ remarks: userAnswer?.remarks || '',
161
+ sopFlag: userAnswer?.sopFlag ?? matchedAnswer?.sopFlag ?? false,
162
+ validation,
163
+ validationType,
164
+ validationAnswer,
165
+ validationDisplayType: getValidationDisplayType( validationType ),
166
+ };
167
+ } );
168
+ }
169
169
 
170
170
 
171
171
  /**
@@ -193,28 +193,29 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
193
193
  ( questionAnswer || [] ).forEach( ( section, sectionIdx ) => {
194
194
  let sectionScore = 0;
195
195
 
196
- let sectionMax = 0;
197
-
198
- let passedCount = 0;
199
-
200
- const questions = [];
201
- const sourceQuestionsCount = section.questions?.length || 0;
202
-
203
-
204
- ( section.questions || [] ).forEach( ( q ) => {
205
- const userAnswersWithRef = buildQuestionAnswerEntries( q );
206
-
207
- if ( !userAnswersWithRef.length ) {
208
- return;
209
- }
210
-
211
- const ua = userAnswersWithRef[0];
212
-
213
- const matchedAnswer = ua && getMatchedAnswerForUserAnswer( q, ua );
214
-
215
- const score = matchedAnswer?.complianceScore ?? q.complianceScore ?? q.answers?.[0]?.complianceScore ?? 0;
216
-
217
- const max = 10;
196
+ let sectionMax = 0;
197
+
198
+ let passedCount = 0;
199
+
200
+ const questions = [];
201
+ const sourceQuestionsCount = section.questions?.length || 0;
202
+
203
+
204
+ ( section.questions || [] ).forEach( ( q ) => {
205
+ const userAnswersWithRef = buildQuestionAnswerEntries( q );
206
+
207
+ if ( !userAnswersWithRef.length ) {
208
+ return;
209
+ }
210
+
211
+ const ua = userAnswersWithRef[0];
212
+
213
+
214
+ const max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
215
+
216
+
217
+ const score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
218
+
218
219
 
219
220
  sectionScore += score;
220
221
 
@@ -227,7 +228,7 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
227
228
  if ( score >= 10 ) passedCount++;
228
229
 
229
230
 
230
- const answerText = ( ua?.answer || '' ).toString().trim();
231
+ const answerText = ( ua?.answer || '' ).toString().trim();
231
232
 
232
233
  const isYes = answerText.toLowerCase() === 'yes';
233
234
 
@@ -249,43 +250,43 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
249
250
  } );
250
251
  }
251
252
 
252
- questions.push( {
253
+ questions.push( {
253
254
 
254
255
  qno: q.qno || q.uniqueqno,
255
256
 
256
- qname: q.qname || q.oldQname,
257
-
258
- score: score,
259
-
260
- remarks: ( q.userAnswer?.[0]?.remarks ) || q.remarks || '',
261
-
262
- answerType: q.answerType || '',
263
-
264
- compliance: Boolean( q.compliance ),
265
-
266
- isYes: isYes || ( !isNo && score >= 10 ),
257
+ qname: q.qname || q.oldQname,
258
+
259
+ score: score,
260
+
261
+ remarks: ( q.userAnswer?.[0]?.remarks ) || q.remarks || '',
262
+
263
+ answerType: q.answerType || '',
264
+
265
+ compliance: Boolean( q.compliance ),
266
+
267
+ isYes: isYes || ( !isNo && score >= 10 ),
267
268
 
268
269
  isNo,
269
270
 
270
271
  answerDisplay: isYes ? 'Yes' : ( isNo ? 'No' : ( answerText && answerText.startsWith( 'http' ) ? 'Image' : ( answerText || '—' ) ) ),
271
272
 
272
- userAnswer: userAnswersWithRef.length ? userAnswersWithRef : [ {
273
- answer: '',
274
- answerType: 'text',
275
- remarks: '',
276
- referenceImage: '',
277
- imageMatchStatus: 'Matched',
278
- validation: false,
279
- validationType: '',
280
- validationAnswer: '',
281
- validationDisplayType: 'text',
282
- } ],
283
-
284
- } );
285
- } );
286
-
287
-
288
- const questionsCount = questions.length || sourceQuestionsCount;
273
+ userAnswer: userAnswersWithRef.length ? userAnswersWithRef : [ {
274
+ answer: '',
275
+ answerType: 'text',
276
+ remarks: '',
277
+ referenceImage: '',
278
+ imageMatchStatus: 'Matched',
279
+ validation: false,
280
+ validationType: '',
281
+ validationAnswer: '',
282
+ validationDisplayType: 'text',
283
+ } ],
284
+
285
+ } );
286
+ } );
287
+
288
+
289
+ const questionsCount = questions.length || sourceQuestionsCount;
289
290
 
290
291
 
291
292
  sectionInsights.push( {
@@ -340,7 +341,7 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
340
341
 
341
342
  */
342
343
 
343
- export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, brandInfo = {} ) {
344
+ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, brandInfo = {} ) {
344
345
  const doc = toPlainObject( processedDoc );
345
346
 
346
347
  const questionAnswer = doc.questionAnswers || [];
@@ -371,28 +372,28 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
371
372
  ( doc.submitTime_string?.split( ',' )?.[0]?.trim() || '' );
372
373
 
373
374
 
374
- const hasCompliancePage = questionAnswer.some( ( section ) =>
375
- ( section.questions || [] ).some( ( question ) => Boolean( question.compliance ) ),
376
- );
377
- const detailPageStart = hasCompliancePage ? 3 : 2;
378
- const detailPageGroups = [];
379
-
380
- for ( let i = 0; i < questionAnswers.length; i += 2 ) {
381
- const groupSections = questionAnswers.slice( i, i + 2 );
382
- const pageNumber = detailPageStart + detailPageGroups.length;
383
-
384
- groupSections.forEach( ( section ) => {
385
- section.pageNumber = pageNumber;
386
- } );
387
-
388
- detailPageGroups.push( {
389
- pageNumber,
390
- sections: groupSections,
391
- isLastGroup: i + 2 >= questionAnswers.length,
392
- } );
393
- }
394
-
395
- const totalPages = 1 + ( hasCompliancePage ? 1 : 0 ) + detailPageGroups.length;
375
+ const hasCompliancePage = questionAnswer.some( ( section ) =>
376
+ ( section.questions || [] ).some( ( question ) => Boolean( question.compliance ) ),
377
+ );
378
+ const detailPageStart = hasCompliancePage ? 3 : 2;
379
+ const detailPageGroups = [];
380
+
381
+ for ( let i = 0; i < questionAnswers.length; i += 2 ) {
382
+ const groupSections = questionAnswers.slice( i, i + 2 );
383
+ const pageNumber = detailPageStart + detailPageGroups.length;
384
+
385
+ groupSections.forEach( ( section ) => {
386
+ section.pageNumber = pageNumber;
387
+ } );
388
+
389
+ detailPageGroups.push( {
390
+ pageNumber,
391
+ sections: groupSections,
392
+ isLastGroup: i + 2 >= questionAnswers.length,
393
+ } );
394
+ }
395
+
396
+ const totalPages = 1 + ( hasCompliancePage ? 1 : 0 ) + detailPageGroups.length;
396
397
 
397
398
 
398
399
  const checklistName = doc.checkListName || 'Visit Checklist';
@@ -401,8 +402,41 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
401
402
 
402
403
  const { titleLine1, titleLine2 } = splitCoverTitle( checklistName );
403
404
 
405
+
406
+ let historyData = [];
407
+
408
+
409
+ const rawHistory = doc.historyData;
410
+
411
+
412
+ if ( Array.isArray( rawHistory ) && rawHistory.length > 0 ) {
413
+ historyData = rawHistory.map( ( h ) => {
414
+ const pct = Math.min( 100, Math.max( h.complianceScore ?? 0 ) );
415
+
416
+
417
+ return {
418
+
419
+
420
+ date: h.date,
421
+
422
+
423
+ value: pct,
424
+
425
+
426
+ percentage: pct,
427
+
428
+
429
+ barHeight: Math.round( ( pct / 100 ) * 140 ),
430
+
431
+
432
+ };
433
+ } );
434
+ }
435
+
436
+
404
437
  return {
405
438
 
439
+
406
440
  brandLogo: brandInfo.brandLogo || '',
407
441
 
408
442
  brandName: brandInfo.clientName || brandInfo.brandName || 'lenskart',
@@ -427,15 +461,15 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
427
461
 
428
462
  aiBreached: typeof doc.runAIFlag === 'number' ? doc.runAIFlag : 0,
429
463
 
430
- submittedBy: doc.userName || '--',
431
-
432
- country: doc.country || '--',
433
-
434
- hasCompliancePage,
435
-
436
- detailPageStart,
437
-
438
- totalPages,
464
+ submittedBy: doc.userName || '--',
465
+
466
+ country: doc.country || '--',
467
+
468
+ hasCompliancePage,
469
+
470
+ detailPageStart,
471
+
472
+ totalPages,
439
473
 
440
474
  totalPercentage,
441
475
 
@@ -449,7 +483,7 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
449
483
 
450
484
  '',
451
485
 
452
- historyData: [],
486
+ historyData,
453
487
 
454
488
  sectionInsights,
455
489
 
@@ -492,26 +526,26 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
492
526
  const totalPercentage = maxScore > 0 ? Math.round( ( totalScore / maxScore ) * 100 ) : 0;
493
527
 
494
528
 
495
- const hasCompliancePage = Boolean( checklistInfo.complianceCount );
496
- const detailPageStart = hasCompliancePage ? 3 : 2;
497
- const detailPageGroups = [];
498
-
499
- for ( let i = 0; i < questionAnswers.length; i += 2 ) {
500
- const groupSections = questionAnswers.slice( i, i + 2 );
501
- const pageNumber = detailPageStart + detailPageGroups.length;
502
-
503
- groupSections.forEach( ( section ) => {
504
- section.pageNumber = pageNumber;
505
- } );
506
-
507
- detailPageGroups.push( {
508
- pageNumber,
509
- sections: groupSections,
510
- isLastGroup: i + 2 >= questionAnswers.length,
511
- } );
512
- }
513
-
514
- const totalPages = 1 + ( hasCompliancePage ? 1 : 0 ) + detailPageGroups.length;
529
+ const hasCompliancePage = Boolean( checklistInfo.complianceCount );
530
+ const detailPageStart = hasCompliancePage ? 3 : 2;
531
+ const detailPageGroups = [];
532
+
533
+ for ( let i = 0; i < questionAnswers.length; i += 2 ) {
534
+ const groupSections = questionAnswers.slice( i, i + 2 );
535
+ const pageNumber = detailPageStart + detailPageGroups.length;
536
+
537
+ groupSections.forEach( ( section ) => {
538
+ section.pageNumber = pageNumber;
539
+ } );
540
+
541
+ detailPageGroups.push( {
542
+ pageNumber,
543
+ sections: groupSections,
544
+ isLastGroup: i + 2 >= questionAnswers.length,
545
+ } );
546
+ }
547
+
548
+ const totalPages = 1 + ( hasCompliancePage ? 1 : 0 ) + detailPageGroups.length;
515
549
 
516
550
  const rawChecklistTitle = checklistInfo?.checklistName || getchecklistData?.data?.checklistName || 'AOM Visit Checklist';
517
551
 
@@ -538,7 +572,6 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
538
572
  };
539
573
  } );
540
574
  }
541
- console.log( brandInfo.brandLogo, 'jhgfghj' );
542
575
 
543
576
  return {
544
577
 
@@ -570,15 +603,15 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
570
603
 
571
604
  aiBreached: checklistAnswer?.aiBreachedCount ?? 0,
572
605
 
573
- submittedBy: checklistInfo?.submittedBy || storeProfile?.userName || '--',
574
-
575
- country: storeProfile?.Country || '--',
576
-
577
- hasCompliancePage,
578
-
579
- detailPageStart,
580
-
581
- totalPages,
606
+ submittedBy: checklistInfo?.submittedBy || storeProfile?.userName || '--',
607
+
608
+ country: storeProfile?.Country || '--',
609
+
610
+ hasCompliancePage,
611
+
612
+ detailPageStart,
613
+
614
+ totalPages,
582
615
 
583
616
  totalPercentage,
584
617
 
@@ -598,7 +631,7 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
598
631
 
599
632
  flags,
600
633
 
601
- complianceCount: hasCompliancePage,
634
+ complianceCount: hasCompliancePage,
602
635
 
603
636
  };
604
637
  }
@@ -625,16 +658,283 @@ export function buildVisitChecklistTemplateData( arg1, arg2, arg3 ) {
625
658
 
626
659
  /**
627
660
 
628
- * Generates PDF buffer from template data
629
661
 
630
- * @param {Object} templateData - Context for visit-checklist.hbs
662
+ * In-memory image cache: URL base64 data URI.
663
+
631
664
 
632
- * @param {string} baseUrl - Base URL for resolving image URLs (e.g. cloudfront)
665
+ * Shared across all PDFs in a single request lifecycle.
633
666
 
634
- * @return {Promise<Buffer>} PDF buffer
635
667
 
636
668
  */
637
669
 
670
+
671
+ export function createImageCache() {
672
+ const cache = new Map();
673
+
674
+
675
+ async function fetchAsDataUri( url ) {
676
+ if ( !url || typeof url !== 'string' || !url.startsWith( 'http' ) ) return url;
677
+
678
+
679
+ if ( cache.has( url ) ) return cache.get( url );
680
+
681
+
682
+ try {
683
+ const controller = new AbortController();
684
+
685
+
686
+ const timeout = setTimeout( () => controller.abort(), 10000 );
687
+
688
+
689
+ const res = await fetch( url, { signal: controller.signal } );
690
+
691
+
692
+ clearTimeout( timeout );
693
+
694
+
695
+ if ( !res.ok ) {
696
+ cache.set( url, url );
697
+
698
+
699
+ return url;
700
+ }
701
+
702
+
703
+ const contentType = res.headers.get( 'content-type' ) || 'image/png';
704
+
705
+
706
+ const buffer = Buffer.from( await res.arrayBuffer() );
707
+
708
+
709
+ const dataUri = `data:${contentType};base64,${buffer.toString( 'base64' )}`;
710
+
711
+
712
+ cache.set( url, dataUri );
713
+
714
+
715
+ return dataUri;
716
+ } catch {
717
+ cache.set( url, url );
718
+
719
+
720
+ return url;
721
+ }
722
+ }
723
+
724
+
725
+ async function resolveAllImages( resolvedData ) {
726
+ const urls = new Set();
727
+
728
+
729
+ if ( resolvedData.brandLogo && resolvedData.brandLogo.startsWith( 'http' ) ) {
730
+ urls.add( resolvedData.brandLogo );
731
+ }
732
+
733
+
734
+ const collectFromSection = ( section ) => {
735
+ section.questions?.forEach( ( q ) => {
736
+ q.userAnswer?.forEach( ( ua ) => {
737
+ if ( ua.referenceImage && ua.referenceImage.startsWith( 'http' ) ) urls.add( ua.referenceImage );
738
+
739
+
740
+ if ( ua.answer && ua.answerType === 'image' && ua.answer.startsWith( 'http' ) ) urls.add( ua.answer );
741
+
742
+
743
+ if ( ua.validationAnswer && ua.validationDisplayType === 'image' && ua.validationAnswer.startsWith( 'http' ) ) urls.add( ua.validationAnswer );
744
+ } );
745
+ } );
746
+ };
747
+
748
+
749
+ resolvedData.questionAnswers?.forEach( collectFromSection );
750
+
751
+
752
+ resolvedData.detailPageGroups?.forEach( ( group ) => {
753
+ group.sections?.forEach( collectFromSection );
754
+ } );
755
+
756
+
757
+ // Fetch all unique URLs in parallel (max 20 concurrent)
758
+
759
+
760
+ const urlArray = [ ...urls ];
761
+
762
+
763
+ const BATCH_SIZE = 20;
764
+
765
+
766
+ for ( let i = 0; i < urlArray.length; i += BATCH_SIZE ) {
767
+ await Promise.all( urlArray.slice( i, i + BATCH_SIZE ).map( ( u ) => fetchAsDataUri( u ) ) );
768
+ }
769
+
770
+
771
+ // Replace URLs with cached data URIs
772
+
773
+
774
+ if ( resolvedData.brandLogo ) {
775
+ resolvedData.brandLogo = cache.get( resolvedData.brandLogo ) || resolvedData.brandLogo;
776
+ }
777
+
778
+
779
+ const replaceInSection = ( section ) => {
780
+ section.questions?.forEach( ( q ) => {
781
+ q.userAnswer?.forEach( ( ua ) => {
782
+ if ( ua.referenceImage && cache.has( ua.referenceImage ) ) ua.referenceImage = cache.get( ua.referenceImage );
783
+
784
+
785
+ if ( ua.answer && ua.answerType === 'image' && cache.has( ua.answer ) ) ua.answer = cache.get( ua.answer );
786
+
787
+
788
+ if ( ua.validationAnswer && ua.validationDisplayType === 'image' && cache.has( ua.validationAnswer ) ) ua.validationAnswer = cache.get( ua.validationAnswer );
789
+ } );
790
+ } );
791
+ };
792
+
793
+
794
+ resolvedData.questionAnswers?.forEach( replaceInSection );
795
+
796
+
797
+ resolvedData.detailPageGroups?.forEach( ( group ) => {
798
+ group.sections?.forEach( replaceInSection );
799
+ } );
800
+
801
+
802
+ return resolvedData;
803
+ }
804
+
805
+
806
+ return { fetchAsDataUri, resolveAllImages, cache };
807
+ }
808
+
809
+
810
+ export function getCompiledVisitChecklistTemplate() {
811
+ const templatePath = path.join( __dirname, '../hbs/visit-checklist.hbs' );
812
+
813
+
814
+ const templateHtml = fs.readFileSync( templatePath, 'utf8' );
815
+
816
+
817
+ return handlebars.compile( templateHtml );
818
+ }
819
+
820
+
821
+ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2sskgmri.cloudfront.net/' ) {
822
+ if ( baseUrl && !baseUrl.endsWith( '/' ) ) baseUrl += '/';
823
+
824
+
825
+ const resolveUrl = ( url ) => {
826
+ if ( !url || typeof url !== 'string' ) return url;
827
+
828
+
829
+ if ( url.startsWith( 'http' ) ) return url;
830
+
831
+
832
+ return baseUrl + url.replace( /^\//, '' );
833
+ };
834
+
835
+
836
+ const resolvedData = JSON.parse( JSON.stringify( templateData ) );
837
+
838
+
839
+ const resolveQuestionMedia = ( section ) => {
840
+ section.questions?.forEach( ( q ) => {
841
+ q.userAnswer?.forEach( ( ua ) => {
842
+ if ( ua.referenceImage ) ua.referenceImage = resolveUrl( ua.referenceImage );
843
+
844
+
845
+ if ( ua.validationAnswer && ua.validationDisplayType !== 'text' ) {
846
+ ua.validationAnswer = resolveUrl( ua.validationAnswer );
847
+ }
848
+
849
+
850
+ if ( ua.answer && ua.answerType !== 'text' ) {
851
+ ua.answer = resolveUrl( ua.answer );
852
+ }
853
+ } );
854
+ } );
855
+ };
856
+
857
+
858
+ resolvedData.questionAnswers?.forEach( resolveQuestionMedia );
859
+
860
+
861
+ resolvedData.detailPageGroups?.forEach( ( group ) => {
862
+ group.sections?.forEach( resolveQuestionMedia );
863
+ } );
864
+
865
+
866
+ if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
867
+ resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
868
+ }
869
+
870
+
871
+ return resolvedData;
872
+ }
873
+
874
+
875
+ export async function generatePDFFromPage( page, template, templateData, baseUrl, imageCache ) {
876
+ const resolvedData = resolveTemplateUrls( templateData, baseUrl );
877
+
878
+
879
+ if ( imageCache ) {
880
+ await imageCache.resolveAllImages( resolvedData );
881
+ }
882
+
883
+
884
+ const html = template( resolvedData );
885
+
886
+
887
+ await page.setContent( html, { waitUntil: 'domcontentloaded' } );
888
+
889
+
890
+ if ( !imageCache ) {
891
+ await Promise.race( [
892
+
893
+
894
+ page.evaluate( async () => {
895
+ const images = Array.from( document.images );
896
+
897
+
898
+ await Promise.all(
899
+
900
+
901
+ images.map( ( img ) => {
902
+ if ( img.complete ) return;
903
+
904
+
905
+ return new Promise( ( resolve ) => {
906
+ img.onload = img.onerror = resolve;
907
+ } );
908
+ } ),
909
+
910
+
911
+ );
912
+ } ),
913
+
914
+
915
+ new Promise( ( resolve ) => setTimeout( resolve, 30000 ) ),
916
+
917
+
918
+ ] );
919
+ }
920
+
921
+
922
+ return page.pdf( {
923
+
924
+
925
+ format: 'A4',
926
+
927
+
928
+ printBackground: true,
929
+
930
+
931
+ margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
932
+
933
+
934
+ } );
935
+ }
936
+
937
+
638
938
  export async function generateVisitChecklistPDF( templateData, baseUrl = 'https://d1r0hc2sskgmri.cloudfront.net/' ) {
639
939
  const templatePath = path.join( __dirname, '../hbs/visit-checklist.hbs' );
640
940
 
@@ -657,26 +957,26 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
657
957
 
658
958
  const resolvedData = JSON.parse( JSON.stringify( templateData ) );
659
959
 
660
- const resolveQuestionMedia = ( section ) => {
661
- section.questions?.forEach( ( q ) => {
662
- q.userAnswer?.forEach( ( ua ) => {
663
- if ( ua.referenceImage ) ua.referenceImage = resolveUrl( ua.referenceImage );
664
-
665
- if ( ua.validationAnswer && ua.validationDisplayType !== 'text' ) {
666
- ua.validationAnswer = resolveUrl( ua.validationAnswer );
667
- }
668
-
669
- if ( ua.answer && ua.answerType !== 'text' ) {
670
- ua.answer = resolveUrl( ua.answer );
671
- }
672
- } );
673
- } );
674
- };
675
-
676
- resolvedData.questionAnswers?.forEach( resolveQuestionMedia );
677
- resolvedData.detailPageGroups?.forEach( ( group ) => {
678
- group.sections?.forEach( resolveQuestionMedia );
679
- } );
960
+ const resolveQuestionMedia = ( section ) => {
961
+ section.questions?.forEach( ( q ) => {
962
+ q.userAnswer?.forEach( ( ua ) => {
963
+ if ( ua.referenceImage ) ua.referenceImage = resolveUrl( ua.referenceImage );
964
+
965
+ if ( ua.validationAnswer && ua.validationDisplayType !== 'text' ) {
966
+ ua.validationAnswer = resolveUrl( ua.validationAnswer );
967
+ }
968
+
969
+ if ( ua.answer && ua.answerType !== 'text' ) {
970
+ ua.answer = resolveUrl( ua.answer );
971
+ }
972
+ } );
973
+ } );
974
+ };
975
+
976
+ resolvedData.questionAnswers?.forEach( resolveQuestionMedia );
977
+ resolvedData.detailPageGroups?.forEach( ( group ) => {
978
+ group.sections?.forEach( resolveQuestionMedia );
979
+ } );
680
980
 
681
981
  if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
682
982
  resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );