tango-app-api-trax 3.9.33 → 3.9.35

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.
Files changed (51) hide show
  1. package/index.js +2 -1
  2. package/package.json +1 -1
  3. package/src/controllers/internalTrax.controller.js +134 -90
  4. package/src/controllers/mobileTrax.controller.js +69 -29
  5. package/src/controllers/trax.controller.js +7 -1
  6. package/src/controllers/traxDashboard.controllers.js +63 -54
  7. package/src/hbs/flag.hbs +1 -1
  8. package/src/hbs/login-otp.hbs +943 -943
  9. package/src/hbs/template.hbs +7 -0
  10. package/src/hbs/visit-checklist.hbs +49 -87
  11. package/src/logging/activityLogFlusher.js +59 -0
  12. package/src/logging/activityLogMiddleware.js +45 -0
  13. package/src/logging/activityLogStore.js +91 -0
  14. package/src/logging/compressBatches.js +83 -0
  15. package/src/logging/config.js +24 -0
  16. package/src/logging/createLoggableService.js +46 -0
  17. package/src/logging/logExternalCall.js +37 -0
  18. package/src/services/app.service.js +15 -9
  19. package/src/services/approver.service.js +23 -15
  20. package/src/services/authentication.service.js +9 -3
  21. package/src/services/camera.service.js +19 -13
  22. package/src/services/checklist.service.js +35 -27
  23. package/src/services/checklistAssign.service.js +43 -38
  24. package/src/services/checklistQuestion.service.js +39 -34
  25. package/src/services/checklistlog.service.js +39 -34
  26. package/src/services/clientRequest.service.js +9 -2
  27. package/src/services/clients.services.js +23 -18
  28. package/src/services/cluster.service.js +31 -23
  29. package/src/services/domain.service.js +23 -18
  30. package/src/services/download.services.js +35 -25
  31. package/src/services/group.service.js +23 -17
  32. package/src/services/lenskartEmployeeMapping.service.js +15 -10
  33. package/src/services/locus.service.js +35 -28
  34. package/src/services/notification.service.js +35 -26
  35. package/src/services/otp.service.js +20 -13
  36. package/src/services/planogram.service.js +9 -2
  37. package/src/services/processedTaskConfig.service.js +35 -27
  38. package/src/services/processedTaskList.service.js +32 -26
  39. package/src/services/processedchecklist.services.js +55 -47
  40. package/src/services/processedchecklistconfig.services.js +39 -34
  41. package/src/services/recurringFlagTracker.service.js +39 -32
  42. package/src/services/runAIFeatures.services.js +32 -27
  43. package/src/services/runAIRequest.services.js +43 -38
  44. package/src/services/store.service.js +32 -27
  45. package/src/services/tagging.service.js +9 -2
  46. package/src/services/taskConfig.service.js +35 -27
  47. package/src/services/teams.service.js +35 -24
  48. package/src/services/ticket.service.js +15 -10
  49. package/src/services/user.service.js +27 -20
  50. package/src/services/userAssignedstores.service.js +12 -5
  51. package/src/utils/visitChecklistPdf.utils.js +192 -16
@@ -180,11 +180,21 @@ function buildQuestionAnswerEntries( question ) {
180
180
  }
181
181
  }
182
182
  const validationImage = flattenImageRefs( userAnswer?.validationImage );
183
+ // Videos captured alongside 'Capture Multiple Image with description' validation (no runAI; shown as links).
184
+ const validationVideo = flattenImageRefs( userAnswer?.validationVideo );
185
+
186
+ const referenceImage = userAnswer?.referenceImage || matchedAnswer?.referenceImage || '';
187
+ // Left column shows a reference image only for non-image/video, non-multipleImage answer types.
188
+ // When there is no reference image, uploaded/validation media is rendered on the left instead of the right.
189
+ const hasReferenceImage = question?.answerType !== 'image/video' &&
190
+ question?.answerType !== 'multipleImage' &&
191
+ Boolean( referenceImage || multiReferenceImage.length );
183
192
 
