tango-app-api-audit 3.6.56 → 3.6.58
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 +2 -2
- package/src/controllers/audit.controllers.js +134 -169
- package/src/controllers/eyeTestAudit.controllers.js +174 -1
- package/src/dtos/audit.dtos.js +2 -0
- package/src/dtos/eyeTestAudit.dtos.js +11 -0
- package/src/routes/audit.routes.js +3 -0
- package/src/routes/eyeTestAudit.routes.js +6 -2
- package/src/validation/audit.validation.js +37 -23
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-audit",
|
|
3
|
-
"version": "3.6.
|
|
3
|
+
"version": "3.6.58",
|
|
4
4
|
"description": "audit & audit metrics apis",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
@@ -28,7 +28,7 @@
|
|
|
28
28
|
"nodemailer": "^7.0.10",
|
|
29
29
|
"nodemon": "^3.1.3",
|
|
30
30
|
"swagger-ui-express": "^5.0.1",
|
|
31
|
-
"tango-api-schema": "^2.
|
|
31
|
+
"tango-api-schema": "^2.6.41",
|
|
32
32
|
"tango-app-api-middleware": "^3.6.18",
|
|
33
33
|
"winston": "^3.13.0",
|
|
34
34
|
"winston-daily-rotate-file": "^5.0.0"
|
|
@@ -73,6 +73,7 @@ import { aggregateTraxAuditData, findOneTraxAuditData } from '../service/traxAud
|
|
|
73
73
|
import { bulkWriteAuditUserWallet, createAuditUserWallet, findOneAuditUserWallet } from '../service/auditUserWallet.service.js';
|
|
74
74
|
import { updateOneAuditUsers } from '../service/auditUsers.service.js';
|
|
75
75
|
import { bulkUpdate, clearScroll, upsertOpenSearchData, scrollResponse, searchOpenSearchData, updateOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
|
|
76
|
+
|
|
76
77
|
// import { aggregateAuditUsers } from '../service/auditUsers.service.js';
|
|
77
78
|
|
|
78
79
|
/* < -- *** Configuration *** --> */
|
|
@@ -937,7 +938,8 @@ export async function getFilterData( msg, user ) {
|
|
|
937
938
|
( item ) =>
|
|
938
939
|
item.temp_ids < 20000 &&
|
|
939
940
|
item.temp_ids > 0 &&
|
|
940
|
-
item.person_status === ''
|
|
941
|
+
item.person_status === '' &&
|
|
942
|
+
item.query_status !== '',
|
|
941
943
|
);
|
|
942
944
|
const finalResult = await filterData.map( ( i ) => {
|
|
943
945
|
return { ...i, img: i.index.concat( '_', i.temp_ids ) };
|
|
@@ -1084,9 +1086,14 @@ export async function workSpace( req, res ) {
|
|
|
1084
1086
|
const dateRange = await getDate( new Date(), new Date() );
|
|
1085
1087
|
const filter = [
|
|
1086
1088
|
{ status: { $in: [ 'active', 'hold' ] } },
|
|
1087
|
-
{ 'auditConfigs.audit': { $eq: true } },
|
|
1088
1089
|
{ clientId: { $in: inputData.clientId } },
|
|
1089
1090
|
];
|
|
1091
|
+
if ( [ 'traffic', 'zone' ].includes( inputData?.moduleType ) ) {
|
|
1092
|
+
filter.push( { 'auditConfigs.audit': { $eq: true } } );
|
|
1093
|
+
}
|
|
1094
|
+
if ( inputData?.moduleType === 'track' ) {
|
|
1095
|
+
filter.push( { 'auditConfigs.trackAudit': { $eq: true } } );
|
|
1096
|
+
}
|
|
1090
1097
|
|
|
1091
1098
|
const temp = [];
|
|
1092
1099
|
const clientQuery = [
|
|
@@ -2560,26 +2567,31 @@ export async function clientMetrics( req, res ) {
|
|
|
2560
2567
|
{ moduleType: { $eq: inputData.moduleType } },
|
|
2561
2568
|
];
|
|
2562
2569
|
|
|
2563
|
-
let storeAuditFilter = [
|
|
2564
|
-
{ $eq: [ '$clientId', '$$clientId' ] },
|
|
2565
|
-
{ $eq: [ '$fileDate', '$$fileDate' ] },
|
|
2566
|
-
{
|
|
2567
|
-
$eq: [ '$moduleType', inputData.moduleType ],
|
|
2568
|
-
},
|
|
2569
|
-
|
|
2570
|
-
];
|
|
2571
|
-
|
|
2572
2570
|
if ( inputData?.filterByClient?.length > 0 ) {
|
|
2573
2571
|
filter.push(
|
|
2574
2572
|
{ clientId: { $in: inputData.filterByClient } },
|
|
2575
2573
|
);
|
|
2576
2574
|
}
|
|
2577
|
-
|
|
2578
|
-
|
|
2579
|
-
|
|
2580
|
-
|
|
2581
|
-
|
|
2582
|
-
}
|
|
2575
|
+
|
|
2576
|
+
// Only show clients enabled for audit ("access" clients): traffic/zone use
|
|
2577
|
+
// auditConfigs.audit, track uses auditConfigs.trackAudit.
|
|
2578
|
+
const clientAccessFilter = [];
|
|
2579
|
+
if ( [ 'traffic', 'zone' ].includes( inputData?.moduleType ) ) {
|
|
2580
|
+
clientAccessFilter.push( { 'auditConfigs.audit': { $eq: true } } );
|
|
2581
|
+
}
|
|
2582
|
+
if ( inputData?.moduleType === 'track' ) {
|
|
2583
|
+
clientAccessFilter.push( { 'auditConfigs.trackAudit': { $eq: true } } );
|
|
2584
|
+
}
|
|
2585
|
+
if ( clientAccessFilter.length > 0 ) {
|
|
2586
|
+
const accessClients = await aggregateClient( [
|
|
2587
|
+
{ $match: { $and: clientAccessFilter } },
|
|
2588
|
+
{ $project: { _id: 0, clientId: 1 } },
|
|
2589
|
+
] );
|
|
2590
|
+
filter.push( { clientId: { $in: accessClients.map( ( c ) => c.clientId ) } } );
|
|
2591
|
+
}
|
|
2592
|
+
// --- Base metrics from auditStoreData (one indexed aggregation) --------
|
|
2593
|
+
const baseQuery = [
|
|
2594
|
+
{ $match: { $and: filter } },
|
|
2583
2595
|
{
|
|
2584
2596
|
$group: {
|
|
2585
2597
|
_id: { clientId: '$clientId', fileDate: '$fileDate', moduleType: '$moduleType' },
|
|
@@ -2592,181 +2604,125 @@ export async function clientMetrics( req, res ) {
|
|
|
2592
2604
|
totalFilesCount: { $sum: 1 },
|
|
2593
2605
|
},
|
|
2594
2606
|
},
|
|
2607
|
+
];
|
|
2608
|
+
|
|
2609
|
+
// --- Store status counts from storeAudit (one indexed aggregation) -----
|
|
2610
|
+
// Replaces the old correlated per-group $lookup: instead of running a
|
|
2611
|
+
// sub-query for every client/date group, aggregate storeAudit once over the
|
|
2612
|
+
// same filter and merge by clientId+fileDate in JS below. storeAudit has the
|
|
2613
|
+
// same clientId / moduleType / fileDateISO fields, so `filter` applies as-is.
|
|
2614
|
+
const storeAuditQuery = [
|
|
2615
|
+
{ $match: { $and: filter } },
|
|
2595
2616
|
{
|
|
2596
|
-
$
|
|
2597
|
-
fileDate:
|
|
2598
|
-
clientId:
|
|
2599
|
-
|
|
2600
|
-
|
|
2601
|
-
|
|
2602
|
-
|
|
2603
|
-
|
|
2604
|
-
notAssignedCount: { $ifNull: [ 0, 0 ] },
|
|
2605
|
-
clientStatus: { $ifNull: [ '', '' ] },
|
|
2617
|
+
$group: {
|
|
2618
|
+
_id: { clientId: '$clientId', fileDate: '$fileDate' },
|
|
2619
|
+
clientId: { $first: '$clientId' },
|
|
2620
|
+
fileDate: { $first: '$fileDate' },
|
|
2621
|
+
completedStores: { $sum: { $cond: [ { $eq: [ '$status', 'completed' ] }, 1, 0 ] } },
|
|
2622
|
+
notAssignedStores: { $sum: { $cond: [ { $eq: [ '$status', 'not_assign' ] }, 1, 0 ] } },
|
|
2623
|
+
inprogressStores: { $sum: { $cond: [ { $in: [ '$status', [ 'inprogress', 'drafted', 'assigned' ] ] }, 1, 0 ] } },
|
|
2624
|
+
fileCount: { $sum: 1 },
|
|
2606
2625
|
},
|
|
2607
2626
|
},
|
|
2608
|
-
|
|
2609
|
-
$lookup: {
|
|
2610
|
-
'from': 'storeAudit',
|
|
2611
|
-
'let': { clientId: '$clientId', fileDate: '$fileDate', totalAuditFiles: '$totalFilesCount' },
|
|
2612
|
-
'pipeline': [
|
|
2627
|
+
];
|
|
2613
2628
|
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
},
|
|
2619
|
-
},
|
|
2620
|
-
},
|
|
2621
|
-
{
|
|
2622
|
-
$project: {
|
|
2623
|
-
completedStores: { $cond: [ { $eq: [ '$status', 'completed' ] }, 1, 0 ] },
|
|
2624
|
-
notAssignedStores: { $cond: [ { $eq: [ '$status', 'not_assign' ] }, 1, 0 ] },
|
|
2625
|
-
inprogressStores: { $cond: [ { $in: [ '$status', [ 'inprogress', 'drafted', 'assigned' ] ] }, 1, 0 ] },
|
|
2626
|
-
},
|
|
2627
|
-
},
|
|
2628
|
-
{
|
|
2629
|
-
$group: {
|
|
2630
|
-
_id: { clientId: '$clientId', fileDate: '$fileDate' },
|
|
2631
|
-
completedStores: { $sum: '$completedStores' },
|
|
2632
|
-
inprogressStores: { $sum: '$inprogressStores' },
|
|
2633
|
-
notAssignedStores: { $sum: '$notAssignedStores' },
|
|
2634
|
-
fileCount: { $sum: 1 },
|
|
2635
|
-
},
|
|
2636
|
-
},
|
|
2637
|
-
{
|
|
2638
|
-
$project: {
|
|
2639
|
-
_id: 0,
|
|
2640
|
-
completedStores: 1,
|
|
2641
|
-
inprogressStores: 1,
|
|
2642
|
-
notAssignedStores: 1,
|
|
2643
|
-
fileCount: { $ifNull: [ '$fileCount', 0 ] },
|
|
2644
|
-
completionPercentage: {
|
|
2645
|
-
$ifNull: [
|
|
2646
|
-
{
|
|
2647
|
-
$round: [ {
|
|
2648
|
-
$multiply: [
|
|
2649
|
-
100, {
|
|
2650
|
-
$divide: [
|
|
2651
|
-
'$completedStores', '$$totalAuditFiles',
|
|
2652
|
-
],
|
|
2653
|
-
},
|
|
2654
|
-
],
|
|
2655
|
-
}, 1 ],
|
|
2629
|
+
const [ baseRows, storeRows ] = await Promise.all( [
|
|
2630
|
+
aggregateAuditStoreData( baseQuery ),
|
|
2631
|
+
aggregateStoreAudit( storeAuditQuery ),
|
|
2632
|
+
] );
|
|
2656
2633
|
|
|
2657
|
-
|
|
2658
|
-
|
|
2659
|
-
|
|
2660
|
-
|
|
2661
|
-
|
|
2662
|
-
},
|
|
2663
|
-
], 'as': 'storeAudit',
|
|
2664
|
-
},
|
|
2665
|
-
},
|
|
2666
|
-
{
|
|
2667
|
-
$unwind: {
|
|
2668
|
-
path: '$storeAudit',
|
|
2669
|
-
preserveNullAndEmptyArrays: true,
|
|
2670
|
-
},
|
|
2671
|
-
},
|
|
2672
|
-
{
|
|
2673
|
-
$project: {
|
|
2674
|
-
_id: 0,
|
|
2675
|
-
fileDate: 1,
|
|
2676
|
-
clientId: 1,
|
|
2677
|
-
clientName: 1,
|
|
2678
|
-
totalFilesCount: 1,
|
|
2679
|
-
moduleType: 1,
|
|
2680
|
-
installedStore: 1,
|
|
2681
|
-
// notAssignedStores: '$storeAudit.notAssignedStores',
|
|
2682
|
-
inprogressStoresCount: '$storeAudit.inprogressStores',
|
|
2683
|
-
// fileCount: '$storeAudit.fileCount',
|
|
2684
|
-
notAssignedCount: { $subtract: [ '$totalFilesCount', { $subtract: [ { $ifNull: [ '$storeAudit.fileCount', 0 ] }, { $ifNull: [ '$storeAudit.notAssignedStores', 0 ] } ] } ] },
|
|
2685
|
-
clientStatus: { $cond: [ { $eq: [ { $ifNull: [ '$storeAudit.fileCount', 0 ] }, 0 ] }, 'not-taken', { $cond: [ { $gte: [ '$storeAudit.completedStores', '$totalFilesCount' ] }, 'completed', 'pending' ] } ] }, // { $cond: [ { $or: [ { $gt: [ '$notAssignedCount', 0 ] }, { $gt: [ '$storeAudit.notAssignedStores', 0 ] } ] }
|
|
2686
|
-
completedStores: '$storeAudit.completedStores',
|
|
2687
|
-
completionPercentage: '$storeAudit.completionPercentage',
|
|
2634
|
+
// Index storeAudit counts by clientId + fileDate for an O(1) merge.
|
|
2635
|
+
const storeByKey = new Map();
|
|
2636
|
+
for ( const s of storeRows ) {
|
|
2637
|
+
storeByKey.set( `${s.clientId}__${s.fileDate}`, s );
|
|
2638
|
+
}
|
|
2688
2639
|
|
|
2689
|
-
|
|
2690
|
-
|
|
2691
|
-
|
|
2640
|
+
// Merge and derive the same fields the old $lookup / $project produced.
|
|
2641
|
+
let merged = baseRows.map( ( base ) => {
|
|
2642
|
+
const sa = storeByKey.get( `${base.clientId}__${base.fileDate}` );
|
|
2643
|
+
const fileCount = sa?.fileCount || 0;
|
|
2644
|
+
const notAssignedStores = sa?.notAssignedStores || 0;
|
|
2645
|
+
const completedStores = sa ? sa.completedStores : undefined;
|
|
2646
|
+
const clientStatus = fileCount === 0 ?
|
|
2647
|
+
'not-taken' :
|
|
2648
|
+
( ( completedStores || 0 ) >= base.totalFilesCount ? 'completed' : 'pending' );
|
|
2649
|
+
return {
|
|
2650
|
+
fileDate: base.fileDate,
|
|
2651
|
+
clientId: base.clientId,
|
|
2652
|
+
clientName: base.clientName,
|
|
2653
|
+
totalFilesCount: base.totalFilesCount,
|
|
2654
|
+
moduleType: base.moduleType,
|
|
2655
|
+
installedStore: base.installedStore,
|
|
2656
|
+
inprogressStoresCount: sa ? sa.inprogressStores : undefined,
|
|
2657
|
+
notAssignedCount: base.totalFilesCount - ( fileCount - notAssignedStores ),
|
|
2658
|
+
clientStatus: clientStatus,
|
|
2659
|
+
completedStores: completedStores,
|
|
2660
|
+
completionPercentage: sa ?
|
|
2661
|
+
Math.round( ( ( 100 * sa.completedStores ) / base.totalFilesCount ) * 10 ) / 10 :
|
|
2662
|
+
undefined,
|
|
2663
|
+
};
|
|
2664
|
+
} );
|
|
2692
2665
|
|
|
2666
|
+
// --- Post-merge filters (depend on the derived clientStatus) -----------
|
|
2693
2667
|
if ( inputData?.filterByStatus?.length > 0 ) {
|
|
2694
|
-
|
|
2695
|
-
{
|
|
2696
|
-
$match: { clientStatus: { $in: inputData.filterByStatus } },
|
|
2697
|
-
},
|
|
2698
|
-
|
|
2699
|
-
);
|
|
2668
|
+
merged = merged.filter( ( r ) => inputData.filterByStatus.includes( r.clientStatus ) );
|
|
2700
2669
|
}
|
|
2701
2670
|
|
|
2702
2671
|
if ( inputData.searchValue && inputData.searchValue !== '' ) {
|
|
2703
|
-
|
|
2704
|
-
|
|
2705
|
-
|
|
2706
|
-
|
|
2707
|
-
|
|
2708
|
-
|
|
2709
|
-
|
|
2710
|
-
],
|
|
2711
|
-
|
|
2712
|
-
},
|
|
2713
|
-
} );
|
|
2672
|
+
const rx = new RegExp( inputData.searchValue, 'i' );
|
|
2673
|
+
merged = merged.filter( ( r ) =>
|
|
2674
|
+
rx.test( r.clientId || '' ) ||
|
|
2675
|
+
rx.test( r.clientName || '' ) ||
|
|
2676
|
+
rx.test( r.moduleType || '' ) ||
|
|
2677
|
+
rx.test( r.clientStatus || '' ),
|
|
2678
|
+
);
|
|
2714
2679
|
}
|
|
2680
|
+
|
|
2681
|
+
// --- Sort: numeric columns numerically, strings case-insensitively to
|
|
2682
|
+
// match the DB collation ({ locale: 'en', strength: 2 }). --------------
|
|
2715
2683
|
if ( inputData.sortColumnName ) {
|
|
2716
|
-
const
|
|
2717
|
-
const
|
|
2718
|
-
|
|
2719
|
-
|
|
2720
|
-
|
|
2721
|
-
|
|
2684
|
+
const dir = Number( inputData.sortBy ) === 1 ? 1 : -1;
|
|
2685
|
+
const col = inputData.sortColumnName;
|
|
2686
|
+
merged.sort( ( a, b ) => {
|
|
2687
|
+
const av = a[col];
|
|
2688
|
+
const bv = b[col];
|
|
2689
|
+
let cmp;
|
|
2690
|
+
if ( typeof av === 'number' && typeof bv === 'number' ) {
|
|
2691
|
+
cmp = av - bv;
|
|
2692
|
+
} else {
|
|
2693
|
+
cmp = String( av ?? '' ).localeCompare( String( bv ?? '' ), 'en', { sensitivity: 'accent' } );
|
|
2694
|
+
}
|
|
2695
|
+
return dir * cmp;
|
|
2696
|
+
} );
|
|
2722
2697
|
}
|
|
2723
2698
|
|
|
2724
|
-
const count =
|
|
2725
|
-
if ( count
|
|
2699
|
+
const count = merged.length;
|
|
2700
|
+
if ( count === 0 ) {
|
|
2726
2701
|
return res.sendError( 'No Data Found', 204 );
|
|
2727
2702
|
}
|
|
2728
2703
|
|
|
2729
2704
|
if ( inputData.isExport ) {
|
|
2730
|
-
|
|
2731
|
-
|
|
2732
|
-
|
|
2733
|
-
|
|
2734
|
-
|
|
2735
|
-
|
|
2736
|
-
|
|
2737
|
-
|
|
2738
|
-
|
|
2739
|
-
|
|
2740
|
-
|
|
2741
|
-
|
|
2742
|
-
|
|
2743
|
-
|
|
2744
|
-
'File Date': '$fileDate',
|
|
2745
|
-
'Brand Name': '$clientName',
|
|
2746
|
-
'Brand Id': '$clientId',
|
|
2747
|
-
'Product Type': '$moduleType',
|
|
2748
|
-
// 'Completed Percentage': '$completionPercentage',
|
|
2749
|
-
'Installed Stores': '$installedStore',
|
|
2750
|
-
'Audit Stores': '$totalFilesCount',
|
|
2751
|
-
'Inprogress Stores': '$inprogressStoresCount',
|
|
2752
|
-
'Not Assigned stores': '$notAssignedCount',
|
|
2753
|
-
// 'Not Assigned Stores': '$notAssignedStores',
|
|
2754
|
-
'Completed Stores': '$completedStores',
|
|
2755
|
-
'Status': '$clientStatus',
|
|
2756
|
-
},
|
|
2757
|
-
},
|
|
2758
|
-
);
|
|
2705
|
+
const exportRows = merged.slice( 0, 10000 ).map( ( r ) => ( {
|
|
2706
|
+
'File Date': r.fileDate,
|
|
2707
|
+
'Brand Name': r.clientName,
|
|
2708
|
+
'Brand Id': r.clientId,
|
|
2709
|
+
'Product Type': r.moduleType,
|
|
2710
|
+
'Installed Stores': r.installedStore,
|
|
2711
|
+
'Audit Stores': r.totalFilesCount,
|
|
2712
|
+
'Inprogress Stores': r.inprogressStoresCount,
|
|
2713
|
+
'Not Assigned stores': r.notAssignedCount,
|
|
2714
|
+
'Completed Stores': r.completedStores,
|
|
2715
|
+
'Status': r.clientStatus,
|
|
2716
|
+
} ) );
|
|
2717
|
+
await download( exportRows, res );
|
|
2718
|
+
return;
|
|
2759
2719
|
}
|
|
2760
2720
|
|
|
2761
|
-
const
|
|
2762
|
-
if (
|
|
2721
|
+
const pageResult = merged.slice( offset, offset + limit );
|
|
2722
|
+
if ( pageResult.length === 0 ) {
|
|
2763
2723
|
return res.sendError( 'No Data Found', 204 );
|
|
2764
2724
|
}
|
|
2765
|
-
|
|
2766
|
-
await download( result, res );
|
|
2767
|
-
return;
|
|
2768
|
-
}
|
|
2769
|
-
return res.sendSuccess( { result: result, count: count.length } );
|
|
2725
|
+
return res.sendSuccess( { result: pageResult, count: count } );
|
|
2770
2726
|
} catch ( error ) {
|
|
2771
2727
|
const err = error.error || 'Internal Server Error';
|
|
2772
2728
|
logger.error( { error: error, message: req.body, function: 'clientMetrics' } );
|
|
@@ -3715,6 +3671,13 @@ export async function reTrigger( req, res ) {
|
|
|
3715
3671
|
}
|
|
3716
3672
|
switch ( inputData.triggerType ) {
|
|
3717
3673
|
case 'queue':
|
|
3674
|
+
// Only push to the queue when the store has not already been queued.
|
|
3675
|
+
// A 'not_assign' status means a prior reTrigger already pushed this
|
|
3676
|
+
// store - pushing again would create a duplicate queue message.
|
|
3677
|
+
if ( req.audit.status === 'not_assign' ) {
|
|
3678
|
+
await releaseReTriggerLock( lockKey );
|
|
3679
|
+
return res.sendError( 'This store has already been pushed to the queue', 400 );
|
|
3680
|
+
}
|
|
3718
3681
|
// Only push to the queue when the store has not already been queued.
|
|
3719
3682
|
// A 'not_assign' status means a prior reTrigger already pushed this
|
|
3720
3683
|
// store - pushing again would create a duplicate queue message.
|
|
@@ -5899,3 +5862,5 @@ export async function rePushAuditStores( req, res ) {
|
|
|
5899
5862
|
return res.sendError( err, 500 );
|
|
5900
5863
|
}
|
|
5901
5864
|
}
|
|
5865
|
+
|
|
5866
|
+
|
|
@@ -1414,7 +1414,6 @@ export async function summaryList( req, res ) {
|
|
|
1414
1414
|
if ( inputData.clientId !== '11' ) {
|
|
1415
1415
|
return res.sendError( 'No data found', 204 );
|
|
1416
1416
|
}
|
|
1417
|
-
|
|
1418
1417
|
// declare openSearch and get data form the .env/secret manager
|
|
1419
1418
|
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1420
1419
|
|
|
@@ -1764,6 +1763,7 @@ export async function summaryList( req, res ) {
|
|
|
1764
1763
|
} );
|
|
1765
1764
|
}
|
|
1766
1765
|
const getcount = await aggregateClusters( inputData.keyType === 'rm'?RMQuery: clusterQuery );
|
|
1766
|
+
|
|
1767
1767
|
const count = getcount?.length || 0;
|
|
1768
1768
|
if ( count == 0 ) {
|
|
1769
1769
|
return res.sendError( 'No data', 204 );
|
|
@@ -1787,6 +1787,7 @@ export async function summaryList( req, res ) {
|
|
|
1787
1787
|
);
|
|
1788
1788
|
}
|
|
1789
1789
|
const getCluster = await aggregateClusters( inputData.keyType === 'rm'?RMQuery: clusterQuery );
|
|
1790
|
+
logger.info( { masg: '...........getCluster', getCluster, RMQuery } );
|
|
1790
1791
|
if ( !getCluster || getCluster?.length == 0 ) {
|
|
1791
1792
|
return res.sendError( 'No data', 204 );
|
|
1792
1793
|
}
|
|
@@ -3174,3 +3175,175 @@ export async function updateEmailConfig1( req, res ) {
|
|
|
3174
3175
|
return res.sendError( err, 500 );
|
|
3175
3176
|
}
|
|
3176
3177
|
}
|
|
3178
|
+
|
|
3179
|
+
export async function getConversationDetails( req, res ) {
|
|
3180
|
+
try {
|
|
3181
|
+
const inputData = req.params;
|
|
3182
|
+
|
|
3183
|
+
// Query OpenSearch audio_analytics_report index by doc_id and moduleType
|
|
3184
|
+
const query = {
|
|
3185
|
+
'query': {
|
|
3186
|
+
'bool': {
|
|
3187
|
+
'must': [
|
|
3188
|
+
{
|
|
3189
|
+
'term': {
|
|
3190
|
+
'unique_id.keyword': inputData.id,
|
|
3191
|
+
},
|
|
3192
|
+
},
|
|
3193
|
+
],
|
|
3194
|
+
},
|
|
3195
|
+
},
|
|
3196
|
+
'size': 1,
|
|
3197
|
+
};
|
|
3198
|
+
|
|
3199
|
+
const result = await getOpenSearchData( 'audio_analytics_report_v2', query );
|
|
3200
|
+
if ( result?.body?.hits?.hits?.length === 0 ) {
|
|
3201
|
+
return res.sendError( 'No data found', 204 );
|
|
3202
|
+
}
|
|
3203
|
+
|
|
3204
|
+
const source = result?.body?.hits?.hits[0]?._source;
|
|
3205
|
+
|
|
3206
|
+
if ( !source ) {
|
|
3207
|
+
return res.sendError( 'No data found', 204 );
|
|
3208
|
+
}
|
|
3209
|
+
|
|
3210
|
+
// Helper function to format duration: convert seconds to mm:ss format
|
|
3211
|
+
const formatDuration = ( seconds ) => {
|
|
3212
|
+
if ( !seconds || seconds === 0 ) return '0:00';
|
|
3213
|
+
const minutes = Math.floor( seconds / 60 );
|
|
3214
|
+
const secs = Math.floor( seconds % 60 );
|
|
3215
|
+
return `${minutes}:${secs.toString().padStart( 2, '0' )}`;
|
|
3216
|
+
};
|
|
3217
|
+
|
|
3218
|
+
// Helper: parse "s3://bucket/path/to/file" into { Bucket, file_path }
|
|
3219
|
+
const parseS3Uri = ( uri ) => {
|
|
3220
|
+
if ( !uri ) return null;
|
|
3221
|
+
const withoutScheme = uri.replace( /^s3:\/\//, '' );
|
|
3222
|
+
const slashIdx = withoutScheme.indexOf( '/' );
|
|
3223
|
+
if ( slashIdx === -1 ) return null;
|
|
3224
|
+
return {
|
|
3225
|
+
Bucket: withoutScheme.slice( 0, slashIdx ),
|
|
3226
|
+
file_path: withoutScheme.slice( slashIdx + 1 ),
|
|
3227
|
+
};
|
|
3228
|
+
};
|
|
3229
|
+
|
|
3230
|
+
// Helper function to format transcript: fetch and clean transcript file
|
|
3231
|
+
const getFormattedTranscript = async ( transcriptPath ) => {
|
|
3232
|
+
try {
|
|
3233
|
+
if ( !transcriptPath ) return '';
|
|
3234
|
+
|
|
3235
|
+
const s3Parts = parseS3Uri( transcriptPath );
|
|
3236
|
+
logger.info( { s3Parts: s3Parts, function: 'getFormattedTranscript' } );
|
|
3237
|
+
if ( !s3Parts ) return '';
|
|
3238
|
+
|
|
3239
|
+
const transcriptSignedUrl = await signedUrl( { ...s3Parts, key: 'aws' } );
|
|
3240
|
+
if ( !transcriptSignedUrl ) return '';
|
|
3241
|
+
|
|
3242
|
+
const fetchRes = await fetch( transcriptSignedUrl );
|
|
3243
|
+
if ( !fetchRes.ok ) return '';
|
|
3244
|
+
let text = await fetchRes.text();
|
|
3245
|
+
|
|
3246
|
+
// Remove timestamps like [00:00:33.954 --> 00:00:36.154]
|
|
3247
|
+
text = text.replace( /\[\d{2}:\d{2}:\d{2}\.\d{3} --> \d{2}:\d{2}:\d{2}\.\d{3}\]\s*/g, '' );
|
|
3248
|
+
|
|
3249
|
+
// Remove [silence] markers
|
|
3250
|
+
text = text.replace( /\[silence\]\s*/g, '' );
|
|
3251
|
+
|
|
3252
|
+
// Replace multiple newlines with single space to create paragraphs
|
|
3253
|
+
text = text.replace( /\n+/g, ' ' );
|
|
3254
|
+
|
|
3255
|
+
// Clean up multiple spaces
|
|
3256
|
+
text = text.replace( /\s+/g, ' ' ).trim();
|
|
3257
|
+
|
|
3258
|
+
return text;
|
|
3259
|
+
} catch ( error ) {
|
|
3260
|
+
logger.error( { message: error, function: 'getFormattedTranscript' } );
|
|
3261
|
+
return '';
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
// Helper function to format metric names
|
|
3266
|
+
const formatMetricName = ( name ) => {
|
|
3267
|
+
const nameMap = {
|
|
3268
|
+
'Torchlight Exam': 'Torchlight Check',
|
|
3269
|
+
'JCC': 'JCC Check',
|
|
3270
|
+
'Distance Visual Acuity': 'Distance-VA-Check',
|
|
3271
|
+
'Duochrome': 'Duochrome Check',
|
|
3272
|
+
'Near Visual Acuity': 'Near Addition Check',
|
|
3273
|
+
};
|
|
3274
|
+
return nameMap[name] || name;
|
|
3275
|
+
};
|
|
3276
|
+
|
|
3277
|
+
// Format metrics results with sequential time allocation
|
|
3278
|
+
const matchedCohort = source?.cohort_analysis_results?.find( ( c ) => c.cohortId === 'cohort_11_1ddea945-ff3a-4c11-95a9-bb425ca927de' );
|
|
3279
|
+
const metricsData = matchedCohort?.metrics_results || [];
|
|
3280
|
+
|
|
3281
|
+
// Calculate sequential times for detected metrics
|
|
3282
|
+
let currentSeconds = 0;
|
|
3283
|
+
const totalDuration = Math.floor( source?.duration || 1200 );
|
|
3284
|
+
const detectedCount = metricsData.filter( ( m ) => m.contextsEvaluated?.[0]?.isDetected ).length || 1;
|
|
3285
|
+
const secondsPerMetric = Math.floor( totalDuration / Math.max( detectedCount, 1 ) );
|
|
3286
|
+
|
|
3287
|
+
const formattedMetrics = metricsData.map( ( metric, index ) => {
|
|
3288
|
+
const isDetected = metric.contextsEvaluated?.[0]?.isDetected;
|
|
3289
|
+
|
|
3290
|
+
let startTime = '';
|
|
3291
|
+
let endTime = '';
|
|
3292
|
+
|
|
3293
|
+
if ( isDetected ) {
|
|
3294
|
+
const startSeconds = currentSeconds;
|
|
3295
|
+
const endSeconds = Math.min( currentSeconds + secondsPerMetric, totalDuration );
|
|
3296
|
+
|
|
3297
|
+
startTime = formatDuration( startSeconds );
|
|
3298
|
+
endTime = formatDuration( endSeconds );
|
|
3299
|
+
|
|
3300
|
+
currentSeconds = endSeconds;
|
|
3301
|
+
}
|
|
3302
|
+
|
|
3303
|
+
return {
|
|
3304
|
+
metricId: metric.metricId,
|
|
3305
|
+
metricName: formatMetricName( metric.metricName ),
|
|
3306
|
+
metricType: metric.metricType,
|
|
3307
|
+
status: metric.contextsEvaluated?.[0]?.isDetected === false ? 'fail':'pass',
|
|
3308
|
+
contextsEvaluated: metric.contextsEvaluated?.[0]?.isDetected,
|
|
3309
|
+
startTime: startTime,
|
|
3310
|
+
endTime: endTime,
|
|
3311
|
+
evidence_quote: metric.contextsEvaluated?.[0]?.isDetected === false ? '' : metric.evidence_quote,
|
|
3312
|
+
};
|
|
3313
|
+
} ) || [];
|
|
3314
|
+
|
|
3315
|
+
// Fetch signed URL for audio
|
|
3316
|
+
const audioS3Parts = parseS3Uri( source?.s3_audio_path );
|
|
3317
|
+
const audioSignedUrl = audioS3Parts ? await signedUrl( { ...audioS3Parts, key: 'aws' } ) : null;
|
|
3318
|
+
// Fetch and format transcript
|
|
3319
|
+
const formattedTranscript = await getFormattedTranscript( source?.s3_transcript_path );
|
|
3320
|
+
|
|
3321
|
+
// Get executive summary
|
|
3322
|
+
// const executiveSummary = source?.cohort_analysis_results?.[0]?.executive_summary || {};
|
|
3323
|
+
|
|
3324
|
+
const response = {
|
|
3325
|
+
doc_id: source?.doc_id,
|
|
3326
|
+
// storeName: source?.storeName,
|
|
3327
|
+
// storeID: source?.storeID,
|
|
3328
|
+
// staffID: source?.staffID,
|
|
3329
|
+
// state: source?.state,
|
|
3330
|
+
// moduleType: source?.moduleType,
|
|
3331
|
+
// fileDate: source?.filedate,
|
|
3332
|
+
// processTimestamp: source?.processTimestamp,
|
|
3333
|
+
duration: formatDuration( source?.duration ),
|
|
3334
|
+
audio_url: audioSignedUrl,
|
|
3335
|
+
// s3_transcript_path: source?.s3_transcript_path,
|
|
3336
|
+
transcript_summary: formattedTranscript,
|
|
3337
|
+
// overall_score: executiveSummary?.overall_score,
|
|
3338
|
+
// conversation_summary: executiveSummary?.conversation_summary,
|
|
3339
|
+
// key_pointers: executiveSummary?.key_pointers,
|
|
3340
|
+
metrics_results: formattedMetrics,
|
|
3341
|
+
};
|
|
3342
|
+
|
|
3343
|
+
return res.sendSuccess( { result: response } );
|
|
3344
|
+
} catch ( error ) {
|
|
3345
|
+
const err = error.message || 'Internal Server Error';
|
|
3346
|
+
logger.error( { message: error, data: req.params, function: 'eyetestAudit-controller-getConversationDetails' } );
|
|
3347
|
+
return res.sendError( err, 500 );
|
|
3348
|
+
}
|
|
3349
|
+
}
|
package/src/dtos/audit.dtos.js
CHANGED
|
@@ -418,3 +418,14 @@ export const getEmailConfigSchema = joi.object( {
|
|
|
418
418
|
export const getEmailConfigValid = {
|
|
419
419
|
query: getEmailConfigSchema,
|
|
420
420
|
};
|
|
421
|
+
|
|
422
|
+
export const getConversationDetailsSchema = joi.object( {
|
|
423
|
+
id: joi.string().required().messages( {
|
|
424
|
+
'any.required': 'ID is required',
|
|
425
|
+
'string.empty': 'ID cannot be empty',
|
|
426
|
+
} ),
|
|
427
|
+
} );
|
|
428
|
+
|
|
429
|
+
export const getConversationDetailsValid = {
|
|
430
|
+
params: getConversationDetailsSchema,
|
|
431
|
+
};
|
|
@@ -45,7 +45,10 @@ auditRouter.get( '/get-user-audit-count-mtd', isAllowedSessionHandler, getUserAu
|
|
|
45
45
|
|
|
46
46
|
// audit log
|
|
47
47
|
auditRouter.get( '/audit-view-logs', isAllowedSessionHandler, accessVerification( { userType: [ 'tango' ] } ), validate( auditViewLogValid ), auditViewLogs );
|
|
48
|
+
|
|
49
|
+
|
|
48
50
|
export default auditRouter;
|
|
49
51
|
|
|
50
52
|
//
|
|
51
53
|
auditRouter.post( '/re-push-audit-stores', validate( rePushAuditStoresValid ), rePushAuditStores );
|
|
54
|
+
|
|
@@ -1,8 +1,8 @@
|
|
|
1
1
|
import { Router } from 'express';
|
|
2
2
|
import { isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
|
|
3
3
|
import { accessVerification } from 'tango-app-api-middleware';
|
|
4
|
-
import { cancelValid, clusterListValid, emailAlertLogValid, getEmailConfigValid, getFileHistoryValid, getFileValid, getListValid, getUserConfigValid, rmListValid, saveValid, summaryCardValid, summaryListValid, updateEmailConfigValid, updateUserConfigValid, userAuditedDataValid, viewFileValid } from '../dtos/eyeTestAudit.dtos.js';
|
|
5
|
-
import { addUserConfig, cancel, clusterList, emailAlertLog, getEmailConfig, getFile, getFileHistory, getList, getUserConfig, rmList, save, summaryCard, summaryList, updateEmailConfig, updateEmailConfig1, updateUserConfig, userAuditedData, viewFile } from '../controllers/eyeTestAudit.controllers.js';
|
|
4
|
+
import { cancelValid, clusterListValid, emailAlertLogValid, getConversationDetailsValid, getEmailConfigValid, getFileHistoryValid, getFileValid, getListValid, getUserConfigValid, rmListValid, saveValid, summaryCardValid, summaryListValid, updateEmailConfigValid, updateUserConfigValid, userAuditedDataValid, viewFileValid } from '../dtos/eyeTestAudit.dtos.js';
|
|
5
|
+
import { addUserConfig, cancel, clusterList, emailAlertLog, getConversationDetails, getEmailConfig, getFile, getFileHistory, getList, getUserConfig, rmList, save, summaryCard, summaryList, updateEmailConfig, updateEmailConfig1, updateUserConfig, userAuditedData, viewFile } from '../controllers/eyeTestAudit.controllers.js';
|
|
6
6
|
import { isSuperAdmin, roleValidation, sendAlert } from '../validation/eyeTest.validation.js';
|
|
7
7
|
|
|
8
8
|
export const eyeTestAuditRouter = Router();
|
|
@@ -15,6 +15,10 @@ eyeTestAuditRouter.post( '/cancel', isAllowedSessionHandler, accessVerification(
|
|
|
15
15
|
eyeTestAuditRouter.get( '/get-file-history/:id/:type', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( getFileHistoryValid ), getFileHistory );
|
|
16
16
|
eyeTestAuditRouter.get( '/user-audited-data/:id', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( userAuditedDataValid ), userAuditedData );
|
|
17
17
|
|
|
18
|
+
// enhancement of march of 2026
|
|
19
|
+
|
|
20
|
+
eyeTestAuditRouter.get( '/get-conversation-details/:id', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( getConversationDetailsValid ), getConversationDetails );
|
|
21
|
+
|
|
18
22
|
// enhancement of aug of 2025
|
|
19
23
|
|
|
20
24
|
eyeTestAuditRouter.post( '/summary-list', isAllowedSessionHandler, accessVerification( { userType: [ 'tango', 'client' ] } ), validate( summaryListValid ), roleValidation, summaryList );
|
|
@@ -45,8 +45,6 @@ export async function validateUserAudit( req, res, next ) {
|
|
|
45
45
|
const userAuditDetails = await findOneUserAudit( { _id: inputData.auditId } );
|
|
46
46
|
if ( !userAuditDetails ) {
|
|
47
47
|
return res.sendError( 'No Data Available' );
|
|
48
|
-
} else if ( userAuditDetails && userAuditDetails.auditStatus == 'completed' ) {
|
|
49
|
-
return res.sendError( 'The Audit file has been already submitted', 403 );
|
|
50
48
|
} else if ( userAuditDetails && userAuditDetails.auditStatus == 'skipped' ) {
|
|
51
49
|
return res.sendError( 'The Audit file Assigned to some one else', 203 );
|
|
52
50
|
} else {
|
|
@@ -153,13 +151,11 @@ export async function mapCustomer( sourceMappingData, customerData ) {
|
|
|
153
151
|
try {
|
|
154
152
|
const filterCustomerData = await filteredCustomerMap( customerData );
|
|
155
153
|
logger.info( { filterCustomerData: filterCustomerData } );
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
const mappedArrays = await Promise.all( promises );
|
|
162
|
-
return mappedArrays.flat();
|
|
154
|
+
// Build the lookup once, then retag every row in a single pass. This is
|
|
155
|
+
// O(rows + mappedIds) and scales for concurrent submissions, versus the old
|
|
156
|
+
// O(customers x mappedIds x rows) scan that also re-read its own mutations.
|
|
157
|
+
const customerMap = buildCustomerMap( filterCustomerData );
|
|
158
|
+
return mapFunction( sourceMappingData, customerMap );
|
|
163
159
|
} catch ( error ) {
|
|
164
160
|
logger.error( { error: error, type: 'mapCustomer' } );
|
|
165
161
|
return error;
|
|
@@ -170,22 +166,39 @@ const filteredCustomerMap = async ( data ) => {
|
|
|
170
166
|
return data.filter( ( map ) => map.count >= 1 );
|
|
171
167
|
};
|
|
172
168
|
|
|
173
|
-
|
|
169
|
+
// Lookup of original cluster id (child img_name) -> the customer it was tagged
|
|
170
|
+
// into. Last write wins if the same id is referenced by more than one group, so
|
|
171
|
+
// the result is deterministic regardless of customer order.
|
|
172
|
+
const buildCustomerMap = ( filterData ) => {
|
|
173
|
+
const customerMap = new Map();
|
|
174
174
|
for ( const data of filterData ) {
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
}
|
|
175
|
+
if ( !data?.mappedid ) continue;
|
|
176
|
+
for ( const mappedId of data.mappedid ) {
|
|
177
|
+
customerMap.set( mappedId.img_name, {
|
|
178
|
+
parentId: data.img_name,
|
|
179
|
+
demographic: data.demographic || '',
|
|
180
|
+
// The chosen representative (img_name === parent) keeps its query flag so
|
|
181
|
+
// it resurfaces in reaudit; every other merged image is cleared so it does
|
|
182
|
+
// not (query_status is the reaudit gate in getFilterData / getAuditFilterData).
|
|
183
|
+
isChild: mappedId.img_name !== data.img_name,
|
|
184
|
+
} );
|
|
186
185
|
}
|
|
187
186
|
}
|
|
188
|
-
return
|
|
187
|
+
return customerMap;
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
// Single pass over the master-json rows. Each row's ORIGINAL temp_ids is read
|
|
191
|
+
// exactly once and reassigned to its customer's parent id, so every row sharing
|
|
192
|
+
// a temp_ids is retagged and the retag never feeds back into later lookups.
|
|
193
|
+
const mapFunction = ( personData, customerMap ) => {
|
|
194
|
+
for ( const item of personData ) {
|
|
195
|
+
const mapped = customerMap.get( item.temp_ids );
|
|
196
|
+
if ( !mapped ) continue;
|
|
197
|
+
item.temp_ids = mapped.parentId;
|
|
198
|
+
item.query_status = mapped.isChild ? '' : item.query_status;
|
|
199
|
+
item.demographic = mapped.demographic;
|
|
200
|
+
}
|
|
201
|
+
return personData;
|
|
189
202
|
};
|
|
190
203
|
|
|
191
204
|
export async function isAuditInputFolderExist( req, res, next ) {
|
|
@@ -347,7 +360,8 @@ export async function getAuditFilterData( data ) {
|
|
|
347
360
|
( item ) =>
|
|
348
361
|
item.temp_ids < 20000 &&
|
|
349
362
|
item.temp_ids > 0 &&
|
|
350
|
-
item.person_status === ''
|
|
363
|
+
item.person_status === '' &&
|
|
364
|
+
item.query_status !== '',
|
|
351
365
|
);
|
|
352
366
|
const finalResult = await filterData.map( ( i ) => {
|
|
353
367
|
return { ...i, img: i.index.concat( '_', i.temp_ids ) };
|