tango-app-api-trax 3.9.39 → 3.9.40
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/index.js +2 -1
- package/package.json +1 -1
- package/src/controllers/internalTrax.controller.js +225 -94
- package/src/controllers/mobileTrax.controller.js +69 -29
- package/src/controllers/trax.controller.js +3 -2
- package/src/controllers/traxDashboard.controllers.js +69 -55
- package/src/hbs/flag.hbs +1 -1
- package/src/hbs/template.hbs +7 -0
- package/src/hbs/visit-checklist.hbs +77 -93
- package/src/logging/activityLogFlusher.js +59 -0
- package/src/logging/activityLogMiddleware.js +45 -0
- package/src/logging/activityLogStore.js +91 -0
- package/src/logging/compressBatches.js +83 -0
- package/src/logging/config.js +24 -0
- package/src/logging/createLoggableService.js +46 -0
- package/src/logging/logExternalCall.js +37 -0
- package/src/routes/internalTraxApi.router.js +1 -0
- package/src/services/app.service.js +15 -9
- package/src/services/approver.service.js +23 -15
- package/src/services/authentication.service.js +9 -3
- package/src/services/camera.service.js +19 -13
- package/src/services/checklist.service.js +35 -27
- package/src/services/checklistAssign.service.js +43 -38
- package/src/services/checklistQuestion.service.js +39 -34
- package/src/services/checklistlog.service.js +39 -34
- package/src/services/clientRequest.service.js +9 -2
- package/src/services/clients.services.js +23 -18
- package/src/services/cluster.service.js +31 -23
- package/src/services/domain.service.js +23 -18
- package/src/services/download.services.js +35 -25
- package/src/services/group.service.js +23 -17
- package/src/services/lenskartEmployeeMapping.service.js +15 -10
- package/src/services/locus.service.js +35 -28
- package/src/services/notification.service.js +35 -26
- package/src/services/otp.service.js +20 -13
- package/src/services/planogram.service.js +9 -2
- package/src/services/processedTaskConfig.service.js +35 -27
- package/src/services/processedTaskList.service.js +32 -26
- package/src/services/processedchecklist.services.js +55 -47
- package/src/services/processedchecklistconfig.services.js +39 -34
- package/src/services/recurringFlagTracker.service.js +39 -32
- package/src/services/runAIFeatures.services.js +32 -27
- package/src/services/runAIRequest.services.js +43 -38
- package/src/services/store.service.js +32 -27
- package/src/services/tagging.service.js +9 -2
- package/src/services/taskConfig.service.js +35 -27
- package/src/services/teams.service.js +35 -24
- package/src/services/ticket.service.js +15 -10
- package/src/services/user.service.js +27 -20
- package/src/services/userAssignedstores.service.js +12 -5
- package/src/utils/visitChecklistPdf.utils.js +449 -21
|
@@ -146,6 +146,54 @@ function flattenImageRefs( arr ) {
|
|
|
146
146
|
.filter( Boolean );
|
|
147
147
|
}
|
|
148
148
|
|
|
149
|
+
/**
|
|
150
|
+
* Builds the ordered media list (reference → uploaded → validation) for a SINGLE
|
|
151
|
+
* answer entry, deduped within that entry. Used to render each answer's images
|
|
152
|
+
* inline next to its own text/label, instead of pooling every answer's images
|
|
153
|
+
* into one question-level grid (which decoupled labels from their images).
|
|
154
|
+
* @param {Object} ua a single entry produced by buildQuestionAnswerEntries
|
|
155
|
+
* @return {Array} list of { type, label, url, detectionStatus } for this entry, in render order
|
|
156
|
+
*/
|
|
157
|
+
function buildEntryMediaItems( ua = {} ) {
|
|
158
|
+
const items = [];
|
|
159
|
+
const seen = new Set();
|
|
160
|
+
|
|
161
|
+
const push = ( type, label, url, detectionStatus = '' ) => {
|
|
162
|
+
if ( !url || typeof url !== 'string' ) return;
|
|
163
|
+
if ( seen.has( url ) ) return;
|
|
164
|
+
seen.add( url );
|
|
165
|
+
items.push( { type, label, url, detectionStatus } );
|
|
166
|
+
};
|
|
167
|
+
|
|
168
|
+
// Each uploaded/validation image shows its OWN runAI verdict (per-image, never
|
|
169
|
+
// aggregated). References are not AI-checked, so they carry no badge.
|
|
170
|
+
const statusByUrl = buildAnswerImageStatus( ua );
|
|
171
|
+
const statusFor = ( url ) => statusByUrl.get( url ) || '';
|
|
172
|
+
|
|
173
|
+
if ( ua.hasReferenceImage ) {
|
|
174
|
+
if ( Array.isArray( ua.multiReferenceImage ) && ua.multiReferenceImage.length ) {
|
|
175
|
+
ua.multiReferenceImage.forEach( ( url ) => push( 'reference', 'Reference Image', url ) );
|
|
176
|
+
} else if ( ua.referenceImage ) {
|
|
177
|
+
push( 'reference', 'Reference Image', ua.referenceImage );
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
if ( ua.answerType === 'image' && ua.answer ) {
|
|
182
|
+
push( 'uploaded', 'Uploaded Image', ua.answer, statusFor( ua.answer ) );
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if ( ua.validation ) {
|
|
186
|
+
if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) {
|
|
187
|
+
push( 'validation', 'Validation Image', ua.validationAnswer, statusFor( ua.validationAnswer ) );
|
|
188
|
+
}
|
|
189
|
+
if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
|
|
190
|
+
ua.validationImage.forEach( ( url ) => push( 'validation', 'Validation Image', url, statusFor( url ) ) );
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
return items;
|
|
195
|
+
}
|
|
196
|
+
|
|
149
197
|
function buildQuestionAnswerEntries( question ) {
|
|
150
198
|
const rawUserAnswers = getSourceUserAnswers( question );
|
|
151
199
|
const uniqueUserAnswers = [];
|
|
@@ -180,21 +228,278 @@ function buildQuestionAnswerEntries( question ) {
|
|
|
180
228
|
}
|
|
181
229
|
}
|
|
182
230
|
const validationImage = flattenImageRefs( userAnswer?.validationImage );
|
|
183
|
-
|
|
184
|
-
|
|
231
|
+
// Videos captured alongside 'Capture Multiple Image with description' validation (no runAI; shown as links).
|
|
232
|
+
const validationVideo = flattenImageRefs( userAnswer?.validationVideo );
|
|
233
|
+
|
|
234
|
+
const referenceImage = userAnswer?.referenceImage || matchedAnswer?.referenceImage || '';
|
|
235
|
+
// Left column shows a reference image only for non-image/video, non-multipleImage answer types.
|
|
236
|
+
// When there is no reference image, uploaded/validation media is rendered on the left instead of the right.
|
|
237
|
+
const hasReferenceImage = question?.answerType !== 'image/video' &&
|
|
238
|
+
question?.answerType !== 'multipleImage' &&
|
|
239
|
+
Boolean( referenceImage || multiReferenceImage.length );
|
|
240
|
+
|
|
241
|
+
// runAI verdicts for this answer, in source order. Used to tag each uploaded /
|
|
242
|
+
// validation image with its OWN verdict (by answerImage filename, else by
|
|
243
|
+
// position) so multi-image answers don't all collapse to one aggregated status.
|
|
244
|
+
const isPerAnswerType = PER_ANSWER_DETECTION_TYPES.includes( question?.answerType );
|
|
245
|
+
const runAIValues = getRunAIMatchValues( userAnswer?.runAIData );
|
|
246
|
+
const imageStatus = buildImageStatusMap( userAnswer?.runAIData );
|
|
247
|
+
|
|
248
|
+
// Choice answers (yes/no, multiple choice, dropdown) take this answer's own
|
|
249
|
+
// verdict directly — never aggregated. Other types keep the aggregate only as
|
|
250
|
+
// a last-resort fallback.
|
|
251
|
+
const detectionStatus = isPerAnswerType ?
|
|
252
|
+
verdictFromValue( runAIValues[0] ) :
|
|
253
|
+
getDetectionStatus( userAnswer?.runAIData );
|
|
254
|
+
|
|
255
|
+
const entry = {
|
|
185
256
|
answer: userAnswer?.answer || '',
|
|
186
257
|
answerType: getMediaDisplayType( question?.answerType, userAnswer ),
|
|
187
|
-
referenceImage
|
|
258
|
+
referenceImage,
|
|
259
|
+
hasReferenceImage,
|
|
188
260
|
multiReferenceImage,
|
|
189
261
|
remarks: userAnswer?.remarks || '',
|
|
190
262
|
sopFlag: userAnswer?.sopFlag ?? matchedAnswer?.sopFlag ?? false,
|
|
263
|
+
detectionStatus,
|
|
264
|
+
imageStatus,
|
|
265
|
+
runAIValues,
|
|
191
266
|
validation,
|
|
192
267
|
validationType,
|
|
193
268
|
validationAnswer,
|
|
194
269
|
validationImage,
|
|
270
|
+
validationVideo,
|
|
195
271
|
validationDisplayType: getValidationDisplayType( validationType ),
|
|
196
272
|
};
|
|
273
|
+
|
|
274
|
+
// Text-label answers (e.g. "Attach the image of Cat") carry their own
|
|
275
|
+
// reference/validation images. Group those images with this entry so the
|
|
276
|
+
// label and its images render together, instead of pooling every answer's
|
|
277
|
+
// images into one question-level grid. Image/video answers keep flowing
|
|
278
|
+
// into the pooled grid (handled by buildQuestionMediaItems).
|
|
279
|
+
entry.answerMedia = entry.answerType === 'text' ? buildEntryMediaItems( entry ) : [];
|
|
280
|
+
|
|
281
|
+
// Choice answers with no media show their verdict next to the answer text;
|
|
282
|
+
// when they carry validation media the badge sits on that media instead.
|
|
283
|
+
entry.showDetectionStatus = isPerAnswerType && Boolean( detectionStatus ) && !entry.answerMedia.length;
|
|
284
|
+
|
|
285
|
+
return entry;
|
|
286
|
+
} );
|
|
287
|
+
}
|
|
288
|
+
|
|
289
|
+
|
|
290
|
+
/**
|
|
291
|
+
* Flattens a question's user answers into one ordered, deduped image list for the
|
|
292
|
+
* 2-per-row answer media grid. Order: reference images first, then uploaded images,
|
|
293
|
+
* then validation images. Videos are shown as links elsewhere, so only images are
|
|
294
|
+
* collected here. When a question has no reference image, the uploaded images simply
|
|
295
|
+
* fill the grid from the start (taking the reference slot).
|
|
296
|
+
* @param {Array} userAnswers entries produced by buildQuestionAnswerEntries
|
|
297
|
+
* @param {string} [forcedStatus] when set (image / image-video), every uploaded &
|
|
298
|
+
* validation image shows this aggregated verdict instead of its own per-image one
|
|
299
|
+
* @return {Array} list of { type, label, url, detectionStatus } media items in render order
|
|
300
|
+
*/
|
|
301
|
+
function buildQuestionMediaItems( userAnswers = [], forcedStatus = '' ) {
|
|
302
|
+
const items = [];
|
|
303
|
+
const seen = new Set();
|
|
304
|
+
|
|
305
|
+
const push = ( type, label, url, detectionStatus = '' ) => {
|
|
306
|
+
if ( !url || typeof url !== 'string' ) return;
|
|
307
|
+
if ( seen.has( url ) ) return;
|
|
308
|
+
seen.add( url );
|
|
309
|
+
items.push( { type, label, url, detectionStatus } );
|
|
310
|
+
};
|
|
311
|
+
|
|
312
|
+
// Entries that render their media inline with the answer (answerMedia) are
|
|
313
|
+
// excluded here so their images are not duplicated in the pooled grid.
|
|
314
|
+
const pool = userAnswers.filter( ( ua ) => !( ua.answerMedia && ua.answerMedia.length ) );
|
|
315
|
+
|
|
316
|
+
// Resolve each image's status per answer (per-image; forcedStatus aggregates
|
|
317
|
+
// image / image-video). Built per answer so positional matching stays aligned.
|
|
318
|
+
const statusByUrl = new Map();
|
|
319
|
+
pool.forEach( ( ua ) => {
|
|
320
|
+
buildAnswerImageStatus( ua, forcedStatus ).forEach( ( status, url ) => statusByUrl.set( url, status ) );
|
|
321
|
+
} );
|
|
322
|
+
const statusFor = ( url ) => statusByUrl.get( url ) || '';
|
|
323
|
+
|
|
324
|
+
// 1. Reference images first (deduped, so a single shared reference shows once).
|
|
325
|
+
// Skipped for image/video & multipleImage upload questions (hasReferenceImage
|
|
326
|
+
// is false), which have no comparison reference — only uploaded answers.
|
|
327
|
+
pool.forEach( ( ua ) => {
|
|
328
|
+
if ( !ua.hasReferenceImage ) return;
|
|
329
|
+
if ( Array.isArray( ua.multiReferenceImage ) && ua.multiReferenceImage.length ) {
|
|
330
|
+
ua.multiReferenceImage.forEach( ( url ) => push( 'reference', 'Reference Image', url ) );
|
|
331
|
+
} else if ( ua.referenceImage ) {
|
|
332
|
+
push( 'reference', 'Reference Image', ua.referenceImage );
|
|
333
|
+
}
|
|
334
|
+
} );
|
|
335
|
+
|
|
336
|
+
// 2. Uploaded images — tagged with each image's own detection status (from runAIData).
|
|
337
|
+
pool.forEach( ( ua ) => {
|
|
338
|
+
if ( ua.answerType === 'image' && ua.answer ) {
|
|
339
|
+
push( 'uploaded', 'Uploaded Image', ua.answer, statusFor( ua.answer ) );
|
|
340
|
+
}
|
|
341
|
+
} );
|
|
342
|
+
|
|
343
|
+
// 3. Validation images (single 'Capture Image' and multi 'Capture Multiple Image').
|
|
344
|
+
pool.forEach( ( ua ) => {
|
|
345
|
+
if ( !ua.validation ) return;
|
|
346
|
+
|
|
347
|
+
if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) {
|
|
348
|
+
push( 'validation', 'Validation Image', ua.validationAnswer, statusFor( ua.validationAnswer ) );
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
|
|
352
|
+
ua.validationImage.forEach( ( url ) => push( 'validation', 'Validation Image', url, statusFor( url ) ) );
|
|
353
|
+
}
|
|
354
|
+
} );
|
|
355
|
+
|
|
356
|
+
return items;
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
/**
|
|
361
|
+
* Extracts every 'Matched/Not Matched' value from a userAnswer's runAIData.
|
|
362
|
+
* Handles both shapes:
|
|
363
|
+
* - [ { answerImage, results: [ { featureName, value } ] } ] (per-image nested)
|
|
364
|
+
* - [ { featureName, value } ] (flat)
|
|
365
|
+
* @param {Array} runAIData
|
|
366
|
+
* @return {string[]} list of 'True' / 'False' values (in source order)
|
|
367
|
+
*/
|
|
368
|
+
function getRunAIMatchValues( runAIData ) {
|
|
369
|
+
if ( !Array.isArray( runAIData ) ) return [];
|
|
370
|
+
|
|
371
|
+
const values = [];
|
|
372
|
+
|
|
373
|
+
runAIData.forEach( ( item ) => {
|
|
374
|
+
if ( Array.isArray( item?.results ) ) {
|
|
375
|
+
item.results.forEach( ( res ) => {
|
|
376
|
+
if ( res?.featureName === 'Matched/Not Matched' ) values.push( res.value );
|
|
377
|
+
} );
|
|
378
|
+
} else if ( item?.featureName === 'Matched/Not Matched' ) {
|
|
379
|
+
values.push( item.value );
|
|
380
|
+
}
|
|
381
|
+
} );
|
|
382
|
+
|
|
383
|
+
return values;
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
|
|
387
|
+
// Answer types whose runAI verdict is aggregated across all uploaded images:
|
|
388
|
+
// if any image is not matched, the whole question reads 'Not Matched'. Every
|
|
389
|
+
// other type shows each image's own verdict.
|
|
390
|
+
const AGGREGATE_DETECTION_TYPES = [ 'image', 'image/video' ];
|
|
391
|
+
|
|
392
|
+
// Choice answers: each selected answer carries its OWN runAI verdict and shows it
|
|
393
|
+
// directly — never aggregated ('.every') across the question's other answers.
|
|
394
|
+
const PER_ANSWER_DETECTION_TYPES = [ 'yes/no', 'multiplechoicesingle', 'multiplechoicemultiple', 'dropdown' ];
|
|
395
|
+
|
|
396
|
+
|
|
397
|
+
/**
|
|
398
|
+
* Maps a single runAI value to a display status.
|
|
399
|
+
* @param {string|boolean} value
|
|
400
|
+
* @return {string} 'Matched' | 'Not Matched' | ''
|
|
401
|
+
*/
|
|
402
|
+
function verdictFromValue( value ) {
|
|
403
|
+
if ( value === 'True' || value === true ) return 'Matched';
|
|
404
|
+
if ( value === 'False' || value === false ) return 'Not Matched';
|
|
405
|
+
return '';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
|
|
409
|
+
/**
|
|
410
|
+
* The filename of an image URL/path, ignoring any query string. Used to match an
|
|
411
|
+
* uploaded/validation image against its runAIData entry (whose answerImage may
|
|
412
|
+
* carry a different prefix / signed-URL query), regardless of CDN resolution.
|
|
413
|
+
* @param {string} url
|
|
414
|
+
* @return {string}
|
|
415
|
+
*/
|
|
416
|
+
function imageBasename( url ) {
|
|
417
|
+
return String( url || '' ).split( '?' )[0].split( '/' ).pop();
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
|
|
421
|
+
/**
|
|
422
|
+
* Builds a per-image detection status map from the nested runAIData shape
|
|
423
|
+
* ([ { answerImage, results: [ { featureName, value } ] } ]), keyed by the
|
|
424
|
+
* answerImage's filename. The flat shape has no per-image key, so it yields an
|
|
425
|
+
* empty map and callers fall back to the answer-level status.
|
|
426
|
+
* @param {Array} runAIData
|
|
427
|
+
* @return {Object} { [filename]: 'Matched' | 'Not Matched' }
|
|
428
|
+
*/
|
|
429
|
+
function buildImageStatusMap( runAIData ) {
|
|
430
|
+
const map = {};
|
|
431
|
+
if ( !Array.isArray( runAIData ) ) return map;
|
|
432
|
+
|
|
433
|
+
runAIData.forEach( ( item ) => {
|
|
434
|
+
if ( !item || !item.answerImage ) return;
|
|
435
|
+
const results = Array.isArray( item.results ) ? item.results : [ item ];
|
|
436
|
+
const values = results
|
|
437
|
+
.filter( ( r ) => r?.featureName === 'Matched/Not Matched' )
|
|
438
|
+
.map( ( r ) => r.value );
|
|
439
|
+
if ( !values.length ) return;
|
|
440
|
+
const matched = values.every( ( v ) => v === 'True' || v === true );
|
|
441
|
+
map[imageBasename( item.answerImage )] = matched ? 'Matched' : 'Not Matched';
|
|
197
442
|
} );
|
|
443
|
+
|
|
444
|
+
return map;
|
|
445
|
+
}
|
|
446
|
+
|
|
447
|
+
|
|
448
|
+
/**
|
|
449
|
+
* Resolves the detection status for each checked image (uploaded + validation) of a
|
|
450
|
+
* single answer, returned as a Map of url → 'Matched' | 'Not Matched' | ''.
|
|
451
|
+
*
|
|
452
|
+
* Each image gets its OWN verdict — never aggregated across the answer's other images:
|
|
453
|
+
* 1. matched explicitly by its answerImage filename (nested runAIData), else
|
|
454
|
+
* 2. matched positionally — the Nth checked image takes the Nth runAI verdict
|
|
455
|
+
* (works for the flat runAIData shape and when filenames don't line up).
|
|
456
|
+
* For image / image-video, forcedStatus (the question-level aggregate) overrides all.
|
|
457
|
+
* @param {Object} ua a single entry produced by buildQuestionAnswerEntries
|
|
458
|
+
* @param {string} [forcedStatus] aggregated status applied to every image (image/image-video)
|
|
459
|
+
* @return {Map<string,string>} url → status
|
|
460
|
+
*/
|
|
461
|
+
function buildAnswerImageStatus( ua = {}, forcedStatus = '' ) {
|
|
462
|
+
const statusByUrl = new Map();
|
|
463
|
+
const byName = ua.imageStatus || {};
|
|
464
|
+
const values = Array.isArray( ua.runAIValues ) ? ua.runAIValues : [];
|
|
465
|
+
let idx = 0;
|
|
466
|
+
|
|
467
|
+
const assign = ( url ) => {
|
|
468
|
+
if ( !url || typeof url !== 'string' || statusByUrl.has( url ) ) return;
|
|
469
|
+
let status = forcedStatus;
|
|
470
|
+
if ( !status ) {
|
|
471
|
+
const key = imageBasename( url );
|
|
472
|
+
status = ( key in byName ) ? byName[key] : verdictFromValue( values[idx] );
|
|
473
|
+
idx++;
|
|
474
|
+
}
|
|
475
|
+
statusByUrl.set( url, status );
|
|
476
|
+
};
|
|
477
|
+
|
|
478
|
+
if ( ua.answerType === 'image' && ua.answer ) assign( ua.answer );
|
|
479
|
+
|
|
480
|
+
if ( ua.validation ) {
|
|
481
|
+
if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) assign( ua.validationAnswer );
|
|
482
|
+
if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
|
|
483
|
+
ua.validationImage.forEach( assign );
|
|
484
|
+
}
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
return statusByUrl;
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
|
|
491
|
+
/**
|
|
492
|
+
* Resolves a 'Matched' / 'Not Matched' detection status from runAIData. Handles both
|
|
493
|
+
* runAIData shapes (per-image nested results, and flat). Any non-match verdict makes
|
|
494
|
+
* the whole status 'Not Matched'. Returns '' when there is no runAI verdict to report.
|
|
495
|
+
* @param {Array} runAIData
|
|
496
|
+
* @return {string} 'Matched' | 'Not Matched' | ''
|
|
497
|
+
*/
|
|
498
|
+
function getDetectionStatus( runAIData ) {
|
|
499
|
+
const values = getRunAIMatchValues( runAIData );
|
|
500
|
+
if ( !values.length ) return '';
|
|
501
|
+
const allMatched = values.every( ( v ) => v === 'True' || v === true );
|
|
502
|
+
return allMatched ? 'Matched' : 'Not Matched';
|
|
198
503
|
}
|
|
199
504
|
|
|
200
505
|
|
|
@@ -219,6 +524,8 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
219
524
|
|
|
220
525
|
const flags = [];
|
|
221
526
|
|
|
527
|
+
let hasSopFlag = false;
|
|
528
|
+
|
|
222
529
|
|
|
223
530
|
( questionAnswer || [] ).forEach( ( section, sectionIdx ) => {
|
|
224
531
|
let sectionScore = 0;
|
|
@@ -240,6 +547,10 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
240
547
|
|
|
241
548
|
const ua = userAnswersWithRef[0];
|
|
242
549
|
|
|
550
|
+
if ( !hasSopFlag && userAnswersWithRef.some( ( entry ) => entry.sopFlag === true ) ) {
|
|
551
|
+
hasSopFlag = true;
|
|
552
|
+
}
|
|
553
|
+
|
|
243
554
|
|
|
244
555
|
const max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
|
|
245
556
|
|
|
@@ -247,14 +558,34 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
247
558
|
let score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
|
|
248
559
|
|
|
249
560
|
|
|
250
|
-
if ( q.answerType
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
561
|
+
if ( q.compliance && ( q.answerType === 'image' || q.answerType === 'image/video' ) ) {
|
|
562
|
+
// image/video questions mix image + video entries in userAnswer, so only score the images.
|
|
563
|
+
const imageAnswers = q.answerType === 'image/video' ?
|
|
564
|
+
( q.userAnswer || [] ).filter( ( ua ) => ua?.answerType === 'image' ) :
|
|
565
|
+
( q.userAnswer || [] );
|
|
566
|
+
|
|
567
|
+
const matchValues = imageAnswers.flatMap( ( ua ) => getRunAIMatchValues( ua?.runAIData ) );
|
|
568
|
+
|
|
569
|
+
if ( matchValues.length ) {
|
|
570
|
+
// All images must be matched ('True'); if any one is not, treat as not matched.
|
|
571
|
+
const allMatched = matchValues.every( ( value ) => value === 'True' );
|
|
572
|
+
score = allMatched ? ( q?.answers?.[0]?.matchedCount ?? 0 ) : ( q?.answers?.[0]?.notMatchedCount ?? 0 );
|
|
573
|
+
}
|
|
574
|
+
}
|
|
575
|
+
|
|
576
|
+
|
|
577
|
+
// image / image-video aggregate their runAI verdict across all uploaded images
|
|
578
|
+
// (any 'Not Matched' → whole question 'Not Matched'). Every other type keeps
|
|
579
|
+
// each image's own verdict, so this stays '' for them.
|
|
580
|
+
let aggregateDetectionStatus = '';
|
|
581
|
+
if ( AGGREGATE_DETECTION_TYPES.includes( q.answerType ) ) {
|
|
582
|
+
const aiAnswers = q.answerType === 'image/video' ?
|
|
583
|
+
( q.userAnswer || [] ).filter( ( a ) => a?.answerType === 'image' ) :
|
|
584
|
+
( q.userAnswer || [] );
|
|
585
|
+
const matchValues = aiAnswers.flatMap( ( a ) => getRunAIMatchValues( a?.runAIData ) );
|
|
586
|
+
if ( matchValues.length ) {
|
|
587
|
+
aggregateDetectionStatus = matchValues.every( ( v ) => v === 'True' || v === true ) ?
|
|
588
|
+
'Matched' : 'Not Matched';
|
|
258
589
|
}
|
|
259
590
|
}
|
|
260
591
|
|
|
@@ -316,6 +647,8 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
316
647
|
|
|
317
648
|
answerDisplay: isYes ? 'Yes' : ( isNo ? 'No' : ( answerText && answerText.startsWith( 'http' ) ? 'Image' : ( answerText || '—' ) ) ),
|
|
318
649
|
|
|
650
|
+
mediaItems: buildQuestionMediaItems( userAnswersWithRef, aggregateDetectionStatus ),
|
|
651
|
+
|
|
319
652
|
userAnswer: userAnswersWithRef.length ? userAnswersWithRef : [ {
|
|
320
653
|
answer: '',
|
|
321
654
|
answerType: 'text',
|
|
@@ -339,15 +672,15 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
339
672
|
|
|
340
673
|
sectionName: section.sectionName || `Section ${sectionIdx + 1}`,
|
|
341
674
|
|
|
342
|
-
targetScore: sectionMax,
|
|
675
|
+
targetScore: sectionMax ? sectionMax : '--',
|
|
343
676
|
|
|
344
|
-
actualScore: sectionScore,
|
|
677
|
+
actualScore: sectionMax ? sectionScore : '--',
|
|
345
678
|
|
|
346
679
|
questionsCount,
|
|
347
680
|
|
|
348
681
|
passedCount,
|
|
349
682
|
|
|
350
|
-
percentage: sectionMax > 0 ? Math.round( ( sectionScore / sectionMax ) * 100 ) :
|
|
683
|
+
percentage: sectionMax > 0 ? Math.round( ( sectionScore / sectionMax ) * 100 ) : '--',
|
|
351
684
|
|
|
352
685
|
} );
|
|
353
686
|
|
|
@@ -373,7 +706,7 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
|
|
|
373
706
|
const numQuestions = questionAnswer.reduce( ( sum, s ) => sum + ( s.questions?.length || 0 ), 0 );
|
|
374
707
|
|
|
375
708
|
|
|
376
|
-
return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions };
|
|
709
|
+
return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag };
|
|
377
710
|
}
|
|
378
711
|
|
|
379
712
|
|
|
@@ -395,7 +728,7 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
|
|
|
395
728
|
|
|
396
729
|
const {
|
|
397
730
|
|
|
398
|
-
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
|
|
731
|
+
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag,
|
|
399
732
|
|
|
400
733
|
} = mapSectionsFromQuestionAnswer( questionAnswer );
|
|
401
734
|
|
|
@@ -481,6 +814,9 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
|
|
|
481
814
|
|
|
482
815
|
let referenceId = doc.coverage == 'store' ? doc?.storeName : doc?.userName;
|
|
483
816
|
|
|
817
|
+
const userImage = doc.userImage || '';
|
|
818
|
+
const userSignature = doc.userSignature || '';
|
|
819
|
+
|
|
484
820
|
return {
|
|
485
821
|
|
|
486
822
|
|
|
@@ -506,12 +842,22 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
|
|
|
506
842
|
|
|
507
843
|
numFlags: typeof doc.questionFlag === 'number' ? doc.questionFlag : flags.length,
|
|
508
844
|
|
|
509
|
-
|
|
845
|
+
showFlags: hasSopFlag,
|
|
846
|
+
|
|
847
|
+
runAIFlag: typeof doc.runAIFlag === 'number' ? doc.runAIFlag : 0,
|
|
848
|
+
|
|
849
|
+
showRunAIFlag: ( typeof doc.runAIQuestionCount === 'number' ? doc.runAIQuestionCount : 0 ) > 0,
|
|
510
850
|
|
|
511
851
|
submittedBy: doc.userName || '--',
|
|
512
852
|
|
|
513
853
|
country: doc.country || '--',
|
|
514
854
|
|
|
855
|
+
userImage,
|
|
856
|
+
|
|
857
|
+
userSignature,
|
|
858
|
+
|
|
859
|
+
showUserVerification: Boolean( userImage || userSignature ),
|
|
860
|
+
|
|
515
861
|
hasCompliancePage,
|
|
516
862
|
|
|
517
863
|
detailPageStart,
|
|
@@ -567,7 +913,7 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
|
|
|
567
913
|
|
|
568
914
|
const {
|
|
569
915
|
|
|
570
|
-
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
|
|
916
|
+
totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag,
|
|
571
917
|
|
|
572
918
|
} = mapSectionsFromQuestionAnswer( questionAnswer );
|
|
573
919
|
|
|
@@ -622,6 +968,9 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
|
|
|
622
968
|
} );
|
|
623
969
|
}
|
|
624
970
|
|
|
971
|
+
const userImage = checklistAnswer?.userImage || checklistInfo?.userImage || '';
|
|
972
|
+
const userSignature = checklistAnswer?.userSignature || checklistInfo?.userSignature || '';
|
|
973
|
+
|
|
625
974
|
return {
|
|
626
975
|
|
|
627
976
|
brandLogo: brandInfo.brandLogo || '',
|
|
@@ -650,12 +999,22 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
|
|
|
650
999
|
|
|
651
1000
|
numFlags: checklistAnswer?.flagCount ?? flags.length,
|
|
652
1001
|
|
|
653
|
-
|
|
1002
|
+
showFlags: hasSopFlag,
|
|
1003
|
+
|
|
1004
|
+
runAIFlag: checklistAnswer?.runAIFlag ?? checklistInfo?.runAIFlag ?? checklistAnswer?.aiBreachedCount ?? 0,
|
|
1005
|
+
|
|
1006
|
+
showRunAIFlag: ( checklistInfo?.runAIQuestionCount ?? checklistAnswer?.runAIQuestionCount ?? 0 ) > 0,
|
|
654
1007
|
|
|
655
1008
|
submittedBy: checklistInfo?.submittedBy || storeProfile?.userName || '--',
|
|
656
1009
|
|
|
657
1010
|
country: storeProfile?.Country || '--',
|
|
658
1011
|
|
|
1012
|
+
userImage,
|
|
1013
|
+
|
|
1014
|
+
userSignature,
|
|
1015
|
+
|
|
1016
|
+
showUserVerification: Boolean( userImage || userSignature ),
|
|
1017
|
+
|
|
659
1018
|
hasCompliancePage,
|
|
660
1019
|
|
|
661
1020
|
detailPageStart,
|
|
@@ -782,6 +1141,11 @@ export function createImageCache() {
|
|
|
782
1141
|
}
|
|
783
1142
|
|
|
784
1143
|
|
|
1144
|
+
if ( resolvedData.userImage && resolvedData.userImage.startsWith( 'http' ) ) {
|
|
1145
|
+
urls.add( resolvedData.userImage );
|
|
1146
|
+
}
|
|
1147
|
+
|
|
1148
|
+
|
|
785
1149
|
const collectFromSection = ( section ) => {
|
|
786
1150
|
section.questions?.forEach( ( q ) => {
|
|
787
1151
|
if ( q.questionReferenceImage && q.questionReferenceImage.startsWith( 'http' ) ) urls.add( q.questionReferenceImage );
|
|
@@ -805,6 +1169,14 @@ export function createImageCache() {
|
|
|
805
1169
|
if ( typeof u === 'string' && u.startsWith( 'http' ) ) urls.add( u );
|
|
806
1170
|
} );
|
|
807
1171
|
}
|
|
1172
|
+
|
|
1173
|
+
ua.answerMedia?.forEach( ( m ) => {
|
|
1174
|
+
if ( typeof m.url === 'string' && m.url.startsWith( 'http' ) ) urls.add( m.url );
|
|
1175
|
+
} );
|
|
1176
|
+
} );
|
|
1177
|
+
|
|
1178
|
+
q.mediaItems?.forEach( ( m ) => {
|
|
1179
|
+
if ( typeof m.url === 'string' && m.url.startsWith( 'http' ) ) urls.add( m.url );
|
|
808
1180
|
} );
|
|
809
1181
|
} );
|
|
810
1182
|
};
|
|
@@ -842,6 +1214,11 @@ export function createImageCache() {
|
|
|
842
1214
|
}
|
|
843
1215
|
|
|
844
1216
|
|
|
1217
|
+
if ( resolvedData.userImage ) {
|
|
1218
|
+
resolvedData.userImage = cache.get( resolvedData.userImage ) || resolvedData.userImage;
|
|
1219
|
+
}
|
|
1220
|
+
|
|
1221
|
+
|
|
845
1222
|
const replaceInSection = ( section ) => {
|
|
846
1223
|
section.questions?.forEach( ( q ) => {
|
|
847
1224
|
if ( q.questionReferenceImage && cache.has( q.questionReferenceImage ) ) q.questionReferenceImage = cache.get( q.questionReferenceImage );
|
|
@@ -863,7 +1240,19 @@ export function createImageCache() {
|
|
|
863
1240
|
if ( ua.validationDisplayType === 'multiImage' && ua.validationImage?.length ) {
|
|
864
1241
|
ua.validationImage = ua.validationImage.map( ( u ) => ( cache.has( u ) ? cache.get( u ) : u ) );
|
|
865
1242
|
}
|
|
1243
|
+
|
|
1244
|
+
if ( ua.answerMedia?.length ) {
|
|
1245
|
+
ua.answerMedia.forEach( ( m ) => {
|
|
1246
|
+
if ( cache.has( m.url ) ) m.url = cache.get( m.url );
|
|
1247
|
+
} );
|
|
1248
|
+
}
|
|
866
1249
|
} );
|
|
1250
|
+
|
|
1251
|
+
if ( q.mediaItems?.length ) {
|
|
1252
|
+
q.mediaItems.forEach( ( m ) => {
|
|
1253
|
+
if ( cache.has( m.url ) ) m.url = cache.get( m.url );
|
|
1254
|
+
} );
|
|
1255
|
+
}
|
|
867
1256
|
} );
|
|
868
1257
|
};
|
|
869
1258
|
|
|
@@ -936,11 +1325,27 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
|
|
|
936
1325
|
ua.validationImage = ua.validationImage.map( ( ele ) => resolveUrl( ele ) );
|
|
937
1326
|
}
|
|
938
1327
|
|
|
1328
|
+
if ( ua?.validationVideo?.length ) {
|
|
1329
|
+
ua.validationVideo = ua.validationVideo.map( ( ele ) => resolveUrl( ele ) );
|
|
1330
|
+
}
|
|
1331
|
+
|
|
939
1332
|
|
|
940
1333
|
if ( ua.answer && ua.answerType !== 'text' ) {
|
|
941
1334
|
ua.answer = resolveUrl( ua.answer );
|
|
942
1335
|
}
|
|
1336
|
+
|
|
1337
|
+
if ( ua.answerMedia?.length ) {
|
|
1338
|
+
ua.answerMedia.forEach( ( m ) => {
|
|
1339
|
+
m.url = resolveUrl( m.url );
|
|
1340
|
+
} );
|
|
1341
|
+
}
|
|
943
1342
|
} );
|
|
1343
|
+
|
|
1344
|
+
if ( q.mediaItems?.length ) {
|
|
1345
|
+
q.mediaItems.forEach( ( m ) => {
|
|
1346
|
+
m.url = resolveUrl( m.url );
|
|
1347
|
+
} );
|
|
1348
|
+
}
|
|
944
1349
|
} );
|
|
945
1350
|
};
|
|
946
1351
|
|
|
@@ -954,11 +1359,14 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
|
|
|
954
1359
|
|
|
955
1360
|
resolvedData.sections?.forEach( resolveQuestionMedia );
|
|
956
1361
|
|
|
957
|
-
|
|
958
|
-
if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
|
|
1362
|
+
if ( resolvedData?.brandLogo && !resolvedData?.brandLogo?.startsWith( 'http' ) ) {
|
|
959
1363
|
resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
|
|
960
1364
|
}
|
|
961
1365
|
|
|
1366
|
+
if ( resolvedData?.userImage && !resolvedData.userImage.startsWith( 'http' ) && !resolvedData.userImage.startsWith( 'data:' ) ) {
|
|
1367
|
+
resolvedData.userImage = resolveUrl( resolvedData.userImage );
|
|
1368
|
+
}
|
|
1369
|
+
|
|
962
1370
|
|
|
963
1371
|
return resolvedData;
|
|
964
1372
|
}
|
|
@@ -1070,10 +1478,26 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
|
|
|
1070
1478
|
ua.validationImage = ua.validationImage.map( ( ele ) => resolveUrl( ele ) );
|
|
1071
1479
|
}
|
|
1072
1480
|
|
|
1481
|
+
if ( ua.validationVideo?.length ) {
|
|
1482
|
+
ua.validationVideo = ua.validationVideo.map( ( ele ) => resolveUrl( ele ) );
|
|
1483
|
+
}
|
|
1484
|
+
|
|
1073
1485
|
if ( ua.answer && ua.answerType !== 'text' ) {
|
|
1074
1486
|
ua.answer = resolveUrl( ua.answer );
|
|
1075
1487
|
}
|
|
1488
|
+
|
|
1489
|
+
if ( ua.answerMedia?.length ) {
|
|
1490
|
+
ua.answerMedia.forEach( ( m ) => {
|
|
1491
|
+
m.url = resolveUrl( m.url );
|
|
1492
|
+
} );
|
|
1493
|
+
}
|
|
1076
1494
|
} );
|
|
1495
|
+
|
|
1496
|
+
if ( q.mediaItems?.length ) {
|
|
1497
|
+
q.mediaItems.forEach( ( m ) => {
|
|
1498
|
+
m.url = resolveUrl( m.url );
|
|
1499
|
+
} );
|
|
1500
|
+
}
|
|
1077
1501
|
} );
|
|
1078
1502
|
};
|
|
1079
1503
|
|
|
@@ -1087,6 +1511,10 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
|
|
|
1087
1511
|
resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
|
|
1088
1512
|
}
|
|
1089
1513
|
|
|
1514
|
+
if ( resolvedData.userImage && !resolvedData.userImage.startsWith( 'http' ) && !resolvedData.userImage.startsWith( 'data:' ) ) {
|
|
1515
|
+
resolvedData.userImage = resolveUrl( resolvedData.userImage );
|
|
1516
|
+
}
|
|
1517
|
+
|
|
1090
1518
|
|
|
1091
1519
|
const html = template( resolvedData );
|
|
1092
1520
|
|