tango-app-api-infra 3.9.4 → 3.9.5-vms.10
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 +3 -3
- package/src/controllers/footfallDirectory.controllers.js +526 -9
- package/src/dtos/footfallDirectory.dtos.js +59 -54
- package/src/routes/footfallDirectory.routes.js +10 -4
- package/src/services/vmsStoreRequest.service.js +4 -0
- package/src/validations/footfallDirectory.validation.js +554 -1
package/package.json
CHANGED
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "tango-app-api-infra",
|
|
3
|
-
"version": "3.9.
|
|
3
|
+
"version": "3.9.5-vms.10",
|
|
4
4
|
"description": "infra",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"scripts": {
|
|
8
|
-
"start": "nodemon --exec \"eslint --fix . && node
|
|
8
|
+
"start": "nodemon --exec \"eslint --fix . && node app.js\""
|
|
9
9
|
},
|
|
10
10
|
"engines": {
|
|
11
11
|
"node": ">=18.10.0"
|
|
@@ -27,7 +27,7 @@
|
|
|
27
27
|
"mongodb": "^6.4.0",
|
|
28
28
|
"nodemon": "^3.1.0",
|
|
29
29
|
"swagger-ui-express": "^5.0.0",
|
|
30
|
-
"tango-api-schema": "^2.
|
|
30
|
+
"tango-api-schema": "^2.4.28",
|
|
31
31
|
"tango-app-api-middleware": "^3.1.93",
|
|
32
32
|
"winston": "^3.12.0",
|
|
33
33
|
"winston-daily-rotate-file": "^5.0.0"
|
|
@@ -1,11 +1,12 @@
|
|
|
1
1
|
import { chunkArray, download, logger, sendMessageToFIFOQueue, sendMessageToQueue } from 'tango-app-api-middleware';
|
|
2
|
-
import { bulkUpdate, getOpenSearchById, getOpenSearchCount, getOpenSearchData, insertWithId, updateOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
|
|
2
|
+
import { bulkUpdate, getOpenSearchById, getOpenSearchCount, getOpenSearchData, insertWithId, updateOpenSearchData, upsertOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
|
|
3
3
|
import { findOneStore } from '../services/store.service.js';
|
|
4
4
|
import { countDocumnetsCamera } from '../services/camera.service.js';
|
|
5
5
|
import { findOneRevopDownload, upsertRevopDownload } from '../services/revopDownload.service.js';
|
|
6
6
|
import dayjs from 'dayjs';
|
|
7
7
|
import utc from 'dayjs/plugin/utc.js';
|
|
8
8
|
import timezone from 'dayjs/plugin/timezone.js';
|
|
9
|
+
import { findUser } from '../services/user.service.js';
|
|
9
10
|
|
|
10
11
|
dayjs.extend( utc );
|
|
11
12
|
dayjs.extend( timezone );
|
|
@@ -133,7 +134,7 @@ async function bulkUpdateStatusToPending( indexName, inputData ) {
|
|
|
133
134
|
}
|
|
134
135
|
}
|
|
135
136
|
|
|
136
|
-
export async function
|
|
137
|
+
export async function ticketSummary1( req, res ) {
|
|
137
138
|
try {
|
|
138
139
|
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
139
140
|
const inputData = req.query;
|
|
@@ -215,7 +216,59 @@ export async function ticketSummary( req, res ) {
|
|
|
215
216
|
}
|
|
216
217
|
}
|
|
217
218
|
|
|
218
|
-
export async function
|
|
219
|
+
export async function ticketSummary( req, res ) {
|
|
220
|
+
try {
|
|
221
|
+
let result = '';
|
|
222
|
+
const userInfo = req.user;
|
|
223
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
|
|
224
|
+
|
|
225
|
+
if ( req.user.userType =='tango' ) {
|
|
226
|
+
result ={
|
|
227
|
+
totalTickets: 0,
|
|
228
|
+
averageAccuracyOverAll: 0,
|
|
229
|
+
openTickets: 0,
|
|
230
|
+
openInfraIssues: 0,
|
|
231
|
+
inprogress: 0,
|
|
232
|
+
closedTickets: 0,
|
|
233
|
+
ticketAccuracyAbove: '0%',
|
|
234
|
+
ticketAccuracyBelow: '0%',
|
|
235
|
+
};
|
|
236
|
+
} else {
|
|
237
|
+
result = req.user.role === 'superadmin'?
|
|
238
|
+
{
|
|
239
|
+
totalTickets: 0,
|
|
240
|
+
openTickets: 0,
|
|
241
|
+
inprogress: 0,
|
|
242
|
+
closedTickets: 0,
|
|
243
|
+
dueToday: 0,
|
|
244
|
+
Expired: 0,
|
|
245
|
+
underTangoReview: 0,
|
|
246
|
+
avgTicket: '0%',
|
|
247
|
+
avgAccuracy: '0%',
|
|
248
|
+
} :
|
|
249
|
+
req.user.role === 'user'? 'NA':
|
|
250
|
+
ticketsFeature?
|
|
251
|
+
{
|
|
252
|
+
totalTickets: 0,
|
|
253
|
+
openTickets: 0,
|
|
254
|
+
inprogress: 0,
|
|
255
|
+
closedTickets: 0,
|
|
256
|
+
dueToday: 0,
|
|
257
|
+
Expired: 0,
|
|
258
|
+
avgTicket: '0%',
|
|
259
|
+
avgAccuracy: '0%',
|
|
260
|
+
}: 'NA';
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
return res.sendSuccess( { result: result } );
|
|
264
|
+
} catch ( error ) {
|
|
265
|
+
const err = error.message || 'Internal Server Error';
|
|
266
|
+
logger.error( { error: error, messgage: req.query } );
|
|
267
|
+
return res.sendSuccess( err, 500 );
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
export async function ticketList1( req, res ) {
|
|
219
272
|
try {
|
|
220
273
|
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
221
274
|
const inputData = req.query;
|
|
@@ -436,6 +489,165 @@ export async function ticketList( req, res ) {
|
|
|
436
489
|
}
|
|
437
490
|
}
|
|
438
491
|
|
|
492
|
+
export async function ticketList( req, res ) {
|
|
493
|
+
try {
|
|
494
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
495
|
+
const inputData= req.query;
|
|
496
|
+
const userInfo = req.user;
|
|
497
|
+
const limit =inputData?.limit || 10;
|
|
498
|
+
const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
|
|
499
|
+
inputData.clientId = inputData.clientId.split( ',' ); // convert strig to array
|
|
500
|
+
inputData.storeId = inputData.storeId.split( ',' );
|
|
501
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
|
|
502
|
+
|
|
503
|
+
|
|
504
|
+
const searchQuery = {
|
|
505
|
+
|
|
506
|
+
size: limit, // or use parseInt(req.query.limit) for dynamic
|
|
507
|
+
from: offset, // or use parseInt(req.query.offset) for dynamic
|
|
508
|
+
sort: [ { 'createdAt': { order: 'desc' } } ],
|
|
509
|
+
query: {
|
|
510
|
+
bool: {
|
|
511
|
+
must: [
|
|
512
|
+
{
|
|
513
|
+
terms: {
|
|
514
|
+
'clientId.keyword': Array.isArray( inputData.clientId ) ?
|
|
515
|
+
inputData.clientId :
|
|
516
|
+
[ inputData.clientId ],
|
|
517
|
+
},
|
|
518
|
+
},
|
|
519
|
+
],
|
|
520
|
+
},
|
|
521
|
+
},
|
|
522
|
+
};
|
|
523
|
+
|
|
524
|
+
// Example: Filtering by storeId if present in the query
|
|
525
|
+
if ( inputData.storeId ) {
|
|
526
|
+
searchQuery.query.bool.must.push( {
|
|
527
|
+
terms: {
|
|
528
|
+
'storeId.keyword': Array.isArray( inputData.storeId ) ?
|
|
529
|
+
inputData.storeId :
|
|
530
|
+
[ inputData.storeId ],
|
|
531
|
+
},
|
|
532
|
+
} );
|
|
533
|
+
}
|
|
534
|
+
|
|
535
|
+
|
|
536
|
+
// You can add more filters as needed
|
|
537
|
+
logger.info( { searchQuery, index: openSearch.footfallDirectory } );
|
|
538
|
+
const searchResult = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
|
|
539
|
+
|
|
540
|
+
const count = searchResult?.body?.hits?.total?.value || 0;
|
|
541
|
+
if ( count === 0 ) {
|
|
542
|
+
return res.sendError( 'no data found', 204 );
|
|
543
|
+
}
|
|
544
|
+
const ticketListData = searchResult?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
|
|
545
|
+
|
|
546
|
+
let temp =[];
|
|
547
|
+
if ( req.user.userType =='tango' ) {
|
|
548
|
+
if ( inputData.tangotype == 'store' ) {
|
|
549
|
+
for ( let item of ticketListData ) {
|
|
550
|
+
temp.push( {
|
|
551
|
+
|
|
552
|
+
ticketId: item?.ticketId,
|
|
553
|
+
storeId: item?.storeId,
|
|
554
|
+
storeName: item?.storeName,
|
|
555
|
+
|
|
556
|
+
ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
|
|
557
|
+
issueDate: item?.dateString,
|
|
558
|
+
dueDate: '',
|
|
559
|
+
footfall: item?.footfallCount,
|
|
560
|
+
storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
|
|
561
|
+
reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
|
|
562
|
+
approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
|
|
563
|
+
tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
|
|
564
|
+
status: item?.status,
|
|
565
|
+
|
|
566
|
+
} );
|
|
567
|
+
}
|
|
568
|
+
} else {
|
|
569
|
+
for ( let item of ticketListData ) {
|
|
570
|
+
temp.push( {
|
|
571
|
+
|
|
572
|
+
ticketId: item?.ticketId,
|
|
573
|
+
storeId: item?.storeId,
|
|
574
|
+
storeName: item?.storeName,
|
|
575
|
+
|
|
576
|
+
ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
|
|
577
|
+
issueDate: item?.dateString,
|
|
578
|
+
footfall: item?.footfallCount,
|
|
579
|
+
dueDate: '',
|
|
580
|
+
type: item.type || 'store',
|
|
581
|
+
storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
|
|
582
|
+
reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
|
|
583
|
+
approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
|
|
584
|
+
tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
|
|
585
|
+
status: item?.status,
|
|
586
|
+
tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
|
|
587
|
+
|
|
588
|
+
} );
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
} else {
|
|
592
|
+
if ( req.user.role === 'superadmin' ) {
|
|
593
|
+
for ( let item of ticketListData ) {
|
|
594
|
+
temp.push( {
|
|
595
|
+
|
|
596
|
+
ticketId: item?.ticketId,
|
|
597
|
+
storeId: item?.storeId,
|
|
598
|
+
storeName: item?.storeName,
|
|
599
|
+
|
|
600
|
+
ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
|
|
601
|
+
issueDate: item?.dateString,
|
|
602
|
+
dueDate: '',
|
|
603
|
+
footfall: item?.footfallCount,
|
|
604
|
+
|
|
605
|
+
type: item.type || 'store',
|
|
606
|
+
storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
|
|
607
|
+
reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
|
|
608
|
+
approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
|
|
609
|
+
tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
|
|
610
|
+
status: item?.status,
|
|
611
|
+
tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
|
|
612
|
+
approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail||'--',
|
|
613
|
+
|
|
614
|
+
} );
|
|
615
|
+
}
|
|
616
|
+
} else if ( req.user.role === 'user' ) {
|
|
617
|
+
temp = [];
|
|
618
|
+
} else if ( ticketsFeature ) {
|
|
619
|
+
for ( let item of ticketListData ) {
|
|
620
|
+
temp.push( {
|
|
621
|
+
|
|
622
|
+
ticketId: item?.ticketId,
|
|
623
|
+
storeId: item?.storeId,
|
|
624
|
+
storeName: item?.storeName,
|
|
625
|
+
ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
|
|
626
|
+
issueDate: item?.dateString,
|
|
627
|
+
footfall: item?.footfallCount,
|
|
628
|
+
dueDate: '',
|
|
629
|
+
type: item.type || 'store',
|
|
630
|
+
storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
|
|
631
|
+
reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
|
|
632
|
+
|
|
633
|
+
status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status|| '--',
|
|
634
|
+
ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail||'--',
|
|
635
|
+
|
|
636
|
+
} );
|
|
637
|
+
}
|
|
638
|
+
} else {
|
|
639
|
+
temp =[];
|
|
640
|
+
}
|
|
641
|
+
}
|
|
642
|
+
|
|
643
|
+
return res.sendSuccess( { result: temp } );
|
|
644
|
+
} catch ( error ) {
|
|
645
|
+
const err = error.message || 'Internal Server Error';
|
|
646
|
+
logger.error( { error: error, messgage: req.query } );
|
|
647
|
+
return res.sendSuccess( err, 500 );
|
|
648
|
+
}
|
|
649
|
+
}
|
|
650
|
+
|
|
439
651
|
export async function getTickets( req, res ) {
|
|
440
652
|
try {
|
|
441
653
|
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
@@ -444,7 +656,7 @@ export async function getTickets( req, res ) {
|
|
|
444
656
|
const skip = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
|
|
445
657
|
inputData.storeId = inputData.storeId.split( ',' ); // convert strig to array
|
|
446
658
|
logger.info( { inputData: inputData, limit: limit, skip: skip } );
|
|
447
|
-
let source = [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'junkCount', 'junkACCount', 'comments', 'employee', 'houseKeeping', 'junk', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'approverRole', 'approverUserName', 'approverEmail' ];
|
|
659
|
+
let source = [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'junkCount', 'junkACCount', 'comments', 'employee', 'houseKeeping', 'junk', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'approverRole', 'approverUserName', 'approverEmail' ];
|
|
448
660
|
let filter = [
|
|
449
661
|
|
|
450
662
|
{
|
|
@@ -520,10 +732,10 @@ export async function getTickets( req, res ) {
|
|
|
520
732
|
},
|
|
521
733
|
},
|
|
522
734
|
} );
|
|
523
|
-
source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'email', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
|
|
524
|
-
inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
|
|
525
|
-
inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
|
|
526
|
-
inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkACCount', 'junkStatus', 'approverRole', 'approverUserName', 'approverEmail' ] : [];
|
|
735
|
+
source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'email', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
|
|
736
|
+
inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
|
|
737
|
+
inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
|
|
738
|
+
inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'duplicateCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkACCount', 'junkStatus', 'approverRole', 'approverUserName', 'approverEmail' ] : [];
|
|
527
739
|
}
|
|
528
740
|
|
|
529
741
|
if ( inputData.action ) {
|
|
@@ -632,7 +844,10 @@ export async function getTickets( req, res ) {
|
|
|
632
844
|
storeId: hit._source.storeId,
|
|
633
845
|
dateString: hit?._source?.dateString,
|
|
634
846
|
ticketName: hit?._source?.ticketName,
|
|
635
|
-
status: hit?._source?.status,
|
|
847
|
+
status: hit?._source?.status?.revicedFootfall,
|
|
848
|
+
revicedPerc: hit?._source?.revicedPerc,
|
|
849
|
+
revicedFootfall: hit?._source?.revicedFootfall,
|
|
850
|
+
mappingInfo: hit?._source?.mappingInfo,
|
|
636
851
|
employeeStatus: hit?._source?.employeeStatus,
|
|
637
852
|
houseKeepingStatus: hit?._source?.houseKeepingStatus,
|
|
638
853
|
duplicateStatus: hit?._source?.duplicateStatus,
|
|
@@ -1504,3 +1719,305 @@ async function extractTempIds( document ) {
|
|
|
1504
1719
|
return result;
|
|
1505
1720
|
}
|
|
1506
1721
|
|
|
1722
|
+
export async function reviewerList( req, res ) {
|
|
1723
|
+
try {
|
|
1724
|
+
const inputData = req.query;
|
|
1725
|
+
// Build the query for users who have rolespermission with featureName "FootfallDirectory",
|
|
1726
|
+
// and a module "Reviewer" where isAdd or isEdit is true.
|
|
1727
|
+
const reviewerRoleQuery = {
|
|
1728
|
+
'clientId': inputData.clientId,
|
|
1729
|
+
'rolespermission': {
|
|
1730
|
+
$elemMatch: {
|
|
1731
|
+
featureName: 'FootfallDirectory',
|
|
1732
|
+
modules: {
|
|
1733
|
+
$elemMatch: {
|
|
1734
|
+
name: 'Reviewer',
|
|
1735
|
+
$or: [ { isAdd: true }, { isEdit: true } ],
|
|
1736
|
+
},
|
|
1737
|
+
},
|
|
1738
|
+
},
|
|
1739
|
+
},
|
|
1740
|
+
};
|
|
1741
|
+
|
|
1742
|
+
const getUserlist = await findUser( reviewerRoleQuery, { userName: 1, email: 1, role: 1 } );
|
|
1743
|
+
return res.sendSuccess( getUserlist|| [] );
|
|
1744
|
+
} catch ( error ) {
|
|
1745
|
+
const err = error.message || 'Internal Server Error';
|
|
1746
|
+
return res.sendError( err, 500 );
|
|
1747
|
+
}
|
|
1748
|
+
}
|
|
1749
|
+
|
|
1750
|
+
export async function openTicketList( req, res ) {
|
|
1751
|
+
try {
|
|
1752
|
+
const inputData = req.body;
|
|
1753
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1754
|
+
|
|
1755
|
+
// INSERT_YOUR_CODE
|
|
1756
|
+
// Build the query to match storeId(s) and dateString range [fromDate, toDate], format: 'yyyy-mm-dd'
|
|
1757
|
+
const { clientId, fromDate, toDate } = inputData;
|
|
1758
|
+
|
|
1759
|
+
const filter = [
|
|
1760
|
+
{
|
|
1761
|
+
terms: {
|
|
1762
|
+
clientId: Array.isArray( clientId ) ? clientId : [ clientId ],
|
|
1763
|
+
},
|
|
1764
|
+
},
|
|
1765
|
+
{
|
|
1766
|
+
range: {
|
|
1767
|
+
dateString: {
|
|
1768
|
+
gte: fromDate,
|
|
1769
|
+
lte: toDate,
|
|
1770
|
+
format: 'yyyy-MM-dd',
|
|
1771
|
+
},
|
|
1772
|
+
},
|
|
1773
|
+
},
|
|
1774
|
+
];
|
|
1775
|
+
|
|
1776
|
+
const openSearchQuery = {
|
|
1777
|
+
size: 10000,
|
|
1778
|
+
query: {
|
|
1779
|
+
bool: {
|
|
1780
|
+
filter: filter,
|
|
1781
|
+
},
|
|
1782
|
+
},
|
|
1783
|
+
_source: [ 'ticketId', 'storeName', 'revicedFootfall', 'footfallCount', 'revicedPerc' ],
|
|
1784
|
+
};
|
|
1785
|
+
|
|
1786
|
+
|
|
1787
|
+
// Assuming getOpenSearchData and openSearch.footfallDirectoryTagging are available
|
|
1788
|
+
const result = await getOpenSearchData( openSearch.footfallDirectory, openSearchQuery );
|
|
1789
|
+
const getUserlist = result?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
|
|
1790
|
+
return res.sendSuccess( getUserlist|| [] );
|
|
1791
|
+
} catch ( error ) {
|
|
1792
|
+
const err = error.message || 'Internal Server Error';
|
|
1793
|
+
logger.error( { error: error, function: 'openTicketList' } );
|
|
1794
|
+
return res.sendError( err, 500 );
|
|
1795
|
+
}
|
|
1796
|
+
}
|
|
1797
|
+
|
|
1798
|
+
export async function assignTicket( req, res ) {
|
|
1799
|
+
try {
|
|
1800
|
+
const inputData = req.body;
|
|
1801
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1802
|
+
|
|
1803
|
+
// INSERT_YOUR_CODE
|
|
1804
|
+
// Build the query to match storeId(s) and dateString range [fromDate, toDate], format: 'yyyy-mm-dd'
|
|
1805
|
+
const { email, userName, role, actionType } = inputData;
|
|
1806
|
+
|
|
1807
|
+
// INSERT_YOUR_CODE
|
|
1808
|
+
|
|
1809
|
+
// Find and update mappingInfo fields for the provided ticketId and actionType
|
|
1810
|
+
// Requires ticketId in inputData
|
|
1811
|
+
const { ticketId } = inputData;
|
|
1812
|
+
if ( !ticketId ) {
|
|
1813
|
+
return res.sendError( 'ticketId is required', 400 );
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
// Build the OpenSearch update-by-query body
|
|
1817
|
+
const updateBody = {
|
|
1818
|
+
script: {
|
|
1819
|
+
source: `
|
|
1820
|
+
if (ctx._source.mappingInfo != null) {
|
|
1821
|
+
for (int i = 0; i < ctx._source.mappingInfo.length; i++) {
|
|
1822
|
+
if (ctx._source.mappingInfo[i].type == params.actionType) {
|
|
1823
|
+
ctx._source.mappingInfo[i].createdByEmail = params.email;
|
|
1824
|
+
ctx._source.mappingInfo[i].createdByUserName = params.userName;
|
|
1825
|
+
ctx._source.mappingInfo[i].createdByRole = params.role;
|
|
1826
|
+
}
|
|
1827
|
+
}
|
|
1828
|
+
}
|
|
1829
|
+
`,
|
|
1830
|
+
lang: 'painless',
|
|
1831
|
+
params: {
|
|
1832
|
+
email,
|
|
1833
|
+
userName,
|
|
1834
|
+
role,
|
|
1835
|
+
actionType,
|
|
1836
|
+
},
|
|
1837
|
+
},
|
|
1838
|
+
query: {
|
|
1839
|
+
bool: {
|
|
1840
|
+
must: [
|
|
1841
|
+
{ term: { 'ticketId.keyword': ticketId } },
|
|
1842
|
+
{
|
|
1843
|
+
nested: {
|
|
1844
|
+
path: 'mappingInfo',
|
|
1845
|
+
query: {
|
|
1846
|
+
bool: {
|
|
1847
|
+
must: [
|
|
1848
|
+
{ match: { 'mappingInfo.type': actionType } },
|
|
1849
|
+
],
|
|
1850
|
+
},
|
|
1851
|
+
},
|
|
1852
|
+
},
|
|
1853
|
+
},
|
|
1854
|
+
],
|
|
1855
|
+
},
|
|
1856
|
+
},
|
|
1857
|
+
};
|
|
1858
|
+
|
|
1859
|
+
// Call OpenSearch _update_by_query to update doc(s) where ticketId and mappingInfo[i].type == actionType
|
|
1860
|
+
const response = await upsertOpenSearchData(
|
|
1861
|
+
openSearch.footfallDirectory,
|
|
1862
|
+
'11-1716_2025-11-20_footfall-directory-tagging',
|
|
1863
|
+
updateBody, // custom arg to indicate passthrough for update-by-query, depends on helper implementation
|
|
1864
|
+
);
|
|
1865
|
+
|
|
1866
|
+
|
|
1867
|
+
logger.info( { response } );
|
|
1868
|
+
|
|
1869
|
+
return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
|
|
1870
|
+
} catch ( error ) {
|
|
1871
|
+
const err = error.message || 'Internal Server Error';
|
|
1872
|
+
logger.error( { error: error, function: 'assignTicket' } );
|
|
1873
|
+
return res.sendError( err, 500 );
|
|
1874
|
+
}
|
|
1875
|
+
}
|
|
1876
|
+
|
|
1877
|
+
export async function updateTempStatus( req, res ) {
|
|
1878
|
+
try {
|
|
1879
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1880
|
+
const { id, status } = req.body;
|
|
1881
|
+
|
|
1882
|
+
// Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
|
|
1883
|
+
// 1. Search for all documents matching the ticket IDs
|
|
1884
|
+
const searchBody = {
|
|
1885
|
+
query: {
|
|
1886
|
+
bool: {
|
|
1887
|
+
must: [
|
|
1888
|
+
{
|
|
1889
|
+
terms: {
|
|
1890
|
+
'id.keyword': id,
|
|
1891
|
+
},
|
|
1892
|
+
},
|
|
1893
|
+
],
|
|
1894
|
+
},
|
|
1895
|
+
},
|
|
1896
|
+
_source: [ '_id' ], // Only bring _id for efficiency
|
|
1897
|
+
};
|
|
1898
|
+
|
|
1899
|
+
const searchResp = await getOpenSearchData(
|
|
1900
|
+
openSearch.revop,
|
|
1901
|
+
searchBody,
|
|
1902
|
+
);
|
|
1903
|
+
logger.info( { searchResp: searchResp } );
|
|
1904
|
+
// Extract bulk IDs to update
|
|
1905
|
+
const hits = searchResp?.body?.hits?.hits ?? [];
|
|
1906
|
+
logger.info( { hits: hits } );
|
|
1907
|
+
if ( !hits.length ) {
|
|
1908
|
+
return res.sendError( 'no data', 204 );
|
|
1909
|
+
}
|
|
1910
|
+
|
|
1911
|
+
// 2. Build bulk update commands
|
|
1912
|
+
// Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
|
|
1913
|
+
|
|
1914
|
+
// 1. Get all IDs from hits
|
|
1915
|
+
const docIdToIndex = {};
|
|
1916
|
+
hits.forEach( ( doc ) => {
|
|
1917
|
+
docIdToIndex[doc._id] = doc._index;
|
|
1918
|
+
} );
|
|
1919
|
+
const docIds = hits.map( ( doc ) => doc._id );
|
|
1920
|
+
logger.info( { docIds } );
|
|
1921
|
+
// 2. Fetch all docs by ID to get 'actions' (in chunks if large)
|
|
1922
|
+
const getBody = [];
|
|
1923
|
+
for ( const doc of hits ) {
|
|
1924
|
+
getBody.push( { _index: doc._index, _id: doc._id } );
|
|
1925
|
+
}
|
|
1926
|
+
|
|
1927
|
+
let mgetResp;
|
|
1928
|
+
try {
|
|
1929
|
+
mgetResp = await getOpenSearchData(
|
|
1930
|
+
openSearch.revop,
|
|
1931
|
+
{
|
|
1932
|
+
query: {
|
|
1933
|
+
ids: {
|
|
1934
|
+
values: docIds,
|
|
1935
|
+
},
|
|
1936
|
+
},
|
|
1937
|
+
_source: true,
|
|
1938
|
+
},
|
|
1939
|
+
);
|
|
1940
|
+
} catch ( err ) {
|
|
1941
|
+
logger.error( { error: err } );
|
|
1942
|
+
mgetResp = undefined;
|
|
1943
|
+
}
|
|
1944
|
+
logger.info( { mgetResp } );
|
|
1945
|
+
// (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
|
|
1946
|
+
// For fallback, fetch all source fields via another search
|
|
1947
|
+
let fullDocs = [];
|
|
1948
|
+
if ( mgetResp && mgetResp.body && mgetResp.body.docs && Array.isArray( mgetResp.body.docs ) ) {
|
|
1949
|
+
fullDocs = mgetResp.body.docs;
|
|
1950
|
+
} else if ( searchResp.body && searchResp.body.hits && searchResp.body.hits.hits ) {
|
|
1951
|
+
// fallback: use searchResp docs (request _source above)
|
|
1952
|
+
fullDocs = searchResp.body.hits.hits;
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
// 3. Prepare the new actions array for each doc, and set up bulk update payloads
|
|
1956
|
+
const reviewActions = [ 'approved', 'rejected' ];
|
|
1957
|
+
const docsToUpdate = [];
|
|
1958
|
+
logger.info( { fullDocs: fullDocs } );
|
|
1959
|
+
for ( const doc of fullDocs ) {
|
|
1960
|
+
const source = doc._source || doc.fields || {}; // support mget and search hits
|
|
1961
|
+
let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
|
|
1962
|
+
if ( reviewActions.includes( status ) ) {
|
|
1963
|
+
// for review: update or push 'review'
|
|
1964
|
+
let found = false;
|
|
1965
|
+
actions = actions.map( ( item ) => {
|
|
1966
|
+
if ( item.actionType === 'review' ) {
|
|
1967
|
+
found = true;
|
|
1968
|
+
return { ...item, action: status };
|
|
1969
|
+
}
|
|
1970
|
+
return item;
|
|
1971
|
+
} );
|
|
1972
|
+
if ( !found ) {
|
|
1973
|
+
actions.push( { actionType: 'review', action: status } );
|
|
1974
|
+
}
|
|
1975
|
+
} else {
|
|
1976
|
+
// tagging: update or push 'tagging'
|
|
1977
|
+
let found = false;
|
|
1978
|
+
actions = actions.map( ( item ) => {
|
|
1979
|
+
if ( item.actionType === 'tagging' ) {
|
|
1980
|
+
found = true;
|
|
1981
|
+
return { ...item, action: 'submitted' };
|
|
1982
|
+
}
|
|
1983
|
+
return item;
|
|
1984
|
+
} );
|
|
1985
|
+
if ( !found ) {
|
|
1986
|
+
actions.push( { actionType: 'tagging', action: 'submitted' } );
|
|
1987
|
+
}
|
|
1988
|
+
}
|
|
1989
|
+
docsToUpdate.push( {
|
|
1990
|
+
_index: doc._index || docIdToIndex[doc._id],
|
|
1991
|
+
_id: doc._id,
|
|
1992
|
+
actions,
|
|
1993
|
+
} );
|
|
1994
|
+
}
|
|
1995
|
+
const bulkPayload = [];
|
|
1996
|
+
// 4. Build bulk update payload
|
|
1997
|
+
for ( const doc of docsToUpdate ) {
|
|
1998
|
+
bulkPayload.push( {
|
|
1999
|
+
update: { _index: doc._index, _id: doc._id },
|
|
2000
|
+
} );
|
|
2001
|
+
bulkPayload.push( {
|
|
2002
|
+
doc: { actions: doc.actions },
|
|
2003
|
+
} );
|
|
2004
|
+
}
|
|
2005
|
+
logger.info( { bulkPayload: bulkPayload } );
|
|
2006
|
+
|
|
2007
|
+
|
|
2008
|
+
// 3. Execute bulk update
|
|
2009
|
+
const bulkResp = await bulkUpdate( bulkPayload );
|
|
2010
|
+
|
|
2011
|
+
// Count successes
|
|
2012
|
+
const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
|
|
2013
|
+
|
|
2014
|
+
logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
|
|
2015
|
+
|
|
2016
|
+
return res.sendSuccess( { updated: updatedCount } );
|
|
2017
|
+
} catch ( error ) {
|
|
2018
|
+
const err = error.message;
|
|
2019
|
+
logger.info( { error: err, function: 'updateTempStatus' } );
|
|
2020
|
+
return res.sendError( err, 500 );
|
|
2021
|
+
}
|
|
2022
|
+
}
|
|
2023
|
+
|
|
@@ -6,61 +6,16 @@ export const createTicketSchema = Joi.object().keys( {
|
|
|
6
6
|
dateString: Joi.string().required(),
|
|
7
7
|
storeId: Joi.string().required(),
|
|
8
8
|
ticketName: Joi.string().required(),
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
|
|
14
|
-
|
|
15
|
-
duplicateImages: Joi.array().items(
|
|
16
|
-
Joi.object( {
|
|
17
|
-
tempId: Joi.number().required(),
|
|
18
|
-
filePath: Joi.string().required(),
|
|
19
|
-
entryTime: Joi.string().required(),
|
|
20
|
-
exitTime: Joi.string().required(),
|
|
21
|
-
timeRange: Joi.string().required(),
|
|
22
|
-
isChecked: Joi.boolean().required(),
|
|
23
|
-
data: Joi.array().items(
|
|
24
|
-
Joi.object( {
|
|
25
|
-
tempId: Joi.number().required(),
|
|
26
|
-
filePath: Joi.string().required(),
|
|
27
|
-
entryTime: Joi.string().required(),
|
|
28
|
-
exitTime: Joi.string().required(),
|
|
29
|
-
timeRange: Joi.string().required(),
|
|
30
|
-
isChecked: Joi.boolean().required(),
|
|
31
|
-
} ),
|
|
32
|
-
).optional(),
|
|
33
|
-
} ) ).optional(),
|
|
34
|
-
|
|
35
|
-
houseKeeping: Joi.array().items( Joi.object( {
|
|
36
|
-
tempId: Joi.number().required(),
|
|
37
|
-
filePath: Joi.string().required(),
|
|
38
|
-
entryTime: Joi.string().required(),
|
|
39
|
-
exitTime: Joi.string().required(),
|
|
40
|
-
timeRange: Joi.string().required(),
|
|
41
|
-
isChecked: Joi.boolean().required(),
|
|
42
|
-
} ) ).optional(),
|
|
43
|
-
|
|
44
|
-
employee: Joi.array().items( Joi.object( {
|
|
45
|
-
tempId: Joi.number().required(),
|
|
46
|
-
filePath: Joi.string().required(),
|
|
47
|
-
entryTime: Joi.string().required(),
|
|
48
|
-
exitTime: Joi.string().required(),
|
|
49
|
-
timeRange: Joi.string().required(),
|
|
50
|
-
isChecked: Joi.boolean().required(),
|
|
51
|
-
|
|
52
|
-
} ) ).optional(),
|
|
53
|
-
|
|
54
|
-
junk: Joi.array().items( Joi.object( {
|
|
55
|
-
tempId: Joi.number().required(),
|
|
56
|
-
filePath: Joi.string().required(),
|
|
57
|
-
entryTime: Joi.string().required(),
|
|
58
|
-
exitTime: Joi.string().required(),
|
|
59
|
-
timeRange: Joi.string().required(),
|
|
60
|
-
isChecked: Joi.boolean().required(),
|
|
61
|
-
|
|
62
|
-
} ) ).optional(),
|
|
9
|
+
type: Joi.string()
|
|
10
|
+
.required()
|
|
11
|
+
.valid( 'create', 'review', 'approve', 'tangRreview' )
|
|
12
|
+
.messages( {
|
|
13
|
+
'any.only': 'type must be one of [create, review, approve, tangRreview]',
|
|
14
|
+
} ),
|
|
63
15
|
|
|
16
|
+
mode: Joi.string().valid( 'mobile', 'web' ).required().messages( {
|
|
17
|
+
'any.only': 'type must be one of [mobile,web]',
|
|
18
|
+
} ),
|
|
64
19
|
|
|
65
20
|
} );
|
|
66
21
|
|
|
@@ -139,6 +94,7 @@ export const ticketListSchema = Joi.object().keys( {
|
|
|
139
94
|
isExport: Joi.boolean().optional(),
|
|
140
95
|
sortBy: Joi.string().optional().allow( '' ),
|
|
141
96
|
sortOrder: Joi.number().valid( -1, 1 ).optional(),
|
|
97
|
+
tangoType: Joi.string().valid( 'store', 'internal', '' ).optional(),
|
|
142
98
|
fromDate: Joi.string()
|
|
143
99
|
.pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
|
|
144
100
|
.required()
|
|
@@ -483,3 +439,52 @@ export const downloadTicketsSchema = Joi.object().keys( {
|
|
|
483
439
|
export const downloadTicketsValid = {
|
|
484
440
|
query: downloadTicketsSchema,
|
|
485
441
|
};
|
|
442
|
+
|
|
443
|
+
export const reviewerListSchema = Joi.object().keys( {
|
|
444
|
+
clientId: Joi.string().required(),
|
|
445
|
+
} );
|
|
446
|
+
|
|
447
|
+
export const reviewerListValid = {
|
|
448
|
+
query: reviewerListSchema,
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
export const openTicketListSchema = Joi.object().keys( {
|
|
452
|
+
fromDate: Joi.string().required(),
|
|
453
|
+
toDate: Joi.string().required(),
|
|
454
|
+
clientId: Joi.array().items(
|
|
455
|
+
Joi.string().required(),
|
|
456
|
+
).required(),
|
|
457
|
+
|
|
458
|
+
|
|
459
|
+
} );
|
|
460
|
+
|
|
461
|
+
export const openTicketListValid = {
|
|
462
|
+
body: openTicketListSchema,
|
|
463
|
+
};
|
|
464
|
+
|
|
465
|
+
|
|
466
|
+
export const assignTicketSchema = Joi.object().keys( {
|
|
467
|
+
email: Joi.string().required(),
|
|
468
|
+
userName: Joi.string().optional(),
|
|
469
|
+
role: Joi.string().optional(),
|
|
470
|
+
actionType: Joi.string().required(),
|
|
471
|
+
ticketId: Joi.string().required(),
|
|
472
|
+
|
|
473
|
+
|
|
474
|
+
} );
|
|
475
|
+
|
|
476
|
+
export const assignTicketValid = {
|
|
477
|
+
body: assignTicketSchema,
|
|
478
|
+
};
|
|
479
|
+
|
|
480
|
+
export const updateTempStatusSchema = Joi.object().keys( {
|
|
481
|
+
id: Joi.array().items( Joi.string().required() ).required(),
|
|
482
|
+
status: Joi.string().required(),
|
|
483
|
+
type: Joi.string().required(),
|
|
484
|
+
|
|
485
|
+
|
|
486
|
+
} );
|
|
487
|
+
|
|
488
|
+
export const updateTempStatusValid = {
|
|
489
|
+
body: updateTempStatusSchema,
|
|
490
|
+
};
|
|
@@ -1,17 +1,23 @@
|
|
|
1
1
|
import express from 'express';
|
|
2
|
-
import { getClusters,
|
|
3
|
-
import { createTicket, downloadTickets, getTaggedStores, getTickets, ticketList, ticketSummary, updateStatus } from '../controllers/footfallDirectory.controllers.js';
|
|
4
|
-
import { createTicketValid, downloadTicketsValid, getTaggedStoresValid, getTicketsValid, ticketListValid, ticketSummaryValid, updateStatusValid } from '../dtos/footfallDirectory.dtos.js';
|
|
2
|
+
import { getClusters, getConfig, isGrantedUsers, isTicketExists, ticketCreation } from '../validations/footfallDirectory.validation.js';
|
|
3
|
+
import { assignTicket, createTicket, downloadTickets, getTaggedStores, getTickets, openTicketList, reviewerList, ticketList, ticketSummary, updateStatus, updateTempStatus } from '../controllers/footfallDirectory.controllers.js';
|
|
4
|
+
import { createTicketValid, downloadTicketsValid, getTaggedStoresValid, getTicketsValid, openTicketListValid, reviewerListValid, ticketListValid, ticketSummaryValid, updateStatusValid, assignTicketValid, updateTempStatusValid } from '../dtos/footfallDirectory.dtos.js';
|
|
5
5
|
import { bulkValidate, getAssinedStore, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
|
|
6
6
|
|
|
7
7
|
export const footfallDirectoryRouter = express.Router();
|
|
8
8
|
|
|
9
|
-
footfallDirectoryRouter.post( '/create-ticket', isAllowedSessionHandler, validate( createTicketValid ),
|
|
9
|
+
footfallDirectoryRouter.post( '/create-ticket', isAllowedSessionHandler, validate( createTicketValid ), isGrantedUsers, getConfig, ticketCreation, createTicket );
|
|
10
10
|
footfallDirectoryRouter.get( '/ticket-summary', isAllowedSessionHandler, bulkValidate( ticketSummaryValid ), ticketSummary );
|
|
11
11
|
|
|
12
12
|
footfallDirectoryRouter.get( '/ticket-list', isAllowedSessionHandler, bulkValidate( ticketListValid ), ticketList );
|
|
13
|
+
// footfallDirectoryRouter.get( '/ticket-list', isAllowedSessionHandler, bulkValidate( ticketListValid ), ticketList );
|
|
13
14
|
footfallDirectoryRouter.get( '/get-tickets', isAllowedSessionHandler, bulkValidate( getTicketsValid ), getTickets );
|
|
14
15
|
footfallDirectoryRouter.get( '/get-tagged-stores', isAllowedSessionHandler, bulkValidate( getTaggedStoresValid ), getAssinedStore, getClusters, getTaggedStores );
|
|
15
16
|
footfallDirectoryRouter.put( '/update-status', isAllowedSessionHandler, bulkValidate( updateStatusValid ), updateStatus );
|
|
16
17
|
footfallDirectoryRouter.get( '/download-tickets', isAllowedSessionHandler, bulkValidate( downloadTicketsValid ), isTicketExists, downloadTickets );
|
|
18
|
+
footfallDirectoryRouter.get( '/reviewer-list', isAllowedSessionHandler, bulkValidate( reviewerListValid ), reviewerList );
|
|
19
|
+
footfallDirectoryRouter.post( '/open-ticket-list', isAllowedSessionHandler, bulkValidate( openTicketListValid ), openTicketList );
|
|
20
|
+
footfallDirectoryRouter.post( '/assign-ticket', isAllowedSessionHandler, bulkValidate( assignTicketValid ), assignTicket );
|
|
21
|
+
footfallDirectoryRouter.post( '/update-temp-status', isAllowedSessionHandler, bulkValidate( updateTempStatusValid ), updateTempStatus );
|
|
22
|
+
|
|
17
23
|
|
|
@@ -1,6 +1,55 @@
|
|
|
1
|
-
import { getOpenSearchCount, logger } from 'tango-app-api-middleware';
|
|
1
|
+
import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger } from 'tango-app-api-middleware';
|
|
2
2
|
import { aggregateCluster } from '../services/cluster.service.js';
|
|
3
3
|
import { findOneRevopDownload } from '../services/revopDownload.service.js';
|
|
4
|
+
import { findOneStore } from '../services/store.service.js';
|
|
5
|
+
import { findOneClient } from '../services/client.service.js';
|
|
6
|
+
import { updateOneUpsertVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
|
|
7
|
+
|
|
8
|
+
function formatRevopTaggingHits( hits = [] ) {
|
|
9
|
+
return hits
|
|
10
|
+
.map( ( hit ) => {
|
|
11
|
+
const source = hit?._source || {};
|
|
12
|
+
if ( Object.keys( source ).length === 0 ) {
|
|
13
|
+
return null;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
const duplicateImages = Array.isArray( source.duplicateImage ) ?
|
|
17
|
+
source.duplicateImage.map( ( item ) => ( {
|
|
18
|
+
tempId: item?.tempId,
|
|
19
|
+
timeRange: item?.timeRange,
|
|
20
|
+
entryTime: item?.entryTime,
|
|
21
|
+
exitTime: item?.exitTime,
|
|
22
|
+
filePath: item?.filePath,
|
|
23
|
+
status: item?.status,
|
|
24
|
+
isChecked: Boolean( item?.isChecked ),
|
|
25
|
+
} ) ) :
|
|
26
|
+
[];
|
|
27
|
+
|
|
28
|
+
return {
|
|
29
|
+
id: hit?._id,
|
|
30
|
+
clientId: source?.clientId,
|
|
31
|
+
storeId: source?.storeId,
|
|
32
|
+
tempId: source?.tempId,
|
|
33
|
+
dateString: source?.dateString,
|
|
34
|
+
timeRange: source?.timeRange,
|
|
35
|
+
processType: source?.processType,
|
|
36
|
+
revopsType: source?.revopsType,
|
|
37
|
+
entryTime: source?.entryTime,
|
|
38
|
+
exitTime: source?.exitTime,
|
|
39
|
+
filePath: source?.filePath,
|
|
40
|
+
status: source?.status,
|
|
41
|
+
description: source?.description || '',
|
|
42
|
+
isChecked: Boolean( source?.isChecked ),
|
|
43
|
+
type: source?.type,
|
|
44
|
+
parent: source?.parent ?? null,
|
|
45
|
+
isParent: duplicateImages.length > 0 && !source?.parent,
|
|
46
|
+
createdAt: source?.createdAt,
|
|
47
|
+
updatedAt: source?.updatedAt,
|
|
48
|
+
data: duplicateImages,
|
|
49
|
+
};
|
|
50
|
+
} )
|
|
51
|
+
.filter( Boolean );
|
|
52
|
+
}
|
|
4
53
|
|
|
5
54
|
export async function isExist( req, res, next ) {
|
|
6
55
|
try {
|
|
@@ -129,3 +178,507 @@ export async function isTicketExists( req, res, next ) {
|
|
|
129
178
|
}
|
|
130
179
|
}
|
|
131
180
|
|
|
181
|
+
export async function isGrantedUsers( req, res, next ) {
|
|
182
|
+
try {
|
|
183
|
+
const userInfo = req?.user;
|
|
184
|
+
switch ( userInfo.userType ) {
|
|
185
|
+
case 'client':
|
|
186
|
+
const ticketsFeature = userInfo?.rolespermission?.find( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && m.isAdd==true ) || f.modules.find( ( m ) => m.name =='Reviewer' && m.isAdd==true ) || f.modules.find( ( m ) => m.name =='Approver' && m.isAdd==true ) ) );
|
|
187
|
+
logger.info( { ticketsFeature } );
|
|
188
|
+
if ( ticketsFeature ) {
|
|
189
|
+
return next();
|
|
190
|
+
} else {
|
|
191
|
+
return res.sendError( 'Forbidden to this action', 403 );
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
case 'tango':
|
|
195
|
+
return next();
|
|
196
|
+
default:
|
|
197
|
+
return res.sendError( 'userType doesnot match', 400 );
|
|
198
|
+
}
|
|
199
|
+
} catch ( error ) {
|
|
200
|
+
const err = error.message || 'Internal Server Error';
|
|
201
|
+
return res.sendError( err, 500 );
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
export async function getConfig( req, res, next ) {
|
|
206
|
+
try {
|
|
207
|
+
const inputData = req.body;
|
|
208
|
+
const storeKey = inputData.storeId.split( '-' )[0];
|
|
209
|
+
|
|
210
|
+
const config = await findOneClient( { clientId: storeKey }, { footfallDirectoryConfigs: 1 } );
|
|
211
|
+
logger.info( { config, storeKey } );
|
|
212
|
+
const accuracyBreach = config?.footfallDirectoryConfigs?.accuracyBreach;
|
|
213
|
+
req.accuracyBreach = accuracyBreach || '';
|
|
214
|
+
return next();
|
|
215
|
+
} catch ( error ) {
|
|
216
|
+
const err = error.message || 'Internal Server Error';
|
|
217
|
+
return res.sendError( err, 500 );
|
|
218
|
+
}
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
|
|
222
|
+
export async function ticketCreation( req, res, next ) {
|
|
223
|
+
try {
|
|
224
|
+
const inputData = req.body;
|
|
225
|
+
if ( inputData?.type !== 'create' ) {
|
|
226
|
+
return next();
|
|
227
|
+
}
|
|
228
|
+
// check the createtion permission from the user permission
|
|
229
|
+
const userInfo = req?.user;
|
|
230
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='creator' && ( m.isAdd==true || m.isEdit==true ) ) ) );
|
|
231
|
+
if ( !ticketsFeature ) {
|
|
232
|
+
return res.sendError( 'Forbidden to Create Ticket', 403 );
|
|
233
|
+
}
|
|
234
|
+
|
|
235
|
+
// get store info by the storeId into mongo db
|
|
236
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
237
|
+
|
|
238
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
239
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
// get the footfall count from opensearch
|
|
243
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
244
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
245
|
+
const getQuery = {
|
|
246
|
+
query: {
|
|
247
|
+
terms: {
|
|
248
|
+
_id: [ dateString ],
|
|
249
|
+
},
|
|
250
|
+
},
|
|
251
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
252
|
+
sort: [
|
|
253
|
+
{
|
|
254
|
+
date_iso: {
|
|
255
|
+
order: 'desc',
|
|
256
|
+
},
|
|
257
|
+
},
|
|
258
|
+
],
|
|
259
|
+
};
|
|
260
|
+
|
|
261
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
262
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
263
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
264
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
// get category details from the client level configuration
|
|
268
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
269
|
+
if ( !getConfig || getConfig == null ) {
|
|
270
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
271
|
+
}
|
|
272
|
+
|
|
273
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
274
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
275
|
+
// Initialize count object from taggingLimitation
|
|
276
|
+
const tempAcc = [];
|
|
277
|
+
const getCategory = taggingLimitation?.reduce( ( acc, item ) => {
|
|
278
|
+
if ( item?.type ) {
|
|
279
|
+
// Convert type to camelCase with "Count" suffix
|
|
280
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
281
|
+
const typeLower = item.type.toLowerCase();
|
|
282
|
+
let key;
|
|
283
|
+
if ( typeLower === 'housekeeping' ) {
|
|
284
|
+
key = 'houseKeepingCount';
|
|
285
|
+
} else {
|
|
286
|
+
// Convert first letter to lowercase and append "Count"
|
|
287
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
|
|
291
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
292
|
+
tempAcc.push( {
|
|
293
|
+
name: item.name,
|
|
294
|
+
value: 0,
|
|
295
|
+
key: key,
|
|
296
|
+
type: item.type,
|
|
297
|
+
} );
|
|
298
|
+
|
|
299
|
+
|
|
300
|
+
return acc;
|
|
301
|
+
}
|
|
302
|
+
}, {} ) || {};
|
|
303
|
+
|
|
304
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
305
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
306
|
+
const revopQuery = {
|
|
307
|
+
size: 0,
|
|
308
|
+
query: {
|
|
309
|
+
bool: {
|
|
310
|
+
must: [
|
|
311
|
+
{
|
|
312
|
+
term: {
|
|
313
|
+
'storeId.keyword': inputData.storeId,
|
|
314
|
+
},
|
|
315
|
+
},
|
|
316
|
+
{
|
|
317
|
+
term: {
|
|
318
|
+
'dateString': inputData.dateString,
|
|
319
|
+
},
|
|
320
|
+
},
|
|
321
|
+
],
|
|
322
|
+
},
|
|
323
|
+
},
|
|
324
|
+
aggs: {
|
|
325
|
+
type_counts: {
|
|
326
|
+
terms: {
|
|
327
|
+
field: 'revopsType.keyword',
|
|
328
|
+
size: 100,
|
|
329
|
+
},
|
|
330
|
+
},
|
|
331
|
+
},
|
|
332
|
+
};
|
|
333
|
+
|
|
334
|
+
|
|
335
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
336
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
337
|
+
|
|
338
|
+
// Map OpenSearch revopsType values to count object keys
|
|
339
|
+
buckets.forEach( ( bucket ) => {
|
|
340
|
+
const revopsType = bucket.key;
|
|
341
|
+
const count = bucket.doc_count || 0;
|
|
342
|
+
|
|
343
|
+
|
|
344
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
345
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
346
|
+
const accMatch = tempAcc.find(
|
|
347
|
+
( acc ) =>
|
|
348
|
+
acc.type &&
|
|
349
|
+
acc.type === revopsType,
|
|
350
|
+
);
|
|
351
|
+
|
|
352
|
+
if ( accMatch && accMatch.key ) {
|
|
353
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
} );
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
|
|
360
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
361
|
+
|
|
362
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
363
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
364
|
+
0;
|
|
365
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
366
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
367
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
368
|
+
return res.sendError( 'Cannot create a ticket because footfall hasn’t changed', 400 );
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
const taggingData = {
|
|
372
|
+
size: 10000,
|
|
373
|
+
query: {
|
|
374
|
+
bool: {
|
|
375
|
+
must: [
|
|
376
|
+
{
|
|
377
|
+
term: {
|
|
378
|
+
'storeId.keyword': inputData.storeId,
|
|
379
|
+
},
|
|
380
|
+
},
|
|
381
|
+
{
|
|
382
|
+
term: {
|
|
383
|
+
'dateString': inputData.dateString,
|
|
384
|
+
},
|
|
385
|
+
},
|
|
386
|
+
],
|
|
387
|
+
},
|
|
388
|
+
},
|
|
389
|
+
};
|
|
390
|
+
|
|
391
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
392
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
393
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
394
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
395
|
+
}
|
|
396
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
397
|
+
|
|
398
|
+
const record = {
|
|
399
|
+
storeId: inputData.storeId,
|
|
400
|
+
type: 'store',
|
|
401
|
+
dateString: inputData.dateString,
|
|
402
|
+
storeName: getstoreName?.storeName,
|
|
403
|
+
ticketName: inputData.ticketName|| 'footfall-directory',
|
|
404
|
+
footfallCount: footfallCount,
|
|
405
|
+
clientId: getstoreName?.clientId,
|
|
406
|
+
ticketId: 'TE_FDT_' + new Date().valueOf(),
|
|
407
|
+
createdAt: new Date(),
|
|
408
|
+
updatedAt: new Date(),
|
|
409
|
+
status: 'raised',
|
|
410
|
+
revicedFootfall: revisedFootfall,
|
|
411
|
+
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
412
|
+
mappingInfo: [
|
|
413
|
+
{
|
|
414
|
+
type: 'tagging',
|
|
415
|
+
mode: inputData.mode,
|
|
416
|
+
revicedFootfall: revisedFootfall,
|
|
417
|
+
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
418
|
+
count: tempAcc,
|
|
419
|
+
revisedDetail: formattedTaggingData,
|
|
420
|
+
status: 'raised',
|
|
421
|
+
createdByEmail: req?.user?.email,
|
|
422
|
+
createdByUserName: req?.user?.userName,
|
|
423
|
+
createdByRole: req?.user?.role,
|
|
424
|
+
createdAt: new Date(),
|
|
425
|
+
},
|
|
426
|
+
],
|
|
427
|
+
};
|
|
428
|
+
|
|
429
|
+
|
|
430
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
431
|
+
let isAutoCloseEnable = false;
|
|
432
|
+
let autoCloseAccuracy = '95%';
|
|
433
|
+
try {
|
|
434
|
+
const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
|
|
435
|
+
if ( clientData?.footfallDirectoryConfigs ) {
|
|
436
|
+
isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
|
|
437
|
+
autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
|
|
438
|
+
}
|
|
439
|
+
} catch ( e ) {
|
|
440
|
+
isAutoCloseEnable = false;
|
|
441
|
+
autoCloseAccuracy = '95%';
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || '95' ).replace( '%', '' ) );
|
|
445
|
+
let revisedPercentage = 0;
|
|
446
|
+
if ( typeof getCategory === 'number' && getCategory > 0 ) {
|
|
447
|
+
revisedPercentage = ( revisedFootfall / getCategory ) * 100;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
451
|
+
if (
|
|
452
|
+
isAutoCloseEnable === true &&
|
|
453
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
454
|
+
) {
|
|
455
|
+
record.status = 'closed';
|
|
456
|
+
record.mappingInfo = [
|
|
457
|
+
{
|
|
458
|
+
type: 'tagging',
|
|
459
|
+
mode: inputData.mode,
|
|
460
|
+
revicedFootfall: revisedFootfall,
|
|
461
|
+
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
462
|
+
count: getCategory,
|
|
463
|
+
revisedDetail: formattedTaggingData,
|
|
464
|
+
status: 'closed',
|
|
465
|
+
createdByEmail: req?.user?.email,
|
|
466
|
+
createdByUserName: req?.user?.userName,
|
|
467
|
+
createdByRole: req?.user?.role,
|
|
468
|
+
},
|
|
469
|
+
];
|
|
470
|
+
} else {
|
|
471
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
472
|
+
let revisionArray = [];
|
|
473
|
+
try {
|
|
474
|
+
const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
|
|
475
|
+
revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
|
|
476
|
+
} catch ( e ) {
|
|
477
|
+
revisionArray = [];
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
// Default fallbacks
|
|
481
|
+
let revisionMapping = null;
|
|
482
|
+
let approverMapping = null;
|
|
483
|
+
let tangoReviewMapping = null;
|
|
484
|
+
|
|
485
|
+
// Find out which roles have isChecked true
|
|
486
|
+
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
487
|
+
for ( const r of revisionArray ) {
|
|
488
|
+
if ( r.actionType === 'reviewer' && r.isChecked === true ) {
|
|
489
|
+
revisionMapping = {
|
|
490
|
+
type: 'review',
|
|
491
|
+
revicedFootfall: revisedFootfall,
|
|
492
|
+
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
493
|
+
count: getCategory,
|
|
494
|
+
revisedDetail: formattedTaggingData,
|
|
495
|
+
status: 'open',
|
|
496
|
+
};
|
|
497
|
+
} else if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
498
|
+
approverMapping = {
|
|
499
|
+
type: 'approver',
|
|
500
|
+
revicedFootfall: revisedFootfall,
|
|
501
|
+
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
502
|
+
count: getCategory,
|
|
503
|
+
revisedDetail: formattedTaggingData,
|
|
504
|
+
status: 'open',
|
|
505
|
+
};
|
|
506
|
+
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
507
|
+
tangoReviewMapping = {
|
|
508
|
+
type: 'tango-review',
|
|
509
|
+
revicedFootfall: revisedFootfall,
|
|
510
|
+
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
511
|
+
count: getCategory,
|
|
512
|
+
revisedDetail: formattedTaggingData,
|
|
513
|
+
status: 'open',
|
|
514
|
+
};
|
|
515
|
+
}
|
|
516
|
+
}
|
|
517
|
+
}
|
|
518
|
+
|
|
519
|
+
// Insert appropriate mappingInfo blocks
|
|
520
|
+
if ( revisionMapping ) {
|
|
521
|
+
// If reviewer and checked
|
|
522
|
+
record.mappingInfo.push( revisionMapping );
|
|
523
|
+
} else if ( approverMapping ) {
|
|
524
|
+
// If approver and checked
|
|
525
|
+
record.mappingInfo.push( approverMapping );
|
|
526
|
+
} else if ( tangoReviewMapping ) {
|
|
527
|
+
// If none above, then tangoReview
|
|
528
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
529
|
+
}
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
|
|
533
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
534
|
+
const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
|
|
535
|
+
if ( insertResult && insertResult.statusCode === 201 ) {
|
|
536
|
+
// After successful ticket creation, update status to "submitted" in revop index for the relevant records
|
|
537
|
+
try {
|
|
538
|
+
const bulkUpdateBody = taggingImages.map( ( img ) => [
|
|
539
|
+
{ update: { _index: openSearch.revop, _id: img._id } },
|
|
540
|
+
{ doc: { status: 'submitted' } },
|
|
541
|
+
] ).flat();
|
|
542
|
+
|
|
543
|
+
if ( bulkUpdateBody.length > 0 ) {
|
|
544
|
+
await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
|
|
545
|
+
}
|
|
546
|
+
} catch ( updateErr ) {
|
|
547
|
+
logger.error( { error: updateErr, message: 'Failed to update status to submitted in revop index' } );
|
|
548
|
+
// Do not block the success response for this failure
|
|
549
|
+
}
|
|
550
|
+
|
|
551
|
+
// Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
|
|
552
|
+
|
|
553
|
+
if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
|
|
554
|
+
const breachDays = Number( req.accuracyBreach.days );
|
|
555
|
+
const breachCount = Number( req.accuracyBreach.ticketCount );
|
|
556
|
+
|
|
557
|
+
// req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
|
|
558
|
+
const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
|
|
559
|
+
const storeId = inputData.storeId;
|
|
560
|
+
const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
561
|
+
|
|
562
|
+
|
|
563
|
+
const formatDate = ( d ) =>
|
|
564
|
+
`${d.getFullYear()}-${String( d.getMonth() + 1 ).padStart( 2, '0' )}-${String( d.getDate() ).padStart( 2, '0' )}`;
|
|
565
|
+
|
|
566
|
+
// Compute current date object
|
|
567
|
+
|
|
568
|
+
|
|
569
|
+
// Calculate start date based on config days
|
|
570
|
+
// If 30 days: start from first day of current month
|
|
571
|
+
// If 60 days: start from first day of last month
|
|
572
|
+
const currentDateObj = new Date(); // Declare currentDateObj as today's date
|
|
573
|
+
const startDateObj = new Date( currentDateObj );
|
|
574
|
+
|
|
575
|
+
if ( breachDays === 30 ) {
|
|
576
|
+
// Consider within this month
|
|
577
|
+
startDateObj.setDate( 1 ); // First day of current month
|
|
578
|
+
} else if ( breachDays === 60 ) {
|
|
579
|
+
// Consider this month and last month
|
|
580
|
+
startDateObj.setMonth( startDateObj.getMonth() - 1 );
|
|
581
|
+
startDateObj.setDate( 1 ); // First day of last month
|
|
582
|
+
} else {
|
|
583
|
+
// For other values, calculate months from days
|
|
584
|
+
const breachMonths = Math.ceil( breachDays / 30 );
|
|
585
|
+
startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
|
|
586
|
+
startDateObj.setDate( 1 );
|
|
587
|
+
}
|
|
588
|
+
|
|
589
|
+
|
|
590
|
+
const startDate = startDateObj;
|
|
591
|
+
|
|
592
|
+
|
|
593
|
+
const endDate = new Date;
|
|
594
|
+
// Query for tickets within this months window for this store and ticket name
|
|
595
|
+
const query = {
|
|
596
|
+
query: {
|
|
597
|
+
bool: {
|
|
598
|
+
must: [
|
|
599
|
+
{ term: { 'storeId.keyword': storeId } },
|
|
600
|
+
{ term: { 'ticketName.keyword': ticketName } },
|
|
601
|
+
{ range: { createdAt: { gte: startDate, lte: endDate } } },
|
|
602
|
+
],
|
|
603
|
+
},
|
|
604
|
+
},
|
|
605
|
+
};
|
|
606
|
+
|
|
607
|
+
// Search in OpenSearch for recent similar tickets
|
|
608
|
+
const breachTicketsResult = await getOpenSearchData( openSearch.footfallDirectory, query );
|
|
609
|
+
const tickets = breachTicketsResult?.body?.hits?.hits || [];
|
|
610
|
+
|
|
611
|
+
// Filter tickets where revised footfall percentage > config accuracy
|
|
612
|
+
let breachTicketsCount = 0;
|
|
613
|
+
for ( const ticket of tickets ) {
|
|
614
|
+
const source = ticket._source || {};
|
|
615
|
+
const footfallCount = source.footfallCount || 0;
|
|
616
|
+
const revicedFootfall = source.mappingInfo?.[0]?.revicedFootfall || 0;
|
|
617
|
+
|
|
618
|
+
// Calculate revised footfall percentage
|
|
619
|
+
let revisedPercentage = 0;
|
|
620
|
+
if ( footfallCount > 0 ) {
|
|
621
|
+
revisedPercentage = ( revicedFootfall / footfallCount ) * 100;
|
|
622
|
+
}
|
|
623
|
+
|
|
624
|
+
// Check if revised footfall percentage > config accuracy
|
|
625
|
+
if ( revisedPercentage > breachAccuracy ) {
|
|
626
|
+
breachTicketsCount++;
|
|
627
|
+
}
|
|
628
|
+
}
|
|
629
|
+
|
|
630
|
+
if ( breachTicketsCount >= breachCount ) {
|
|
631
|
+
// Calculate remaining future days in the config period
|
|
632
|
+
const futureDates = [];
|
|
633
|
+
|
|
634
|
+
// Calculate end date of config period
|
|
635
|
+
const configEndDateObj = new Date( currentDateObj );
|
|
636
|
+
if ( breachDays === 30 ) {
|
|
637
|
+
// End of current month
|
|
638
|
+
configEndDateObj.setMonth( configEndDateObj.getMonth() + 1 );
|
|
639
|
+
configEndDateObj.setDate( 0 ); // Last day of current month
|
|
640
|
+
} else if ( breachDays === 60 ) {
|
|
641
|
+
// End of next month
|
|
642
|
+
configEndDateObj.setMonth( configEndDateObj.getMonth() + 2 );
|
|
643
|
+
configEndDateObj.setDate( 0 ); // Last day of next month
|
|
644
|
+
} else {
|
|
645
|
+
// For other values, add the remaining days
|
|
646
|
+
const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
|
|
647
|
+
configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
|
|
648
|
+
}
|
|
649
|
+
|
|
650
|
+
// Generate all dates from tomorrow until end of config period
|
|
651
|
+
const tomorrow = new Date( currentDateObj );
|
|
652
|
+
tomorrow.setDate( tomorrow.getDate() + 1 );
|
|
653
|
+
|
|
654
|
+
const dateIterator = new Date( tomorrow );
|
|
655
|
+
while ( dateIterator <= configEndDateObj ) {
|
|
656
|
+
futureDates.push( formatDate( dateIterator ) );
|
|
657
|
+
dateIterator.setDate( dateIterator.getDate() + 1 );
|
|
658
|
+
}
|
|
659
|
+
|
|
660
|
+
// Insert a record for each future date
|
|
661
|
+
for ( const futureDateString of futureDates ) {
|
|
662
|
+
const record = {
|
|
663
|
+
clientId: getstoreName?.clientId,
|
|
664
|
+
storeId: inputData?.storeId,
|
|
665
|
+
storeName: getstoreName?.storeName,
|
|
666
|
+
dateString: futureDateString,
|
|
667
|
+
status: 'block',
|
|
668
|
+
};
|
|
669
|
+
|
|
670
|
+
await updateOneUpsertVmsStoreRequest( { storeId: inputData?.storeId, dateString: futureDateString }, record );
|
|
671
|
+
}
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
|
|
675
|
+
return res.sendSuccess( 'Ticket raised successfully' );
|
|
676
|
+
}
|
|
677
|
+
} catch ( error ) {
|
|
678
|
+
const err = error.message || 'Internal Server Error';
|
|
679
|
+
logger.error( { error: err, funtion: 'ticketCreation' } );
|
|
680
|
+
return res.sendError( err, 500 );
|
|
681
|
+
}
|
|
682
|
+
}
|
|
683
|
+
|
|
684
|
+
|