tango-app-api-trax 3.7.77 → 3.7.79
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 +1 -1
- package/src/controllers/download.controller.js +375 -1
- package/src/controllers/handlebar-helper.js +18 -0
- package/src/controllers/internalTrax.controller.js +13 -8
- package/src/controllers/mobileTrax.controller.js +11 -9
- package/src/controllers/trax.controller.js +11 -2
- package/src/hbs/partials/tangoye-footer-logo.hbs +18 -0
- package/src/hbs/visit-checklist.hbs +261 -0
- package/src/routes/internalTraxApi.router.js +1 -0
- package/src/utils/visitChecklistPdf.utils.js +738 -0
|
@@ -0,0 +1,738 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
|
|
3
|
+
import path from 'path';
|
|
4
|
+
|
|
5
|
+
import { fileURLToPath } from 'url';
|
|
6
|
+
|
|
7
|
+
import puppeteer from 'puppeteer';
|
|
8
|
+
|
|
9
|
+
import handlebars from '../controllers/handlebar-helper.js';
|
|
10
|
+
|
|
11
|
+
import dayjs from 'dayjs';
|
|
12
|
+
|
|
13
|
+
|
|
14
|
+
const __filename = fileURLToPath( import.meta.url );
|
|
15
|
+
|
|
16
|
+
const __dirname = path.dirname( __filename );
|
|
17
|
+
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
|
|
21
|
+
* @param {Object} doc - Mongoose doc or plain object
|
|
22
|
+
|
|
23
|
+
* @return {Object}
|
|
24
|
+
|
|
25
|
+
*/
|
|
26
|
+
|
|
27
|
+
function toPlainObject( doc ) {
|
|
28
|
+
if ( !doc ) return {};
|
|
29
|
+
|
|
30
|
+
if ( typeof doc.toObject === 'function' ) return doc.toObject();
|
|
31
|
+
|
|
32
|
+
return { ...doc };
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
/**
|
|
37
|
+
* Splits checklist display name into two cover lines (e.g. "AOM" + "Visit Checklist").
|
|
38
|
+
* @param {string} [checklistName] Full checklist name from API or DB
|
|
39
|
+
* @return {{ titleLine1: string, titleLine2: string }} titleLine1 and titleLine2 for the PDF cover
|
|
40
|
+
*/
|
|
41
|
+
function splitCoverTitle( checklistName ) {
|
|
42
|
+
const n = ( checklistName || '' ).trim();
|
|
43
|
+
|
|
44
|
+
if ( !n ) {
|
|
45
|
+
return { titleLine1: 'AOM', titleLine2: 'Visit Checklist' };
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
const m = n.match( /^(.+?)\s+(Visit Checklist)\s*$/i );
|
|
49
|
+
|
|
50
|
+
if ( m ) {
|
|
51
|
+
return { titleLine1: m[1].trim() || 'AOM', titleLine2: 'Visit Checklist' };
|
|
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
|
+
}
|
|
169
|
+
|
|
170
|
+
|
|
171
|
+
/**
|
|
172
|
+
|
|
173
|
+
* Map section array { sectionName, questions[] } (view API or processed checklist) to scores / template sections.
|
|
174
|
+
|
|
175
|
+
* @param {Array} questionAnswer
|
|
176
|
+
|
|
177
|
+
* @return {{ totalScore: number, maxScore: number, sectionInsights: Array, questionAnswers: Array, flags: Array, numQuestions: number }}
|
|
178
|
+
|
|
179
|
+
*/
|
|
180
|
+
|
|
181
|
+
function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
182
|
+
let totalScore = 0;
|
|
183
|
+
|
|
184
|
+
let maxScore = 0;
|
|
185
|
+
|
|
186
|
+
const sectionInsights = [];
|
|
187
|
+
|
|
188
|
+
const questionAnswers = [];
|
|
189
|
+
|
|
190
|
+
const flags = [];
|
|
191
|
+
|
|
192
|
+
|
|
193
|
+
( questionAnswer || [] ).forEach( ( section, sectionIdx ) => {
|
|
194
|
+
let sectionScore = 0;
|
|
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;
|
|
218
|
+
|
|
219
|
+
sectionScore += score;
|
|
220
|
+
|
|
221
|
+
sectionMax += max;
|
|
222
|
+
|
|
223
|
+
totalScore += score;
|
|
224
|
+
|
|
225
|
+
maxScore += max;
|
|
226
|
+
|
|
227
|
+
if ( score >= 10 ) passedCount++;
|
|
228
|
+
|
|
229
|
+
|
|
230
|
+
const answerText = ( ua?.answer || '' ).toString().trim();
|
|
231
|
+
|
|
232
|
+
const isYes = answerText.toLowerCase() === 'yes';
|
|
233
|
+
|
|
234
|
+
const isNo = answerText.toLowerCase() === 'no';
|
|
235
|
+
|
|
236
|
+
const isFlagged = isNo;
|
|
237
|
+
|
|
238
|
+
if ( isFlagged ) {
|
|
239
|
+
flags.push( {
|
|
240
|
+
|
|
241
|
+
sectionName: section.sectionName || `Section ${sectionIdx + 1}`,
|
|
242
|
+
|
|
243
|
+
qno: q.qno || q.uniqueqno,
|
|
244
|
+
|
|
245
|
+
qname: q.qname || q.oldQname,
|
|
246
|
+
|
|
247
|
+
answer: answerText,
|
|
248
|
+
|
|
249
|
+
} );
|
|
250
|
+
}
|
|
251
|
+
|
|
252
|
+
questions.push( {
|
|
253
|
+
|
|
254
|
+
qno: q.qno || q.uniqueqno,
|
|
255
|
+
|
|
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 ),
|
|
267
|
+
|
|
268
|
+
isNo,
|
|
269
|
+
|
|
270
|
+
answerDisplay: isYes ? 'Yes' : ( isNo ? 'No' : ( answerText && answerText.startsWith( 'http' ) ? 'Image' : ( answerText || '—' ) ) ),
|
|
271
|
+
|
|
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;
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
sectionInsights.push( {
|
|
292
|
+
|
|
293
|
+
sectionName: section.sectionName || `Section ${sectionIdx + 1}`,
|
|
294
|
+
|
|
295
|
+
targetScore: sectionMax,
|
|
296
|
+
|
|
297
|
+
actualScore: sectionScore,
|
|
298
|
+
|
|
299
|
+
questionsCount,
|
|
300
|
+
|
|
301
|
+
passedCount,
|
|
302
|
+
|
|
303
|
+
percentage: sectionMax > 0 ? Math.round( ( sectionScore / sectionMax ) * 100 ) : 0,
|
|
304
|
+
|
|
305
|
+
} );
|
|
306
|
+
|
|
307
|
+
|
|
308
|
+
questionAnswers.push( {
|
|
309
|
+
|
|
310
|
+
sectionName: section.sectionName || `Section ${sectionIdx + 1}`,
|
|
311
|
+
|
|
312
|
+
currentScore: sectionScore,
|
|
313
|
+
|
|
314
|
+
maxScore: sectionMax,
|
|
315
|
+
|
|
316
|
+
percentage: sectionMax > 0 ? Math.round( ( sectionScore / sectionMax ) * 100 ) : 0,
|
|
317
|
+
|
|
318
|
+
pageNumber: sectionIdx + 3,
|
|
319
|
+
|
|
320
|
+
questions,
|
|
321
|
+
|
|
322
|
+
} );
|
|
323
|
+
} );
|
|
324
|
+
|
|
325
|
+
|
|
326
|
+
const numQuestions = questionAnswer.reduce( ( sum, s ) => sum + ( s.questions?.length || 0 ), 0 );
|
|
327
|
+
|
|
328
|
+
|
|
329
|
+
return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions };
|
|
330
|
+
}
|
|
331
|
+
|
|
332
|
+
|
|
333
|
+
/**
|
|
334
|
+
|
|
335
|
+
* Build template context from a processed checklist document (MongoDB / findOne shape).
|
|
336
|
+
|
|
337
|
+
* @param {Object} processedDoc - processedchecklist row
|
|
338
|
+
|
|
339
|
+
* @param {Object} brandInfo - { brandLogo, clientName }
|
|
340
|
+
|
|
341
|
+
*/
|
|
342
|
+
|
|
343
|
+
export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, brandInfo = {} ) {
|
|
344
|
+
const doc = toPlainObject( processedDoc );
|
|
345
|
+
|
|
346
|
+
const questionAnswer = doc.questionAnswers || [];
|
|
347
|
+
|
|
348
|
+
|
|
349
|
+
const {
|
|
350
|
+
|
|
351
|
+
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
|
|
352
|
+
|
|
353
|
+
} = mapSectionsFromQuestionAnswer( questionAnswer );
|
|
354
|
+
|
|
355
|
+
|
|
356
|
+
const totalPercentage = maxScore > 0 ? Math.round( ( totalScore / maxScore ) * 100 ) : 0;
|
|
357
|
+
|
|
358
|
+
|
|
359
|
+
const submitMoment = doc.submitTime || doc.date_iso || doc.date_string;
|
|
360
|
+
|
|
361
|
+
const formattedDate = submitMoment && dayjs( submitMoment ).isValid() ?
|
|
362
|
+
|
|
363
|
+
dayjs( submitMoment ).format( 'DD MMMM, YYYY' ) :
|
|
364
|
+
|
|
365
|
+
( doc.date_string || '' );
|
|
366
|
+
|
|
367
|
+
const formattedTime = doc.submitTime && dayjs( doc.submitTime ).isValid() ?
|
|
368
|
+
|
|
369
|
+
dayjs( doc.submitTime ).format( 'hh:mm A' ) :
|
|
370
|
+
|
|
371
|
+
( doc.submitTime_string?.split( ',' )?.[0]?.trim() || '' );
|
|
372
|
+
|
|
373
|
+
|
|
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;
|
|
396
|
+
|
|
397
|
+
|
|
398
|
+
const checklistName = doc.checkListName || 'Visit Checklist';
|
|
399
|
+
|
|
400
|
+
const refFromId = doc._id && String( doc._id ).length > 8 ? String( doc._id ).slice( -8 ).toUpperCase() : ( doc._id ? String( doc._id ) : '--' );
|
|
401
|
+
|
|
402
|
+
const { titleLine1, titleLine2 } = splitCoverTitle( checklistName );
|
|
403
|
+
|
|
404
|
+
return {
|
|
405
|
+
|
|
406
|
+
brandLogo: brandInfo.brandLogo || '',
|
|
407
|
+
|
|
408
|
+
brandName: brandInfo.clientName || brandInfo.brandName || 'lenskart',
|
|
409
|
+
|
|
410
|
+
checklistType: checklistName,
|
|
411
|
+
|
|
412
|
+
checklistTitle: checklistName,
|
|
413
|
+
|
|
414
|
+
titleLine1,
|
|
415
|
+
|
|
416
|
+
titleLine2,
|
|
417
|
+
|
|
418
|
+
referenceId: doc.store_id || doc.storeName || refFromId,
|
|
419
|
+
|
|
420
|
+
date: formattedDate,
|
|
421
|
+
|
|
422
|
+
time: formattedTime,
|
|
423
|
+
|
|
424
|
+
numQuestions: doc.questionCount ?? numQuestions,
|
|
425
|
+
|
|
426
|
+
numFlags: typeof doc.questionFlag === 'number' ? doc.questionFlag : flags.length,
|
|
427
|
+
|
|
428
|
+
aiBreached: typeof doc.runAIFlag === 'number' ? doc.runAIFlag : 0,
|
|
429
|
+
|
|
430
|
+
submittedBy: doc.userName || '--',
|
|
431
|
+
|
|
432
|
+
country: doc.country || '--',
|
|
433
|
+
|
|
434
|
+
hasCompliancePage,
|
|
435
|
+
|
|
436
|
+
detailPageStart,
|
|
437
|
+
|
|
438
|
+
totalPages,
|
|
439
|
+
|
|
440
|
+
totalPercentage,
|
|
441
|
+
|
|
442
|
+
totalScore,
|
|
443
|
+
|
|
444
|
+
maxScore,
|
|
445
|
+
|
|
446
|
+
reportDate: submitMoment && dayjs( submitMoment ).isValid() ?
|
|
447
|
+
|
|
448
|
+
dayjs( submitMoment ).format( 'DD MMM, YYYY dddd' ) :
|
|
449
|
+
|
|
450
|
+
'',
|
|
451
|
+
|
|
452
|
+
historyData: [],
|
|
453
|
+
|
|
454
|
+
sectionInsights,
|
|
455
|
+
|
|
456
|
+
questionAnswers,
|
|
457
|
+
|
|
458
|
+
detailPageGroups,
|
|
459
|
+
|
|
460
|
+
flags,
|
|
461
|
+
|
|
462
|
+
};
|
|
463
|
+
}
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandInfo = {} ) {
|
|
467
|
+
const checklistAnswer = viewchecklistData?.data?.checklistAnswers?.[0] || viewchecklistData?.checklistAnswers?.[0];
|
|
468
|
+
|
|
469
|
+
const storeProfile = checklistAnswer?.storeProfile || {};
|
|
470
|
+
|
|
471
|
+
const checklistInfo = checklistAnswer?.checklistInfo || {};
|
|
472
|
+
|
|
473
|
+
const questionAnswer = checklistAnswer?.questionAnswer || [];
|
|
474
|
+
|
|
475
|
+
|
|
476
|
+
const submitDate = checklistInfo?.date || checklistInfo?.submitDate || '';
|
|
477
|
+
|
|
478
|
+
const submitTime = checklistInfo?.time || checklistInfo?.submitTime || '';
|
|
479
|
+
|
|
480
|
+
const formattedDate = submitDate ? ( dayjs( submitDate ).isValid() ? dayjs( submitDate ).format( 'DD MMMM, YYYY' ) : submitDate ) : submitDate;
|
|
481
|
+
|
|
482
|
+
const formattedTime = submitTime ? ( dayjs( submitTime ).isValid() ? dayjs( submitTime ).format( 'hh:mm A' ) : submitTime ) : submitTime;
|
|
483
|
+
|
|
484
|
+
|
|
485
|
+
const {
|
|
486
|
+
|
|
487
|
+
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
|
|
488
|
+
|
|
489
|
+
} = mapSectionsFromQuestionAnswer( questionAnswer );
|
|
490
|
+
|
|
491
|
+
|
|
492
|
+
const totalPercentage = maxScore > 0 ? Math.round( ( totalScore / maxScore ) * 100 ) : 0;
|
|
493
|
+
|
|
494
|
+
|
|
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;
|
|
515
|
+
|
|
516
|
+
const rawChecklistTitle = checklistInfo?.checklistName || getchecklistData?.data?.checklistName || 'AOM Visit Checklist';
|
|
517
|
+
|
|
518
|
+
const { titleLine1, titleLine2 } = splitCoverTitle( rawChecklistTitle );
|
|
519
|
+
|
|
520
|
+
let historyData = [];
|
|
521
|
+
|
|
522
|
+
const rawHistory = viewchecklistData?.data?.historyData || viewchecklistData?.historyData || checklistAnswer?.historyData;
|
|
523
|
+
|
|
524
|
+
if ( Array.isArray( rawHistory ) && rawHistory.length > 0 ) {
|
|
525
|
+
historyData = rawHistory.map( ( h ) => {
|
|
526
|
+
const pct = Math.min( 100, Math.max( 0, h.value ?? h.percentage ?? 0 ) );
|
|
527
|
+
|
|
528
|
+
return {
|
|
529
|
+
|
|
530
|
+
date: h.date || h.label || '',
|
|
531
|
+
|
|
532
|
+
value: pct,
|
|
533
|
+
|
|
534
|
+
percentage: pct,
|
|
535
|
+
|
|
536
|
+
barHeight: Math.round( ( pct / 100 ) * 140 ),
|
|
537
|
+
|
|
538
|
+
};
|
|
539
|
+
} );
|
|
540
|
+
}
|
|
541
|
+
console.log( brandInfo.brandLogo, 'jhgfghj' );
|
|
542
|
+
|
|
543
|
+
return {
|
|
544
|
+
|
|
545
|
+
brandLogo: brandInfo.brandLogo || '',
|
|
546
|
+
|
|
547
|
+
brandName: brandInfo.clientName || brandInfo.brandName || 'lenskart',
|
|
548
|
+
|
|
549
|
+
checklistType: rawChecklistTitle,
|
|
550
|
+
|
|
551
|
+
checklistTitle: checklistInfo?.checklistName || rawChecklistTitle,
|
|
552
|
+
|
|
553
|
+
titleLine1,
|
|
554
|
+
|
|
555
|
+
titleLine2,
|
|
556
|
+
|
|
557
|
+
referenceId: storeProfile?.store_id || ( checklistInfo?.checklistId && String( checklistInfo.checklistId ).length > 8 ?
|
|
558
|
+
|
|
559
|
+
String( checklistInfo.checklistId ).slice( -8 ).toUpperCase() :
|
|
560
|
+
|
|
561
|
+
checklistInfo?.checklistId || '--' ),
|
|
562
|
+
|
|
563
|
+
date: formattedDate,
|
|
564
|
+
|
|
565
|
+
time: formattedTime,
|
|
566
|
+
|
|
567
|
+
numQuestions: checklistInfo?.noofQuestions || numQuestions,
|
|
568
|
+
|
|
569
|
+
numFlags: checklistAnswer?.flagCount ?? flags.length,
|
|
570
|
+
|
|
571
|
+
aiBreached: checklistAnswer?.aiBreachedCount ?? 0,
|
|
572
|
+
|
|
573
|
+
submittedBy: checklistInfo?.submittedBy || storeProfile?.userName || '--',
|
|
574
|
+
|
|
575
|
+
country: storeProfile?.Country || '--',
|
|
576
|
+
|
|
577
|
+
hasCompliancePage,
|
|
578
|
+
|
|
579
|
+
detailPageStart,
|
|
580
|
+
|
|
581
|
+
totalPages,
|
|
582
|
+
|
|
583
|
+
totalPercentage,
|
|
584
|
+
|
|
585
|
+
totalScore,
|
|
586
|
+
|
|
587
|
+
maxScore,
|
|
588
|
+
|
|
589
|
+
reportDate: formattedDate && submitDate && dayjs( submitDate ).isValid() ? dayjs( submitDate ).format( 'DD MMM, YYYY dddd' ) : '',
|
|
590
|
+
|
|
591
|
+
historyData,
|
|
592
|
+
|
|
593
|
+
sectionInsights,
|
|
594
|
+
|
|
595
|
+
questionAnswers,
|
|
596
|
+
|
|
597
|
+
detailPageGroups,
|
|
598
|
+
|
|
599
|
+
flags,
|
|
600
|
+
|
|
601
|
+
complianceCount: hasCompliancePage,
|
|
602
|
+
|
|
603
|
+
};
|
|
604
|
+
}
|
|
605
|
+
|
|
606
|
+
|
|
607
|
+
/**
|
|
608
|
+
|
|
609
|
+
* Builds template data for visit-checklist.hbs.
|
|
610
|
+
|
|
611
|
+
* - Two args: (processedChecklistDoc, brandInfo) — MongoDB processed checklist shape (questionAnswers, storeName, …)
|
|
612
|
+
|
|
613
|
+
* - Three args: (getchecklistData, viewchecklistData, brandInfo) — legacy view-checklist API shape
|
|
614
|
+
|
|
615
|
+
*/
|
|
616
|
+
|
|
617
|
+
export function buildVisitChecklistTemplateData( arg1, arg2, arg3 ) {
|
|
618
|
+
if ( arg3 !== undefined && arg3 !== null ) {
|
|
619
|
+
return buildFromViewChecklistApi( arg1, arg2, arg3 );
|
|
620
|
+
}
|
|
621
|
+
|
|
622
|
+
return buildVisitChecklistTemplateDataFromProcessed( arg1, arg2 || {} );
|
|
623
|
+
}
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
/**
|
|
627
|
+
|
|
628
|
+
* Generates PDF buffer from template data
|
|
629
|
+
|
|
630
|
+
* @param {Object} templateData - Context for visit-checklist.hbs
|
|
631
|
+
|
|
632
|
+
* @param {string} baseUrl - Base URL for resolving image URLs (e.g. cloudfront)
|
|
633
|
+
|
|
634
|
+
* @return {Promise<Buffer>} PDF buffer
|
|
635
|
+
|
|
636
|
+
*/
|
|
637
|
+
|
|
638
|
+
export async function generateVisitChecklistPDF( templateData, baseUrl = 'https://d1r0hc2sskgmri.cloudfront.net/' ) {
|
|
639
|
+
const templatePath = path.join( __dirname, '../hbs/visit-checklist.hbs' );
|
|
640
|
+
|
|
641
|
+
const templateHtml = fs.readFileSync( templatePath, 'utf8' );
|
|
642
|
+
|
|
643
|
+
const template = handlebars.compile( templateHtml );
|
|
644
|
+
|
|
645
|
+
|
|
646
|
+
if ( baseUrl && !baseUrl.endsWith( '/' ) ) baseUrl += '/';
|
|
647
|
+
|
|
648
|
+
|
|
649
|
+
const resolveUrl = ( url ) => {
|
|
650
|
+
if ( !url || typeof url !== 'string' ) return url;
|
|
651
|
+
|
|
652
|
+
if ( url.startsWith( 'http' ) ) return url;
|
|
653
|
+
|
|
654
|
+
return baseUrl + url.replace( /^\//, '' );
|
|
655
|
+
};
|
|
656
|
+
|
|
657
|
+
|
|
658
|
+
const resolvedData = JSON.parse( JSON.stringify( templateData ) );
|
|
659
|
+
|
|
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
|
+
} );
|
|
680
|
+
|
|
681
|
+
if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
|
|
682
|
+
resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
|
|
683
|
+
}
|
|
684
|
+
|
|
685
|
+
|
|
686
|
+
const html = template( resolvedData );
|
|
687
|
+
|
|
688
|
+
|
|
689
|
+
const browser = await puppeteer.launch( {
|
|
690
|
+
|
|
691
|
+
headless: 'new',
|
|
692
|
+
|
|
693
|
+
args: [ '--no-sandbox', '--disable-setuid-sandbox', '--disable-dev-shm-usage' ],
|
|
694
|
+
|
|
695
|
+
} );
|
|
696
|
+
|
|
697
|
+
|
|
698
|
+
try {
|
|
699
|
+
const page = await browser.newPage();
|
|
700
|
+
|
|
701
|
+
await page.setContent( html, { waitUntil: 'domcontentloaded' } );
|
|
702
|
+
|
|
703
|
+
|
|
704
|
+
await page.evaluate( async () => {
|
|
705
|
+
const images = Array.from( document.images );
|
|
706
|
+
|
|
707
|
+
await Promise.all(
|
|
708
|
+
|
|
709
|
+
images.map( ( img ) => {
|
|
710
|
+
if ( img.complete ) return;
|
|
711
|
+
|
|
712
|
+
return new Promise( ( resolve ) => {
|
|
713
|
+
img.onload = img.onerror = resolve;
|
|
714
|
+
} );
|
|
715
|
+
} ),
|
|
716
|
+
|
|
717
|
+
);
|
|
718
|
+
} );
|
|
719
|
+
|
|
720
|
+
|
|
721
|
+
const pdfBuffer = await page.pdf( {
|
|
722
|
+
|
|
723
|
+
format: 'A4',
|
|
724
|
+
|
|
725
|
+
printBackground: true,
|
|
726
|
+
|
|
727
|
+
margin: { top: '10mm', right: '10mm', bottom: '10mm', left: '10mm' },
|
|
728
|
+
|
|
729
|
+
} );
|
|
730
|
+
|
|
731
|
+
|
|
732
|
+
return pdfBuffer;
|
|
733
|
+
} finally {
|
|
734
|
+
await browser.close();
|
|
735
|
+
}
|
|
736
|
+
}
|
|
737
|
+
|
|
738
|
+
|