tango-app-api-trax 3.9.53 → 3.9.55

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-trax",
3
- "version": "3.9.53",
3
+ "version": "3.9.55",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -30,7 +30,7 @@
30
30
  "path": "^0.12.7",
31
31
  "puppeteer": "^24.39.1",
32
32
  "swagger-ui-express": "^5.0.1",
33
- "tango-api-schema": "^2.6.23",
33
+ "tango-api-schema": "^2.6.30",
34
34
  "tango-app-api-middleware": "^3.5.2",
35
35
  "url": "^0.11.4",
36
36
  "winston": "^3.13.1",
@@ -2141,6 +2141,7 @@ export async function sopMobilechecklistMultiSectionFormatterv2( req, res, next
2141
2141
 
2142
2142
  if ( requestData.submittype == 'submit' ) {
2143
2143
  requestData.userComplianceCount = 0;
2144
+ requestData.complianceCount = 0;
2144
2145
  for ( let section of sectionFormat ) {
2145
2146
  for ( let question of section.questions ) {
2146
2147
  let mustValidate = !question.linkType || ( question.linkType && question.linkquestionenabled );
@@ -2148,9 +2149,27 @@ export async function sopMobilechecklistMultiSectionFormatterv2( req, res, next
2148
2149
  return res.sendError( 'Please Fill All Fields', 400 );
2149
2150
  }
2150
2151
  if ( question.compliance ) {
2152
+ // score = user's achieved value; max = best possible for the question
2153
+ // (same max formula as config-time complianceCount in trax.controller).
2151
2154
  const scores = ( question.userAnswer || [] ).map( ( ua ) => ua?.complianceScore ?? 0 );
2152
2155
  let score = scores.length ? Math.max( ...scores ) : 0;
2156
+
2157
+ const answerScores = ( question.answers || [] ).map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) );
2158
+ let max = answerScores.length ? Math.max( ...answerScores ) : 0;
2159
+
2160
+ // Single-choice questions answered 'NA' / 'N/A' (case-insensitive) are
2161
+ // excluded from compliance entirely (score AND max = 0), matching the
2162
+ // visit-checklist PDF and dashboard compliance calculation.
2163
+ if ( question.answerType === 'multiplechoicesingle' ) {
2164
+ const naAnswer = ( question.userAnswer?.[0]?.answer || '' ).toString().trim().toLowerCase();
2165
+ if ( naAnswer === 'na' || naAnswer === 'n/a' ) {
2166
+ score = 0;
2167
+ max = 0;
2168
+ }
2169
+ }
2170
+
2153
2171
  requestData.userComplianceCount += score;
2172
+ requestData.complianceCount += max;
2154
2173
  }
2155
2174
  }
2156
2175
  }
