tango-app-api-trax 3.9.52 → 3.9.54

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.52",
3
+ "version": "3.9.54",
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",
@@ -2150,6 +2150,14 @@ export async function sopMobilechecklistMultiSectionFormatterv2( req, res, next
2150
2150
  if ( question.compliance ) {
2151
2151
  const scores = ( question.userAnswer || [] ).map( ( ua ) => ua?.complianceScore ?? 0 );
2152
2152
  let score = scores.length ? Math.max( ...scores ) : 0;
2153
+ // Single-choice questions answered 'NA' / 'N/A' (case-insensitive) score 0,
2154
+ // matching the visit-checklist PDF and dashboard compliance calculation.
2155
+ if ( question.answerType === 'multiplechoicesingle' ) {
2156
+ const naAnswer = ( question.userAnswer?.[0]?.answer || '' ).toString().trim().toLowerCase();
2157
+ if ( naAnswer === 'na' || naAnswer === 'n/a' ) {
2158
+ score = 0;
2159
+ }
2160
+ }
2153
2161
  requestData.userComplianceCount += score;
2154
2162
  }
2155
2163
  }
@@ -326,6 +326,83 @@ export const checklistPerformance = async ( req, res ) => {
326
326
  // },
327
327
  // } );
328
328
 
329
+ // Recompute compliance per submission straight from questionAnswers so the
330
+ // dashboard % matches the visit-checklist PDF: a 'multiplechoicesingle'
331
+ // question answered 'NA'/'N/A' (case-insensitive) is excluded from BOTH the
332
+ // score (numerator) and the max (denominator). Non-compliance questions and
333
+ // non-NA questions keep the existing max/score formula.
334
+ findQuery.push( {
335
+ $addFields: {
336
+ naAwareCompliance: {
337
+ $reduce: {
338
+ input: { $ifNull: [ '$questionAnswers', [] ] },
339
+ initialValue: { score: 0, max: 0 },
340
+ in: {
341
+ $reduce: {
342
+ input: { $ifNull: [ '$$this.questions', [] ] },
343
+ initialValue: '$$value',
344
+ in: {
345
+ $let: {
346
+ vars: {
347
+ // Count this question's score/max unless it is a single-choice
348
+ // question answered NA/N/A, or a non-compliance question.
349
+ countIt: {
350
+ $and: [
351
+ { $toBool: { $ifNull: [ '$$this.compliance', false ] } },
352
+ { $not: [ {
353
+ $and: [
354
+ { $eq: [ '$$this.answerType', 'multiplechoicesingle' ] },
355
+ { $in: [
356
+ { $toLower: { $trim: { input: { $convert: {
357
+ input: { $arrayElemAt: [ { $ifNull: [ '$$this.userAnswer.answer', [] ] }, 0 ] },
358
+ to: 'string', onError: '', onNull: '',
359
+ } } } } },
360
+ [ 'na', 'n/a' ],
361
+ ] },
362
+ ],
363
+ } ] },
364
+ ],
365
+ },
366
+ },
367
+ in: {
368
+ score: {
369
+ $add: [
370
+ '$$value.score',
371
+ { $cond: [
372
+ '$$countIt',
373
+ { $ifNull: [ { $max: { $map: {
374
+ input: { $ifNull: [ '$$this.userAnswer', [] ] },
375
+ as: 'u',
376
+ in: { $ifNull: [ '$$u.complianceScore', 0 ] },
377
+ } } }, 0 ] },
378
+ 0,
379
+ ] },
380
+ ],
381
+ },
382
+ max: {
383
+ $add: [
384
+ '$$value.max',
385
+ { $cond: [
386
+ '$$countIt',
387
+ { $ifNull: [ { $max: { $map: {
388
+ input: { $ifNull: [ '$$this.answers', [] ] },
389
+ as: 'o',
390
+ in: { $ifNull: [ '$$o.complianceScore', { $max: [ { $ifNull: [ '$$o.matchedCount', 0 ] }, { $ifNull: [ '$$o.notMatchedCount', 0 ] } ] } ] },
391
+ } } }, 0 ] },
392
+ 0,
393
+ ] },
394
+ ],
395
+ },
396
+ },
397
+ },
398
+ },
399
+ },
400
+ },
401
+ },
402
+ },
403
+ },
404
+ } );
405
+
329
406
  findQuery.push( {
330
407
  $group: {
331
408
  _id: '$sourceCheckList_id',
@@ -344,16 +421,24 @@ export const checklistPerformance = async ( req, res ) => {
344
421
  timeFlag: { $sum: '$timeFlag' },
345
422
  questionFlagCount: { $sum: '$questionFlag' },
346
423
  runAIFlagCount: { $sum: '$runAIFlag' },
347
- userComplianceCountTotal: {
424
+ userComplianceScoreTotal: {
348
425
  $sum: {
349
426
  $cond: [
350
427
  { $eq: [ '$checklistStatus', 'submit' ] },
351
- { $ifNull: [ '$userComplianceCount', 0 ] },
428
+ { $ifNull: [ '$naAwareCompliance.score', 0 ] },
429
+ 0,
430
+ ],
431
+ },
432
+ },
433
+ complianceMaxTotal: {
434
+ $sum: {
435
+ $cond: [
436
+ { $eq: [ '$checklistStatus', 'submit' ] },
437
+ { $ifNull: [ '$naAwareCompliance.max', 0 ] },
352
438
  0,
353
439
  ],
354
440
  },
355
441
  },
356
- complianceCount: { $first: '$complianceCount' },
357
442
  checkListType: { $last: '$checkListType' },
358
443
  redo: { $sum: { $cond: [ { $eq: [ '$redoStatus', true ] }, 1, 0 ] } },
359
444
  task: {
@@ -414,39 +499,26 @@ export const checklistPerformance = async ( req, res ) => {
414
499
  redo: 1,
415
500
  task: 1,
416
501
  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 ] },
502
+ $cond: [
503
+ { $gt: [ '$complianceMaxTotal', 0 ] },
504
+ {
505
+ $max: [
506
+ 0,
430
507
  {
431
- $max: [
432
- 0,
508
+ $round: [
433
509
  {
434
- $round: [
435
- {
436
- $multiply: [
437
- { $divide: [ '$userComplianceCountTotal', '$$divisor' ] },
438
- 100,
439
- ],
440
- },
441
- 0,
510
+ $multiply: [
511
+ { $divide: [ '$userComplianceScoreTotal', '$complianceMaxTotal' ] },
512
+ 100,
442
513
  ],
443
514
  },
515
+ 0,
444
516
  ],
445
517
  },
446
- 0,
447
518
  ],
448
519
  },
449
- },
520
+ 0,
521
+ ],
450
522
  },
451
523
  },
452
524
  } );
@@ -552,11 +552,21 @@ function mapSectionsFromQuestionAnswer( questionAnswer ) {
552
552
  }
553
553
 
554
554
 
555
- const max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
556
-
555
+ let max = q.compliance ? Math.max( ...q?.answers.map( ( o ) => o?.complianceScore ?? Math.max( o?.matchedCount ?? 0, o?.notMatchedCount ?? 0 ) ) ) : 0;
557
556
 
558
557
  let score = q.compliance ? Math.max( ...q?.userAnswer?.map( ( o ) => o?.complianceScore ?? 0 ) ) : 0;
559
558
 
559
+ // Single-choice questions answered 'NA' / 'N/A' (case-insensitive) are
560
+ // excluded from scoring entirely: both score and max are 0, so they add
561
+ // nothing to the section/total numerator or denominator.
562
+ if ( q.answerType === 'multiplechoicesingle' ) {
563
+ const naAnswer = ( ua?.answer || '' ).toString().trim().toLowerCase();
564
+ if ( naAnswer === 'na' || naAnswer === 'n/a' ) {
565
+ score = 0;
566
+ max = 0;
567
+ }
568
+ }
569
+
560
570
 
561
571
  if ( q.compliance && ( q.answerType === 'image' || q.answerType === 'image/video' ) ) {
562
572
  // image/video questions mix image + video entries in userAnswer, so only score the images.