tango-app-api-trax 3.9.54 → 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.54",
3
+ "version": "3.9.55",
4
4
  "description": "Trax",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -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,17 +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;
2153
- // Single-choice questions answered 'NA' / 'N/A' (case-insensitive) score 0,
2154
- // matching the visit-checklist PDF and dashboard compliance calculation.
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.
2155
2163
  if ( question.answerType === 'multiplechoicesingle' ) {
2156
2164
  const naAnswer = ( question.userAnswer?.[0]?.answer || '' ).toString().trim().toLowerCase();
2157
2165
  if ( naAnswer === 'na' || naAnswer === 'n/a' ) {
2158
2166
  score = 0;
2167
+ max = 0;
2159
2168
  }
2160
2169
  }
2170
+
2161
2171
  requestData.userComplianceCount += score;
2172
+ requestData.complianceCount += max;
2162
2173
  }
2163
2174
  }
2164
2175
  }
@@ -2804,6 +2815,8 @@ export async function submitChecklist( req, res ) {
2804
2815
  updateData.updatedAt = dayjs.utc( currentDateTime.format( 'hh:mm:ss A, DD MMM YYYY' ), 'hh:mm:ss A, DD MMM YYYY' ).format();
2805
2816
  updateData.submitMobileTime = requestData?.currentTime;
2806
2817
  updateData.userComplianceCount = requestData?.userComplianceCount;
2818
+ // NA-aware max (denominator) recomputed at submit, alongside the score above.
2819
+ updateData.complianceCount = requestData?.complianceCount;
2807
2820
  updateData.userSignature = requestData?.userSignature;
2808
2821
  if ( requestData?.userImage ) {
2809
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
  }
@@ -326,83 +332,6 @@ export const checklistPerformance = async ( req, res ) => {
326
332
  // },
327
333
  // } );
328
334
 
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
-
406
335
  findQuery.push( {
407
336
  $group: {
408
337
  _id: '$sourceCheckList_id',
@@ -421,24 +350,6 @@ export const checklistPerformance = async ( req, res ) => {
421
350
  timeFlag: { $sum: '$timeFlag' },
422
351
  questionFlagCount: { $sum: '$questionFlag' },
423
352
  runAIFlagCount: { $sum: '$runAIFlag' },
424
- userComplianceScoreTotal: {
425
- $sum: {
426
- $cond: [
427
- { $eq: [ '$checklistStatus', 'submit' ] },
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 ] },
438
- 0,
439
- ],
440
- },
441
- },
442
353
  checkListType: { $last: '$checkListType' },
443
354
  redo: { $sum: { $cond: [ { $eq: [ '$redoStatus', true ] }, 1, 0 ] } },
444
355
  task: {
@@ -498,71 +409,104 @@ export const checklistPerformance = async ( req, res ) => {
498
409
  checkListType: 1,
499
410
  redo: 1,
500
411
  task: 1,
501
- questionCompliance: {
502
- $cond: [
503
- { $gt: [ '$complianceMaxTotal', 0 ] },
504
- {
505
- $max: [
506
- 0,
507
- {
508
- $round: [
509
- {
510
- $multiply: [
511
- { $divide: [ '$userComplianceScoreTotal', '$complianceMaxTotal' ] },
512
- 100,
513
- ],
514
- },
515
- 0,
516
- ],
517
- },
518
- ],
519
- },
520
- 0,
521
- ],
522
- },
523
412
  },
524
413
  } );
525
414
 
526
- if ( requestData.sortColumnName && requestData.sortColumnName != '' && requestData.sortBy && requestData.sortBy !='' ) {
527
- findQuery.push( { $sort: { [requestData.sortColumnName]: requestData.sortBy } } );
528
- } else {
529
- findQuery.push( { $sort: { ['submittedChecklist']: -1 } } );
530
- }
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
+ };
531
428
 
532
- findQuery.push( {
533
- $facet: {
534
- data: [
535
- { $skip: skip }, { $limit: limit },
536
- ],
537
- count: [
538
- { $count: 'total' },
539
- ],
540
- },
541
- } );
542
- let getChecklistPerformanceData = await processedchecklistService.aggregate( findQuery );
543
- 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 ) {
544
446
  return res.sendError( 'no data found', 204 );
545
447
  }
546
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
+
547
494
  if ( requestData.export ) {
548
- const exportdata = [];
549
- getChecklistPerformanceData[0].data.forEach( ( element ) => {
550
- exportdata.push( {
551
- 'Checklist Name': element.checkListName || '--',
552
- 'Coverage': element.coverage || '--',
553
- 'Scheduled': element.scheduleRepeatedType || '--',
554
- 'Assigned To': element.storeCount || '--',
555
- 'Submitted': element.submittedChecklist || '--',
556
- 'Flags': element.flaggedChecklist || '--',
557
- 'Tasks': element.task || '--',
558
- 'ReDo': element.redo || '--',
559
- } );
560
- } );
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
+ } ) );
561
505
  return await download( exportdata, res );
562
506
  }
563
507
 
564
- result.totalCount = getChecklistPerformanceData[0].count[0].total;
565
- result.checklistPerformance = getChecklistPerformanceData[0].data;
508
+ result.totalCount = getChecklistPerformanceData.length;
509
+ result.checklistPerformance = pageRows;
566
510
  return res.sendSuccess( result );
567
511
  } catch ( error ) {
568
512
  console.log( 'error =>', error );