@@ -2796,6 +2815,8 @@ export async function submitChecklist( req, res ) {
2796
2815
  updateData.updatedAt = dayjs.utc( currentDateTime.format( 'hh:mm:ss A, DD MMM YYYY' ), 'hh:mm:ss A, DD MMM YYYY' ).format();
2797
2816
  updateData.submitMobileTime = requestData?.currentTime;
2798
2817
  updateData.userComplianceCount = requestData?.userComplianceCount;
2818
+ // NA-aware max (denominator) recomputed at submit, alongside the score above.
2819
+ updateData.complianceCount = requestData?.complianceCount;
2799
2820
  updateData.userSignature = requestData?.userSignature;
2800
2821
  if ( requestData?.userImage ) {
2801
2822
  let splitImgUrl = requestData?.userImage.split( '/' );
@@ -297,15 +297,21 @@ export const checklistPerformance = async ( req, res ) => {
297
297
 
298
298
  findQuery.push( { $match: { $and: findAndQuery } } );
299
299
 
300
+ // Same search expressed as a plain find condition, so the parallel compliance
301
+ // query (below) stays scoped to the same checklists without depending on the
302
+ // aggregation's result.
303
+ let complianceSearch = null;
300
304
  if ( requestData.searchValue && requestData.searchValue != '' ) {
301
305
  let checkListSearch = requestData.searchValue.split( ',' ).map( ( item ) => item.trim().toLowerCase() );
302
306
  let query;
303
307
  if ( checkListSearch.length > 1 ) {
304
308
  findQuery.push( { $addFields: { cheklistlowercase: { $toLower: '$checkListName' } } } );
305
309
  query = { cheklistlowercase: { $in: checkListSearch } };
310
+ complianceSearch = { $expr: { $in: [ { $toLower: '$checkListName' }, checkListSearch ] } };
306
311
  } else {
307
312
  const safeSearch = escapeRegex( req.body?.searchValue );
308
313
  query = { checkListName: { $regex: safeSearch, $options: 'i' } };
314
+ complianceSearch = { checkListName: { $regex: safeSearch, $options: 'i' } };
309
315
  }
310
316
  findQuery.push( { $match: { $or: [ query ] } } );
311
317
  }
@@ -344,16 +350,6 @@ export const checklistPerformance = async ( req, res ) => {
344
350
  timeFlag: { $sum: '$timeFlag' },
345
351
  questionFlagCount: { $sum: '$questionFlag' },
346
352
  runAIFlagCount: { $sum: '$runAIFlag' },
347
- userComplianceCountTotal: {
348
- $sum: {
349
- $cond: [
350
- { $eq: [ '$checklistStatus', 'submit' ] },
351
- { $ifNull: [ '$userComplianceCount', 0 ] },
352
- 0,
353
- ],
354
- },
355
- },
356
- complianceCount: { $first: '$complianceCount' },
357
353
  checkListType: { $last: '$checkListType' },
358
354
  redo: { $sum: { $cond: [ { $eq: [ '$redoStatus', true ] }, 1, 0 ] } },
359
355
  task: {
@@ -413,84 +409,104 @@ export const checklistPerformance = async ( req, res ) => {
413
409
  checkListType: 1,
414
410
  redo: 1,
415
411
  task: 1,
416
- questionCompliance: {
417
- $let: {
418
- vars: {
419
- divisor: {
420
- $cond: [
421
- { $gt: [ '$submittedChecklist', 1 ] },
422
- { $multiply: [ { $ifNull: [ '$complianceCount', 0 ] }, '$submittedChecklist' ] },
423
- { $ifNull: [ '$complianceCount', 0 ] },
424
- ],
425
- },
426
- },
427
- in: {
428
- $cond: [
429
- { $gt: [ '$$divisor', 0 ] },
430
- {
431
- $max: [
432
- 0,
433
- {
434
- $round: [
435
- {
436
- $multiply: [
437
- { $divide: [ '$userComplianceCountTotal', '$$divisor' ] },
438
- 100,
439
- ],
440
- },
441
- 0,
442
- ],
443
- },
444
- ],
445
- },
446
- 0,
447
- ],
448
- },
449
- },
450
- },
451
412
  },
452
413
  } );
453
414
 
454
- if ( requestData.sortColumnName && requestData.sortColumnName != '' && requestData.sortBy && requestData.sortBy !='' ) {
455
- findQuery.push( { $sort: { [requestData.sortColumnName]: requestData.sortBy } } );
456
- } else {
457
- findQuery.push( { $sort: { ['submittedChecklist']: -1 } } );
458
- }
415
+ // Compliance % is computed in JS (not in the pipeline). The grouped aggregation
416
+ // and the compliance fetch are independent (the compliance query is scoped by the
417
+ // same base filter + search, NOT by the aggregation's result), so we run them in
418
+ // parallel. We then reduce the compliance docs per checklist in JS (NA-aware,
419
+ // matching the visit-checklist PDF) and sort + paginate in JS — because the table
420
+ // can sort by the compliance column, which no longer exists in the pipeline.
421
+ const complianceFilter = {
422
+ $and: [
423
+ ...findAndQuery,
424
+ { checklistStatus: 'submit' },
425
+ ...( complianceSearch ? [ complianceSearch ] : [] ),
426
+ ],
427
+ };
459
428
 
460
- findQuery.push( {
461
- $facet: {
462
- data: [
463
- { $skip: skip }, { $limit: limit },
464
- ],
465
- count: [
466
- { $count: 'total' },
467
- ],
468
- },
469
- } );
470
- let getChecklistPerformanceData = await processedchecklistService.aggregate( findQuery );
471
- if ( !getChecklistPerformanceData?.[0]?.data?.length ) {
429
+ const [ getChecklistPerformanceData, complianceDocs ] = await Promise.all( [
430
+ processedchecklistService.aggregate( findQuery ),
431
+ // Only the compliance-relevant subfields, so we transfer far less than the
432
+ // full questionAnswers blob.
433
+ processedchecklistService.find( complianceFilter, {
434
+ 'sourceCheckList_id': 1,
435
+ 'questionAnswers.questions.compliance': 1,
436
+ 'questionAnswers.questions.answerType': 1,
437
+ 'questionAnswers.questions.answers.complianceScore': 1,
438
+ 'questionAnswers.questions.answers.matchedCount': 1,
439
+ 'questionAnswers.questions.answers.notMatchedCount': 1,
440
+ 'questionAnswers.questions.userAnswer.complianceScore': 1,
441
+ 'questionAnswers.questions.userAnswer.answer': 1,
442
+ } ).lean(),
443
+ ] );
444
+
445
+ if ( !getChecklistPerformanceData.length ) {
472
446
  return res.sendError( 'no data found', 204 );
473
447
  }
474
448
 
449
+ // Sum NA-aware { score, max } per checklist.
450
+ const complianceByChecklist = {};
451
+ for ( const doc of complianceDocs ) {
452
+ const id = doc.sourceCheckList_id?.toString();
453
+ if ( !id ) continue;
454
+ if ( !complianceByChecklist[id] ) complianceByChecklist[id] = { score: 0, max: 0 };
455
+
456
+ for ( const section of ( doc.questionAnswers || [] ) ) {
457
+ for ( const q of ( section.questions || [] ) ) {
458
+ if ( !q?.compliance ) continue;
459
+
460
+ // Single-choice answered NA/N/A (case-insensitive) is excluded from both.
461
+ if ( q.answerType === 'multiplechoicesingle' ) {
462
+ const ans = ( q.userAnswer?.[0]?.answer || '' ).toString().trim().toLowerCase();
463
+ if ( ans === 'na' || ans === 'n/a' ) continue;
464
+ }
465
+
466
+ const max = Math.max( 0, ...( q.answers || [] ).map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) );
467
+ const score = Math.max( 0, ...( q.userAnswer || [] ).map( ( o ) => o?.complianceScore ?? 0 ) );
468
+
469
+ complianceByChecklist[id].score += score;
470
+ complianceByChecklist[id].max += max;
471
+ }
472
+ }
473
+ }
474
+
475
+ getChecklistPerformanceData.forEach( ( row ) => {
476
+ const c = complianceByChecklist[row.sourceCheckList_id?.toString()] || { score: 0, max: 0 };
477
+ row.questionCompliance = c.max > 0 ? Math.max( 0, Math.round( ( c.score / c.max ) * 100 ) ) : 0;
478
+ } );
479
+
480
+ // Sort in JS (column may be the JS-computed questionCompliance), then paginate.
481
+ const sortColumn = ( requestData.sortColumnName && requestData.sortColumnName != '' ) ? requestData.sortColumnName : 'submittedChecklist';
482
+ const sortDir = Number( requestData.sortBy ) === 1 ? 1 : -1;
483
+ getChecklistPerformanceData.sort( ( a, b ) => {
484
+ const av = a[sortColumn];
485
+ const bv = b[sortColumn];
486
+ if ( typeof av === 'string' || typeof bv === 'string' ) {
487
+ return ( av ?? '' ).toString().localeCompare( ( bv ?? '' ).toString() ) * sortDir;
488
+ }
489
+ return ( ( av ?? 0 ) - ( bv ?? 0 ) ) * sortDir;
490
+ } );
491
+
492
+ const pageRows = getChecklistPerformanceData.slice( skip, skip + limit );
493
+
475
494
  if ( requestData.export ) {
476
- const exportdata = [];
477
- getChecklistPerformanceData[0].data.forEach( ( element ) => {
478
- exportdata.push( {
479
- 'Checklist Name': element.checkListName || '--',
480
- 'Coverage': element.coverage || '--',
481
- 'Scheduled': element.scheduleRepeatedType || '--',
482
- 'Assigned To': element.storeCount || '--',
483
- 'Submitted': element.submittedChecklist || '--',
484
- 'Flags': element.flaggedChecklist || '--',
485
- 'Tasks': element.task || '--',
486
- 'ReDo': element.redo || '--',
487
- } );
488
- } );
495
+ const exportdata = pageRows.map( ( element ) => ( {
496
+ 'Checklist Name': element.checkListName || '--',
497
+ 'Coverage': element.coverage || '--',
498
+ 'Scheduled': element.scheduleRepeatedType || '--',
499
+ 'Assigned To': element.storeCount || '--',
500
+ 'Submitted': element.submittedChecklist || '--',
501
+ 'Flags': element.flaggedChecklist || '--',
502
+ 'Tasks': element.task || '--',
503
+ 'ReDo': element.redo || '--',
504
+ } ) );
489
505
  return await download( exportdata, res );
490
506
  }
491
507
 
492
- result.totalCount = getChecklistPerformanceData[0].count[0].total;
493
- result.checklistPerformance = getChecklistPerformanceData[0].data;
508
+ result.totalCount = getChecklistPerformanceData.length;
509
+ result.checklistPerformance = pageRows;
494
510
  return res.sendSuccess( result );
495
511
  } catch ( error ) {
496
512
  console.log( 'error =>', error );