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
|
@@ -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
|
-
|
|
2154
|
-
|
|
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
|
-
|
|
527
|
-
|
|
528
|
-
|
|
529
|
-
|
|
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
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
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
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
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
|
|
565
|
-
result.checklistPerformance =
|
|
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 );
|