184
193
  return {
185
194
  answer: userAnswer?.answer || '',
186
195
  answerType: getMediaDisplayType( question?.answerType, userAnswer ),
187
- referenceImage: userAnswer?.referenceImage || matchedAnswer?.referenceImage || '',
196
+ referenceImage,
197
+ hasReferenceImage,
188
198
  multiReferenceImage,
189
199
  remarks: userAnswer?.remarks || '',
190
200
  sopFlag: userAnswer?.sopFlag ?? matchedAnswer?.sopFlag ?? false,
@@ -192,12 +202,93 @@ function buildQuestionAnswerEntries( question ) {
192
202
  validationType,
193
203
  validationAnswer,
194
204
  validationImage,
205
+ validationVideo,
195
206
  validationDisplayType: getValidationDisplayType( validationType ),
196
207
  };
197
208
  } );
198
209
  }
199
210
 
200
211
 
212
+ /**
213
+ * Flattens a question's user answers into one ordered, deduped image list for the
214
+ * 2-per-row answer media grid. Order: reference images first, then uploaded images,
215
+ * then validation images. Videos are shown as links elsewhere, so only images are
216
+ * collected here. When a question has no reference image, the uploaded images simply
217
+ * fill the grid from the start (taking the reference slot).
218
+ * @param {Array} userAnswers entries produced by buildQuestionAnswerEntries
219
+ * @return {Array} list of { type, label, url } media items in render order
220
+ */
221
+ function buildQuestionMediaItems( userAnswers = [] ) {
222
+ const items = [];
223
+ const seen = new Set();
224
+
225
+ const push = ( type, label, url ) => {
226
+ if ( !url || typeof url !== 'string' ) return;
227
+ if ( seen.has( url ) ) return;
228
+ seen.add( url );
229
+ items.push( { type, label, url } );
230
+ };
231
+
232
+ // 1. Reference images first (deduped, so a single shared reference shows once).
233
+ userAnswers.forEach( ( ua ) => {
234
+ if ( Array.isArray( ua.multiReferenceImage ) && ua.multiReferenceImage.length ) {
235
+ ua.multiReferenceImage.forEach( ( url ) => push( 'reference', 'Reference Image', url ) );
236
+ } else if ( ua.referenceImage ) {
237
+ push( 'reference', 'Reference Image', ua.referenceImage );
238
+ }
239
+ } );
240
+
241
+ // 2. Uploaded images.
242
+ userAnswers.forEach( ( ua ) => {
243
+ if ( ua.answerType === 'image' && ua.answer ) {
244
+ push( 'uploaded', 'Uploaded Image', ua.answer );
245
+ }
246
+ } );
247
+
248
+ // 3. Validation images (single 'Capture Image' and multi 'Capture Multiple Image').
249
+ userAnswers.forEach( ( ua ) => {
250
+ if ( !ua.validation ) return;
251
+
252
+ if ( ua.validationDisplayType === 'image' && ua.validationAnswer ) {
253
+ push( 'validation', 'Validation Image', ua.validationAnswer );
254
+ }
255
+
256
+ if ( ua.validationDisplayType === 'multiImage' && Array.isArray( ua.validationImage ) ) {
257
+ ua.validationImage.forEach( ( url ) => push( 'validation', 'Validation Image', url ) );
258
+ }
259
+ } );
260
+
261
+ return items;
262
+ }
263
+
264
+
265
+ /**
266
+ * Extracts every 'Matched/Not Matched' value from a userAnswer's runAIData.
267
+ * Handles both shapes:
268
+ * - [ { answerImage, results: [ { featureName, value } ] } ] (per-image nested)
269
+ * - [ { featureName, value } ] (flat)
270
+ * @param {Array} runAIData
271
+ * @return {string[]} list of 'True' / 'False' values (in source order)
272
+ */
273
+ function getRunAIMatchValues( runAIData ) {
274
+ if ( !Array.isArray( runAIData ) ) return [];
275
+
276
+ const values = [];
277
+
278
+ runAIData.forEach( ( item ) => {
279
+ if ( Array.isArray( item?.results ) ) {
280
+ item.results.forEach( ( res ) => {
281
+ if ( res?.featureName === 'Matched/Not Matched' ) values.push( res.value );
282
+ } );
283
+ } else if ( item?.featureName === 'Matched/Not Matched' ) {
284
+ values.push( item.value );
285
+ }
286
+ } );
287
+
288
+ return values;
289
+ }
290
+
291
+
201
292
  /**
202
293
 
203
294
  * Map section array { sectionName, questions[] } (view API or processed checklist) to scores / template sections.
@@ -219,6 +310,8 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
219
310
 
220
311
  const flags = [];
221
312
 
313
+ let hasSopFlag = false;
314
+
222
315
 
223
316
  ( questionAnswer || [] ).forEach( ( section, sectionIdx ) => {
224
317
  let sectionScore = 0;
@@ -240,6 +333,10 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
240
333
 
241
334
  const ua = userAnswersWithRef[0];
242
335
 
336
+ if ( !hasSopFlag && userAnswersWithRef.some( ( entry ) => entry.sopFlag === true ) ) {
337
+ hasSopFlag = true;
338
+ }
339
+
243
340
 
244
341
  const max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
245
342
 
@@ -247,14 +344,18 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
247
344
  let score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
248
345
 
249
346
 
250
- if ( q.answerType == 'image' && q.compliance && q.userAnswer?.[0]?.runAIData ) {
251
- let find = q.userAnswer?.[0]?.runAIData?.find( ( run ) => run?.featureName == 'Matched/Not Matched' );
252
- if ( find ) {
253
- if ( find?.value == 'True' ) {
254
- score = q?.answers?.[0]?.matchedCount;
255
- } else {
256
- score = q?.answers?.[0]?.notMatchedCount;
257
- }
347
+ if ( q.compliance && ( q.answerType === 'image' || q.answerType === 'image/video' ) ) {
348
+ // image/video questions mix image + video entries in userAnswer, so only score the images.
349
+ const imageAnswers = q.answerType === 'image/video' ?
350
+ ( q.userAnswer || [] ).filter( ( ua ) => ua?.answerType === 'image' ) :
351
+ ( q.userAnswer || [] );
352
+
353
+ const matchValues = imageAnswers.flatMap( ( ua ) => getRunAIMatchValues( ua?.runAIData ) );
354
+
355
+ if ( matchValues.length ) {
356
+ // All images must be matched ('True'); if any one is not, treat as not matched.
357
+ const allMatched = matchValues.every( ( value ) => value === 'True' );
358
+ score = allMatched ? ( q?.answers?.[0]?.matchedCount ?? 0 ) : ( q?.answers?.[0]?.notMatchedCount ?? 0 );
258
359
  }
259
360
  }
260
361
 
@@ -316,6 +417,8 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
316
417
 
317
418
  answerDisplay: isYes ? 'Yes' : ( isNo ? 'No' : ( answerText && answerText.startsWith( 'http' ) ? 'Image' : ( answerText || '—' ) ) ),
318
419
 
420
+ mediaItems: buildQuestionMediaItems( userAnswersWithRef ),
421
+
319
422
  userAnswer: userAnswersWithRef.length ? userAnswersWithRef : [ {
320
423
  answer: '',
321
424
  answerType: 'text',
@@ -373,7 +476,7 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
373
476
  const numQuestions = questionAnswer.reduce( ( sum, s ) => sum + ( s.questions?.length || 0 ), 0 );
374
477
 
375
478
 
376
- return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions };
479
+ return { totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag };
377
480
  }
378
481
 
379
482
 
@@ -395,7 +498,7 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
395
498
 
396
499
  const {
397
500
 
398
- totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
501
+ totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag,
399
502
 
400
503
  } = mapSectionsFromQuestionAnswer( questionAnswer );
401
504
 
@@ -481,6 +584,9 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
481
584
 
482
585
  let referenceId = doc.coverage == 'store' ? doc?.storeName : doc?.userName;
483
586
 
587
+ const userImage = doc.userImage || '';
588
+ const userSignature = doc.userSignature || '';
589
+
484
590
  return {
485
591
 
486
592
 
@@ -506,12 +612,22 @@ export function buildVisitChecklistTemplateDataFromProcessed( processedDoc, bran
506
612
 
507
613
  numFlags: typeof doc.questionFlag === 'number' ? doc.questionFlag : flags.length,
508
614
 
509
- aiBreached: typeof doc.runAIFlag === 'number' ? doc.runAIFlag : 0,
615
+ showFlags: hasSopFlag,
616
+
617
+ runAIFlag: typeof doc.runAIFlag === 'number' ? doc.runAIFlag : 0,
618
+
619
+ showRunAIFlag: ( typeof doc.runAIQuestionCount === 'number' ? doc.runAIQuestionCount : 0 ) > 0,
510
620
 
511
621
  submittedBy: doc.userName || '--',
512
622
 
513
623
  country: doc.country || '--',
514
624
 
625
+ userImage,
626
+
627
+ userSignature,
628
+
629
+ showUserVerification: Boolean( userImage || userSignature ),
630
+
515
631
  hasCompliancePage,
516
632
 
517
633
  detailPageStart,
@@ -567,7 +683,7 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
567
683
 
568
684
  const {
569
685
 
570
- totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions,
686
+ totalScore, maxScore, sectionInsights, questionAnswers, flags, numQuestions, hasSopFlag,
571
687
 
572
688
  } = mapSectionsFromQuestionAnswer( questionAnswer );
573
689
 
@@ -622,6 +738,9 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
622
738
  } );
623
739
  }
624
740
 
741
+ const userImage = checklistAnswer?.userImage || checklistInfo?.userImage || '';
742
+ const userSignature = checklistAnswer?.userSignature || checklistInfo?.userSignature || '';
743
+
625
744
  return {
626
745
 
627
746
  brandLogo: brandInfo.brandLogo || '',
@@ -650,12 +769,22 @@ function buildFromViewChecklistApi( getchecklistData, viewchecklistData, brandIn
650
769
 
651
770
  numFlags: checklistAnswer?.flagCount ?? flags.length,
652
771
 
653
- aiBreached: checklistAnswer?.aiBreachedCount ?? 0,
772
+ showFlags: hasSopFlag,
773
+
774
+ runAIFlag: checklistAnswer?.runAIFlag ?? checklistInfo?.runAIFlag ?? checklistAnswer?.aiBreachedCount ?? 0,
775
+
776
+ showRunAIFlag: ( checklistInfo?.runAIQuestionCount ?? checklistAnswer?.runAIQuestionCount ?? 0 ) > 0,
654
777
 
655
778
  submittedBy: checklistInfo?.submittedBy || storeProfile?.userName || '--',
656
779
 
657
780
  country: storeProfile?.Country || '--',
658
781
 
782
+ userImage,
783
+
784
+ userSignature,
785
+
786
+ showUserVerification: Boolean( userImage || userSignature ),
787
+
659
788
  hasCompliancePage,
660
789
 
661
790
  detailPageStart,
@@ -782,6 +911,11 @@ export function createImageCache() {
782
911
  }
783
912
 
784
913
 
914
+ if ( resolvedData.userImage && resolvedData.userImage.startsWith( 'http' ) ) {
915
+ urls.add( resolvedData.userImage );
916
+ }
917
+
918
+
785
919
  const collectFromSection = ( section ) => {
786
920
  section.questions?.forEach( ( q ) => {
787
921
  if ( q.questionReferenceImage && q.questionReferenceImage.startsWith( 'http' ) ) urls.add( q.questionReferenceImage );
@@ -806,6 +940,10 @@ export function createImageCache() {
806
940
  } );
807
941
  }
808
942
  } );
943
+
944
+ q.mediaItems?.forEach( ( m ) => {
945
+ if ( typeof m.url === 'string' && m.url.startsWith( 'http' ) ) urls.add( m.url );
946
+ } );
809
947
  } );
810
948
  };
811
949
 
@@ -842,6 +980,11 @@ export function createImageCache() {
842
980
  }
843
981
 
844
982
 
983
+ if ( resolvedData.userImage ) {
984
+ resolvedData.userImage = cache.get( resolvedData.userImage ) || resolvedData.userImage;
985
+ }
986
+
987
+
845
988
  const replaceInSection = ( section ) => {
846
989
  section.questions?.forEach( ( q ) => {
847
990
  if ( q.questionReferenceImage && cache.has( q.questionReferenceImage ) ) q.questionReferenceImage = cache.get( q.questionReferenceImage );
@@ -864,6 +1007,12 @@ export function createImageCache() {
864
1007
  ua.validationImage = ua.validationImage.map( ( u ) => ( cache.has( u ) ? cache.get( u ) : u ) );
865
1008
  }
866
1009
  } );
1010
+
1011
+ if ( q.mediaItems?.length ) {
1012
+ q.mediaItems.forEach( ( m ) => {
1013
+ if ( cache.has( m.url ) ) m.url = cache.get( m.url );
1014
+ } );
1015
+ }
867
1016
  } );
868
1017
  };
869
1018
 
@@ -936,11 +1085,21 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
936
1085
  ua.validationImage = ua.validationImage.map( ( ele ) => resolveUrl( ele ) );
937
1086
  }
938
1087
 
1088
+ if ( ua?.validationVideo?.length ) {
1089
+ ua.validationVideo = ua.validationVideo.map( ( ele ) => resolveUrl( ele ) );
1090
+ }
1091
+
939
1092
 
940
1093
  if ( ua.answer && ua.answerType !== 'text' ) {
941
1094
  ua.answer = resolveUrl( ua.answer );
942
1095
  }
943
1096
  } );
1097
+
1098
+ if ( q.mediaItems?.length ) {
1099
+ q.mediaItems.forEach( ( m ) => {
1100
+ m.url = resolveUrl( m.url );
1101
+ } );
1102
+ }
944
1103
  } );
945
1104
  };
946
1105
 
@@ -954,11 +1113,14 @@ export function resolveTemplateUrls( templateData, baseUrl = 'https://d1r0hc2ssk
954
1113
 
955
1114
  resolvedData.sections?.forEach( resolveQuestionMedia );
956
1115
 
957
-
958
- if ( resolvedData.brandLogo && !resolvedData.brandLogo.startsWith( 'http' ) ) {
1116
+ if ( resolvedData?.brandLogo && !resolvedData?.brandLogo?.startsWith( 'http' ) ) {
959
1117
  resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
960
1118
  }
961
1119
 
1120
+ if ( resolvedData?.userImage && !resolvedData.userImage.startsWith( 'http' ) && !resolvedData.userImage.startsWith( 'data:' ) ) {
1121
+ resolvedData.userImage = resolveUrl( resolvedData.userImage );
1122
+ }
1123
+
962
1124
 
963
1125
  return resolvedData;
964
1126
  }
@@ -1070,10 +1232,20 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
1070
1232
  ua.validationImage = ua.validationImage.map( ( ele ) => resolveUrl( ele ) );
1071
1233
  }
1072
1234
 
1235
+ if ( ua.validationVideo?.length ) {
1236
+ ua.validationVideo = ua.validationVideo.map( ( ele ) => resolveUrl( ele ) );
1237
+ }
1238
+
1073
1239
  if ( ua.answer && ua.answerType !== 'text' ) {
1074
1240
  ua.answer = resolveUrl( ua.answer );
1075
1241
  }
1076
1242
  } );
1243
+
1244
+ if ( q.mediaItems?.length ) {
1245
+ q.mediaItems.forEach( ( m ) => {
1246
+ m.url = resolveUrl( m.url );
1247
+ } );
1248
+ }
1077
1249
  } );
1078
1250
  };
1079
1251
 
@@ -1087,6 +1259,10 @@ export async function generateVisitChecklistPDF( templateData, baseUrl = 'https:
1087
1259
  resolvedData.brandLogo = resolveUrl( resolvedData.brandLogo );
1088
1260
  }
1089
1261
 
1262
+ if ( resolvedData.userImage && !resolvedData.userImage.startsWith( 'http' ) && !resolvedData.userImage.startsWith( 'data:' ) ) {
1263
+ resolvedData.userImage = resolveUrl( resolvedData.userImage );
1264
+ }
1265
+
1090
1266
 
1091
1267
  const html = template( resolvedData );
1092
1268