tango-app-api-infra 3.9.5-vms.8 → 3.9.5-vms.81
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 -2
- package/src/controllers/footfallDirectory.controllers.js +3802 -1216
- package/src/dtos/footfallDirectory.dtos.js +188 -60
- package/src/routes/footfallDirectory.routes.js +15 -5
- package/src/services/storeAccuracyIssues.service.js +9 -0
- package/src/validations/footfallDirectory.validation.js +1479 -83
|
@@ -1,9 +1,18 @@
|
|
|
1
|
-
import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger } from 'tango-app-api-middleware';
|
|
1
|
+
import { bulkUpdate, getOpenSearchCount, getOpenSearchData, insertWithId, logger, sendMessageToFIFOQueue, updateOpenSearchData } from 'tango-app-api-middleware';
|
|
2
2
|
import { aggregateCluster } from '../services/cluster.service.js';
|
|
3
3
|
import { findOneRevopDownload } from '../services/revopDownload.service.js';
|
|
4
4
|
import { findOneStore } from '../services/store.service.js';
|
|
5
5
|
import { findOneClient } from '../services/client.service.js';
|
|
6
6
|
import { updateOneUpsertVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
|
|
7
|
+
import { findOneUser, aggregateUser } from '../services/user.service.js';
|
|
8
|
+
import { findteams } from '../services/teams.service.js';
|
|
9
|
+
import { findcluster } from '../services/cluster.service.js';
|
|
10
|
+
import { sendPushNotification } from 'tango-app-api-middleware';
|
|
11
|
+
import dayjs from 'dayjs';
|
|
12
|
+
import { sendSqsMessage } from '../controllers/footfallDirectory.controllers.js';
|
|
13
|
+
import { countDocumnetsCamera } from '../services/camera.service.js';
|
|
14
|
+
// import utc from 'dayjs/plugin/utc.js';
|
|
15
|
+
// import timezone from 'dayjs/plugin/timezone.js';
|
|
7
16
|
|
|
8
17
|
function formatRevopTaggingHits( hits = [] ) {
|
|
9
18
|
return hits
|
|
@@ -14,19 +23,21 @@ function formatRevopTaggingHits( hits = [] ) {
|
|
|
14
23
|
}
|
|
15
24
|
|
|
16
25
|
const duplicateImages = Array.isArray( source.duplicateImage ) ?
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
26
|
+
source.duplicateImage.map( ( item ) => ( {
|
|
27
|
+
id: item?.id,
|
|
28
|
+
tempId: item?.tempId,
|
|
29
|
+
timeRange: item?.timeRange,
|
|
30
|
+
entryTime: item?.entryTime,
|
|
31
|
+
exitTime: item?.exitTime,
|
|
32
|
+
filePath: item?.filePath,
|
|
33
|
+
status: item?.status,
|
|
34
|
+
action: item?.action,
|
|
35
|
+
isChecked: Boolean( item?.isChecked ),
|
|
36
|
+
} ) ) :
|
|
37
|
+
[];
|
|
27
38
|
|
|
28
39
|
return {
|
|
29
|
-
id:
|
|
40
|
+
id: source?.id,
|
|
30
41
|
clientId: source?.clientId,
|
|
31
42
|
storeId: source?.storeId,
|
|
32
43
|
tempId: source?.tempId,
|
|
@@ -41,11 +52,12 @@ function formatRevopTaggingHits( hits = [] ) {
|
|
|
41
52
|
description: source?.description || '',
|
|
42
53
|
isChecked: Boolean( source?.isChecked ),
|
|
43
54
|
type: source?.type,
|
|
55
|
+
action: source?.action,
|
|
44
56
|
parent: source?.parent ?? null,
|
|
45
57
|
isParent: duplicateImages.length > 0 && !source?.parent,
|
|
46
58
|
createdAt: source?.createdAt,
|
|
47
59
|
updatedAt: source?.updatedAt,
|
|
48
|
-
|
|
60
|
+
duplicateImage: duplicateImages,
|
|
49
61
|
};
|
|
50
62
|
} )
|
|
51
63
|
.filter( Boolean );
|
|
@@ -53,7 +65,7 @@ function formatRevopTaggingHits( hits = [] ) {
|
|
|
53
65
|
|
|
54
66
|
export async function isExist( req, res, next ) {
|
|
55
67
|
try {
|
|
56
|
-
const inputData=req.body;
|
|
68
|
+
const inputData = req.body;
|
|
57
69
|
const opensearch = JSON.parse( process.env.OPENSEARCH );
|
|
58
70
|
const query = {
|
|
59
71
|
query: {
|
|
@@ -75,7 +87,7 @@ export async function isExist( req, res, next ) {
|
|
|
75
87
|
};
|
|
76
88
|
|
|
77
89
|
const getData = await getOpenSearchCount( opensearch.footfallDirectory, query );
|
|
78
|
-
const isExist = getData?.body?.count == 0? true : false;
|
|
90
|
+
const isExist = getData?.body?.count == 0 ? true : false;
|
|
79
91
|
logger.info( { isExist: isExist, count: getData?.body } );
|
|
80
92
|
if ( isExist === true ) {
|
|
81
93
|
next();
|
|
@@ -91,12 +103,12 @@ export async function isExist( req, res, next ) {
|
|
|
91
103
|
|
|
92
104
|
export async function getClusters( req, res, next ) {
|
|
93
105
|
try {
|
|
94
|
-
const inputData=req.query;
|
|
106
|
+
const inputData = req.query;
|
|
95
107
|
// const assignedStores = req.body.assignedStores;
|
|
96
108
|
inputData.clientId = inputData?.clientId?.split( ',' );
|
|
97
109
|
const clusters = inputData?.clusters?.split( ',' ); // convert strig to array
|
|
98
110
|
// logger.info( { assignedStores, clusters } );
|
|
99
|
-
let filter =[
|
|
111
|
+
let filter = [
|
|
100
112
|
{
|
|
101
113
|
clientId: { $in: inputData.clientId },
|
|
102
114
|
},
|
|
@@ -183,7 +195,7 @@ export async function isGrantedUsers( req, res, next ) {
|
|
|
183
195
|
const userInfo = req?.user;
|
|
184
196
|
switch ( userInfo.userType ) {
|
|
185
197
|
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 =='
|
|
198
|
+
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
199
|
logger.info( { ticketsFeature } );
|
|
188
200
|
if ( ticketsFeature ) {
|
|
189
201
|
return next();
|
|
@@ -208,7 +220,6 @@ export async function getConfig( req, res, next ) {
|
|
|
208
220
|
const storeKey = inputData.storeId.split( '-' )[0];
|
|
209
221
|
|
|
210
222
|
const config = await findOneClient( { clientId: storeKey }, { footfallDirectoryConfigs: 1 } );
|
|
211
|
-
logger.info( { config, storeKey } );
|
|
212
223
|
const accuracyBreach = config?.footfallDirectoryConfigs?.accuracyBreach;
|
|
213
224
|
req.accuracyBreach = accuracyBreach || '';
|
|
214
225
|
return next();
|
|
@@ -222,12 +233,14 @@ export async function getConfig( req, res, next ) {
|
|
|
222
233
|
export async function ticketCreation( req, res, next ) {
|
|
223
234
|
try {
|
|
224
235
|
const inputData = req.body;
|
|
236
|
+
const sqs = JSON.parse( process.env.SQS );
|
|
237
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
225
238
|
if ( inputData?.type !== 'create' ) {
|
|
226
239
|
return next();
|
|
227
240
|
}
|
|
228
241
|
// check the createtion permission from the user permission
|
|
229
242
|
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 ) ) ) );
|
|
243
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'creator' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
231
244
|
if ( !ticketsFeature ) {
|
|
232
245
|
return res.sendError( 'Forbidden to Create Ticket', 403 );
|
|
233
246
|
}
|
|
@@ -240,7 +253,6 @@ export async function ticketCreation( req, res, next ) {
|
|
|
240
253
|
}
|
|
241
254
|
|
|
242
255
|
// get the footfall count from opensearch
|
|
243
|
-
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
244
256
|
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
245
257
|
const getQuery = {
|
|
246
258
|
query: {
|
|
@@ -265,7 +277,7 @@ export async function ticketCreation( req, res, next ) {
|
|
|
265
277
|
}
|
|
266
278
|
|
|
267
279
|
// get category details from the client level configuration
|
|
268
|
-
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
280
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1, clientId: 1 } );
|
|
269
281
|
if ( !getConfig || getConfig == null ) {
|
|
270
282
|
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
271
283
|
}
|
|
@@ -274,7 +286,7 @@ export async function ticketCreation( req, res, next ) {
|
|
|
274
286
|
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
275
287
|
// Initialize count object from taggingLimitation
|
|
276
288
|
const tempAcc = [];
|
|
277
|
-
|
|
289
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
278
290
|
if ( item?.type ) {
|
|
279
291
|
// Convert type to camelCase with "Count" suffix
|
|
280
292
|
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
@@ -318,6 +330,16 @@ export async function ticketCreation( req, res, next ) {
|
|
|
318
330
|
'dateString': inputData.dateString,
|
|
319
331
|
},
|
|
320
332
|
},
|
|
333
|
+
{
|
|
334
|
+
term: {
|
|
335
|
+
'isParent': false,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
term: {
|
|
340
|
+
isChecked: true,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
321
343
|
],
|
|
322
344
|
},
|
|
323
345
|
},
|
|
@@ -397,25 +419,30 @@ export async function ticketCreation( req, res, next ) {
|
|
|
397
419
|
|
|
398
420
|
const record = {
|
|
399
421
|
storeId: inputData.storeId,
|
|
422
|
+
type: 'store',
|
|
400
423
|
dateString: inputData.dateString,
|
|
401
424
|
storeName: getstoreName?.storeName,
|
|
402
|
-
ticketName: inputData.ticketName|| 'footfall-directory',
|
|
425
|
+
ticketName: inputData.ticketName || 'footfall-directory',
|
|
403
426
|
footfallCount: footfallCount,
|
|
404
427
|
clientId: getstoreName?.clientId,
|
|
405
428
|
ticketId: 'TE_FDT_' + new Date().valueOf(),
|
|
406
429
|
createdAt: new Date(),
|
|
407
430
|
updatedAt: new Date(),
|
|
408
|
-
status: '
|
|
431
|
+
status: 'Raised',
|
|
432
|
+
comments: inputData?.comments || '',
|
|
409
433
|
revicedFootfall: revisedFootfall,
|
|
410
|
-
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
434
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
435
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
411
436
|
mappingInfo: [
|
|
412
437
|
{
|
|
413
438
|
type: 'tagging',
|
|
414
439
|
mode: inputData.mode,
|
|
415
440
|
revicedFootfall: revisedFootfall,
|
|
441
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
442
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
416
443
|
count: tempAcc,
|
|
417
444
|
revisedDetail: formattedTaggingData,
|
|
418
|
-
status: '
|
|
445
|
+
status: 'Raised',
|
|
419
446
|
createdByEmail: req?.user?.email,
|
|
420
447
|
createdByUserName: req?.user?.userName,
|
|
421
448
|
createdByRole: req?.user?.role,
|
|
@@ -426,85 +453,92 @@ export async function ticketCreation( req, res, next ) {
|
|
|
426
453
|
|
|
427
454
|
|
|
428
455
|
// Retrieve client footfallDirectoryConfigs revision
|
|
429
|
-
let isAutoCloseEnable =
|
|
430
|
-
let autoCloseAccuracy =
|
|
431
|
-
try {
|
|
432
|
-
const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
|
|
433
|
-
if ( clientData?.footfallDirectoryConfigs ) {
|
|
434
|
-
isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
|
|
435
|
-
autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
|
|
436
|
-
}
|
|
437
|
-
} catch ( e ) {
|
|
438
|
-
isAutoCloseEnable = false;
|
|
439
|
-
autoCloseAccuracy = '95%';
|
|
440
|
-
}
|
|
456
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
457
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy; ;
|
|
441
458
|
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
}
|
|
459
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
460
|
+
|
|
461
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
462
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
447
463
|
|
|
448
464
|
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
449
465
|
if (
|
|
450
466
|
isAutoCloseEnable === true &&
|
|
451
467
|
revisedPercentage >= autoCloseAccuracyValue
|
|
452
468
|
) {
|
|
453
|
-
record.status = '
|
|
469
|
+
record.status = 'Closed';
|
|
454
470
|
record.mappingInfo = [
|
|
455
471
|
{
|
|
456
472
|
type: 'tagging',
|
|
457
473
|
mode: inputData.mode,
|
|
458
474
|
revicedFootfall: revisedFootfall,
|
|
459
|
-
|
|
475
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
476
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
477
|
+
count: tempAcc,
|
|
478
|
+
revisedDetail: formattedTaggingData,
|
|
479
|
+
status: 'Closed',
|
|
480
|
+
createdByEmail: req?.user?.email,
|
|
481
|
+
createdByUserName: req?.user?.userName,
|
|
482
|
+
createdByRole: req?.user?.role,
|
|
483
|
+
createdAt: new Date(),
|
|
484
|
+
},
|
|
485
|
+
{
|
|
486
|
+
type: 'finalRevision',
|
|
487
|
+
mode: inputData.mode,
|
|
488
|
+
revicedFootfall: revisedFootfall,
|
|
489
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
490
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
491
|
+
count: tempAcc,
|
|
460
492
|
revisedDetail: formattedTaggingData,
|
|
461
|
-
status: '
|
|
493
|
+
status: 'Closed',
|
|
462
494
|
createdByEmail: req?.user?.email,
|
|
463
495
|
createdByUserName: req?.user?.userName,
|
|
464
496
|
createdByRole: req?.user?.role,
|
|
497
|
+
createdAt: new Date(),
|
|
465
498
|
},
|
|
466
499
|
];
|
|
467
500
|
} else {
|
|
468
|
-
|
|
501
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
469
502
|
let revisionArray = [];
|
|
470
|
-
try {
|
|
471
|
-
const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
|
|
472
|
-
revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
|
|
473
|
-
} catch ( e ) {
|
|
474
|
-
revisionArray = [];
|
|
475
|
-
}
|
|
476
503
|
|
|
504
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
477
505
|
// Default fallbacks
|
|
478
506
|
let revisionMapping = null;
|
|
479
507
|
let approverMapping = null;
|
|
480
508
|
let tangoReviewMapping = null;
|
|
481
|
-
|
|
482
509
|
// Find out which roles have isChecked true
|
|
483
510
|
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
484
511
|
for ( const r of revisionArray ) {
|
|
485
512
|
if ( r.actionType === 'reviewer' && r.isChecked === true ) {
|
|
486
513
|
revisionMapping = {
|
|
487
514
|
type: 'review',
|
|
488
|
-
revicedFootfall: revisedFootfall,
|
|
489
|
-
|
|
515
|
+
// revicedFootfall: revisedFootfall,
|
|
516
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
517
|
+
count: tempAcc,
|
|
490
518
|
revisedDetail: formattedTaggingData,
|
|
491
|
-
status: '
|
|
519
|
+
status: 'Open',
|
|
520
|
+
dueDate: new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
521
|
+
|
|
492
522
|
};
|
|
493
523
|
} else if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
494
524
|
approverMapping = {
|
|
495
|
-
type: '
|
|
496
|
-
revicedFootfall: revisedFootfall,
|
|
497
|
-
|
|
525
|
+
type: 'approve',
|
|
526
|
+
// revicedFootfall: revisedFootfall,
|
|
527
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
528
|
+
count: tempAcc,
|
|
498
529
|
revisedDetail: formattedTaggingData,
|
|
499
|
-
status: '
|
|
530
|
+
status: 'Open',
|
|
531
|
+
dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
500
532
|
};
|
|
501
533
|
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
502
534
|
tangoReviewMapping = {
|
|
503
|
-
type: '
|
|
504
|
-
revicedFootfall: revisedFootfall,
|
|
505
|
-
|
|
535
|
+
type: 'tangoreview',
|
|
536
|
+
// revicedFootfall: revisedFootfall,
|
|
537
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
538
|
+
count: tempAcc,
|
|
506
539
|
revisedDetail: formattedTaggingData,
|
|
507
|
-
status: '
|
|
540
|
+
status: 'Open',
|
|
541
|
+
dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
508
542
|
};
|
|
509
543
|
}
|
|
510
544
|
}
|
|
@@ -512,36 +546,179 @@ export async function ticketCreation( req, res, next ) {
|
|
|
512
546
|
|
|
513
547
|
// Insert appropriate mappingInfo blocks
|
|
514
548
|
if ( revisionMapping ) {
|
|
515
|
-
|
|
549
|
+
// If reviewer and checked
|
|
516
550
|
record.mappingInfo.push( revisionMapping );
|
|
517
551
|
} else if ( approverMapping ) {
|
|
518
|
-
|
|
552
|
+
// If approver and checked
|
|
519
553
|
record.mappingInfo.push( approverMapping );
|
|
520
554
|
} else if ( tangoReviewMapping ) {
|
|
521
|
-
|
|
555
|
+
// If none above, then tangoReview
|
|
522
556
|
record.mappingInfo.push( tangoReviewMapping );
|
|
523
557
|
}
|
|
524
558
|
}
|
|
525
559
|
|
|
560
|
+
const revision = getConfig.footfallDirectoryConfigs?.revision ?? [];
|
|
561
|
+
|
|
562
|
+
const hasReviewer = revision.some(
|
|
563
|
+
( data ) => data.actionType === 'reviewer' && data.isChecked === true,
|
|
564
|
+
);
|
|
565
|
+
const hasApprover = revision.some(
|
|
566
|
+
( data ) => data.actionType === 'approver' && data.isChecked === true,
|
|
567
|
+
);
|
|
568
|
+
|
|
569
|
+
if ( hasReviewer || hasApprover ) {
|
|
570
|
+
const userQuery = [
|
|
571
|
+
{
|
|
572
|
+
$match: {
|
|
573
|
+
clientId: getstoreName.clientId,
|
|
574
|
+
role: 'admin',
|
|
575
|
+
isActive: true,
|
|
576
|
+
},
|
|
577
|
+
},
|
|
578
|
+
];
|
|
579
|
+
|
|
580
|
+
const finduserList = await aggregateUser( userQuery );
|
|
581
|
+
|
|
582
|
+
|
|
583
|
+
const createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
584
|
+
const title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
585
|
+
const description = `Created on ${createdOn}`;
|
|
586
|
+
|
|
587
|
+
const Data = {
|
|
588
|
+
title,
|
|
589
|
+
body: description,
|
|
590
|
+
type: 'create',
|
|
591
|
+
date: record.dateString,
|
|
592
|
+
storeId: record.storeId,
|
|
593
|
+
clientId: record.clientId,
|
|
594
|
+
ticketId: record.ticketId,
|
|
595
|
+
};
|
|
596
|
+
|
|
597
|
+
await Promise.all(
|
|
598
|
+
( finduserList || [] ).map( async ( userData ) => {
|
|
599
|
+
const ticketsFeature = userData?.rolespermission?.some(
|
|
600
|
+
( f ) =>
|
|
601
|
+
f.featureName === 'FootfallDirectory' &&
|
|
602
|
+
f.modules?.some(
|
|
603
|
+
( m ) =>
|
|
604
|
+
m.name === 'reviewer' && ( m.isAdd === true || m.isEdit === true ),
|
|
605
|
+
),
|
|
606
|
+
);
|
|
607
|
+
|
|
608
|
+
|
|
609
|
+
if ( !ticketsFeature ) return;
|
|
610
|
+
|
|
611
|
+
const notifyUser = await getAssinedStore( userData, req.body.storeId );
|
|
612
|
+
if ( !notifyUser || !userData?.fcmToken ) return;
|
|
613
|
+
|
|
614
|
+
await sendPushNotification( title, description, userData.fcmToken, Data );
|
|
615
|
+
} ),
|
|
616
|
+
);
|
|
617
|
+
}
|
|
618
|
+
|
|
526
619
|
|
|
527
620
|
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
528
621
|
const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
|
|
529
622
|
if ( insertResult && insertResult.statusCode === 201 ) {
|
|
530
623
|
// After successful ticket creation, update status to "submitted" in revop index for the relevant records
|
|
531
|
-
|
|
532
|
-
|
|
533
|
-
|
|
534
|
-
|
|
535
|
-
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
} catch ( updateErr ) {
|
|
541
|
-
logger.error( { error: updateErr, message: 'Failed to update status to submitted in revop index' } );
|
|
542
|
-
// Do not block the success response for this failure
|
|
624
|
+
|
|
625
|
+
|
|
626
|
+
const bulkUpdateBody = taggingImages.map( ( img ) => [
|
|
627
|
+
{ update: { _index: openSearch.revop, _id: img._id } },
|
|
628
|
+
{ doc: { status: 'submitted' } },
|
|
629
|
+
] ).flat();
|
|
630
|
+
|
|
631
|
+
if ( bulkUpdateBody.length > 0 ) {
|
|
632
|
+
await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
|
|
543
633
|
}
|
|
544
634
|
|
|
635
|
+
if ( record.status = 'Closed' ) {
|
|
636
|
+
const query = {
|
|
637
|
+
storeId: inputData?.storeId,
|
|
638
|
+
isVideoStream: true,
|
|
639
|
+
};
|
|
640
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
641
|
+
const revopInfoQuery = {
|
|
642
|
+
size: 10000,
|
|
643
|
+
query: {
|
|
644
|
+
bool: {
|
|
645
|
+
must: [
|
|
646
|
+
{
|
|
647
|
+
term: {
|
|
648
|
+
'storeId.keyword': inputData.storeId,
|
|
649
|
+
},
|
|
650
|
+
},
|
|
651
|
+
{
|
|
652
|
+
term: {
|
|
653
|
+
'dateString': inputData.dateString,
|
|
654
|
+
},
|
|
655
|
+
},
|
|
656
|
+
{
|
|
657
|
+
term: {
|
|
658
|
+
'isParent': false,
|
|
659
|
+
},
|
|
660
|
+
},
|
|
661
|
+
{
|
|
662
|
+
term: {
|
|
663
|
+
isChecked: true,
|
|
664
|
+
},
|
|
665
|
+
},
|
|
666
|
+
],
|
|
667
|
+
},
|
|
668
|
+
},
|
|
669
|
+
_source: [ 'tempId' ],
|
|
670
|
+
|
|
671
|
+
};
|
|
672
|
+
|
|
673
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
674
|
+
|
|
675
|
+
// Get all tempIds from revopInfo response
|
|
676
|
+
const tempIds =
|
|
677
|
+
revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
678
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
679
|
+
const managerEyeZoneQuery = {
|
|
680
|
+
size: 1,
|
|
681
|
+
query: {
|
|
682
|
+
bool: {
|
|
683
|
+
must: [
|
|
684
|
+
{
|
|
685
|
+
term: {
|
|
686
|
+
'storeId.keyword': inputData.storeId,
|
|
687
|
+
},
|
|
688
|
+
},
|
|
689
|
+
{
|
|
690
|
+
term: {
|
|
691
|
+
'storeDate': inputData.dateString,
|
|
692
|
+
},
|
|
693
|
+
},
|
|
694
|
+
],
|
|
695
|
+
},
|
|
696
|
+
},
|
|
697
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
698
|
+
};
|
|
699
|
+
|
|
700
|
+
// Query the managerEyeZone index for the matching document
|
|
701
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
702
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
703
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
704
|
+
const mapping =
|
|
705
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
706
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
707
|
+
{};
|
|
708
|
+
|
|
709
|
+
// If you want to compare or find matching tempIds in the mapping
|
|
710
|
+
// The mapping is { "1": tempId1, ... }, so get values as array of tempIds
|
|
711
|
+
// const managerMappedTempIds = Object.values( mapping );
|
|
712
|
+
|
|
713
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
714
|
+
const temp = [];
|
|
715
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
|
|
716
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
717
|
+
if ( isSendMessge == true ) {
|
|
718
|
+
logger.info( '....1' );
|
|
719
|
+
// return true; // res.sendSuccess( 'Ticket has been updated successfully' );
|
|
720
|
+
} // Example: log or use these tempIds for further logic
|
|
721
|
+
}
|
|
545
722
|
// Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
|
|
546
723
|
|
|
547
724
|
if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
|
|
@@ -551,7 +728,7 @@ export async function ticketCreation( req, res, next ) {
|
|
|
551
728
|
// req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
|
|
552
729
|
const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
|
|
553
730
|
const storeId = inputData.storeId;
|
|
554
|
-
const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
731
|
+
const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
555
732
|
|
|
556
733
|
|
|
557
734
|
const formatDate = ( d ) =>
|
|
@@ -665,14 +842,1233 @@ export async function ticketCreation( req, res, next ) {
|
|
|
665
842
|
}
|
|
666
843
|
}
|
|
667
844
|
}
|
|
845
|
+
const sqsName = sqs.vmsPickleExtention;
|
|
846
|
+
const sqsProduceQueue = {
|
|
847
|
+
QueueUrl: `${sqs.url}${sqsName}`,
|
|
848
|
+
MessageBody: JSON.stringify( {
|
|
849
|
+
store_id: inputData?.storeId,
|
|
850
|
+
store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
|
|
851
|
+
primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
852
|
+
time: new Date(),
|
|
853
|
+
} ),
|
|
854
|
+
MessageGroupId: 'revops-pickle',
|
|
855
|
+
MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
856
|
+
};
|
|
857
|
+
const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
|
|
858
|
+
|
|
859
|
+
if ( sqsQueue.statusCode ) {
|
|
860
|
+
logger.error( {
|
|
861
|
+
error: `${sqsQueue}`,
|
|
862
|
+
type: 'SQS_NOT_SEND_ERROR',
|
|
863
|
+
} );
|
|
864
|
+
}
|
|
865
|
+
|
|
668
866
|
|
|
669
867
|
return res.sendSuccess( 'Ticket raised successfully' );
|
|
670
868
|
}
|
|
671
869
|
} catch ( error ) {
|
|
672
870
|
const err = error.message || 'Internal Server Error';
|
|
673
|
-
logger.error( { error:
|
|
871
|
+
logger.error( { error: error, funtion: 'ticketCreation' } );
|
|
674
872
|
return res.sendError( err, 500 );
|
|
675
873
|
}
|
|
676
874
|
}
|
|
677
875
|
|
|
876
|
+
export async function ticketReview( req, res, next ) {
|
|
877
|
+
try {
|
|
878
|
+
const inputData = req.body;
|
|
879
|
+
if ( inputData?.type !== 'review' ) {
|
|
880
|
+
return next();
|
|
881
|
+
}
|
|
882
|
+
// check the createtion permission from the user permission
|
|
883
|
+
const userInfo = req?.user;
|
|
884
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
885
|
+
if ( !ticketsFeature ) {
|
|
886
|
+
return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// get store info by the storeId into mongo db
|
|
890
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
891
|
+
|
|
892
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
893
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// get the footfall count from opensearch
|
|
897
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
898
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
899
|
+
const getQuery = {
|
|
900
|
+
query: {
|
|
901
|
+
terms: {
|
|
902
|
+
_id: [ dateString ],
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
906
|
+
sort: [
|
|
907
|
+
{
|
|
908
|
+
date_iso: {
|
|
909
|
+
order: 'desc',
|
|
910
|
+
},
|
|
911
|
+
},
|
|
912
|
+
],
|
|
913
|
+
};
|
|
914
|
+
|
|
915
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
916
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
917
|
+
logger.info( { hits } );
|
|
918
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
919
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
920
|
+
}
|
|
921
|
+
|
|
922
|
+
// get category details from the client level configuration
|
|
923
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
924
|
+
if ( !getConfig || getConfig == null ) {
|
|
925
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
926
|
+
}
|
|
927
|
+
|
|
928
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
929
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
930
|
+
// Initialize count object from taggingLimitation
|
|
931
|
+
const tempAcc = [];
|
|
932
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
933
|
+
if ( item?.type ) {
|
|
934
|
+
// Convert type to camelCase with "Count" suffix
|
|
935
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
936
|
+
const typeLower = item.type.toLowerCase();
|
|
937
|
+
let key;
|
|
938
|
+
if ( typeLower === 'housekeeping' ) {
|
|
939
|
+
key = 'houseKeepingCount';
|
|
940
|
+
} else {
|
|
941
|
+
// Convert first letter to lowercase and append "Count"
|
|
942
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
943
|
+
}
|
|
944
|
+
|
|
945
|
+
|
|
946
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
947
|
+
tempAcc.push( {
|
|
948
|
+
name: item.name,
|
|
949
|
+
value: 0,
|
|
950
|
+
key: key,
|
|
951
|
+
type: item.type,
|
|
952
|
+
} );
|
|
953
|
+
|
|
954
|
+
|
|
955
|
+
return acc;
|
|
956
|
+
}
|
|
957
|
+
}, {} ) || {};
|
|
958
|
+
|
|
959
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
960
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
961
|
+
const revopQuery = {
|
|
962
|
+
size: 0,
|
|
963
|
+
query: {
|
|
964
|
+
bool: {
|
|
965
|
+
must: [
|
|
966
|
+
{
|
|
967
|
+
term: {
|
|
968
|
+
'storeId.keyword': inputData.storeId,
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
{
|
|
972
|
+
term: {
|
|
973
|
+
'dateString': inputData.dateString,
|
|
974
|
+
},
|
|
975
|
+
},
|
|
976
|
+
{
|
|
977
|
+
term: {
|
|
978
|
+
'isParent': false,
|
|
979
|
+
},
|
|
980
|
+
},
|
|
981
|
+
{
|
|
982
|
+
term: {
|
|
983
|
+
isChecked: true,
|
|
984
|
+
},
|
|
985
|
+
},
|
|
986
|
+
],
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
aggs: {
|
|
990
|
+
type_counts: {
|
|
991
|
+
terms: {
|
|
992
|
+
field: 'revopsType.keyword',
|
|
993
|
+
size: 100,
|
|
994
|
+
},
|
|
995
|
+
},
|
|
996
|
+
},
|
|
997
|
+
};
|
|
998
|
+
|
|
999
|
+
|
|
1000
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
1001
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
1002
|
+
|
|
1003
|
+
// Map OpenSearch revopsType values to count object keys
|
|
1004
|
+
buckets.forEach( ( bucket ) => {
|
|
1005
|
+
const revopsType = bucket.key;
|
|
1006
|
+
const count = bucket.doc_count || 0;
|
|
1007
|
+
|
|
1008
|
+
|
|
1009
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
1010
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
1011
|
+
const accMatch = tempAcc.find(
|
|
1012
|
+
( acc ) =>
|
|
1013
|
+
acc.type &&
|
|
1014
|
+
acc.type === revopsType,
|
|
1015
|
+
);
|
|
1016
|
+
|
|
1017
|
+
if ( accMatch && accMatch.key ) {
|
|
1018
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
1019
|
+
}
|
|
1020
|
+
}
|
|
1021
|
+
} );
|
|
1022
|
+
}
|
|
1023
|
+
|
|
1024
|
+
|
|
1025
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1026
|
+
|
|
1027
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1028
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1029
|
+
0;
|
|
1030
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1031
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1032
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
1033
|
+
return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1034
|
+
}
|
|
1035
|
+
const taggingData = {
|
|
1036
|
+
size: 10000,
|
|
1037
|
+
query: {
|
|
1038
|
+
bool: {
|
|
1039
|
+
must: [
|
|
1040
|
+
{
|
|
1041
|
+
term: {
|
|
1042
|
+
'storeId.keyword': inputData.storeId,
|
|
1043
|
+
},
|
|
1044
|
+
},
|
|
1045
|
+
{
|
|
1046
|
+
term: {
|
|
1047
|
+
'dateString': inputData.dateString,
|
|
1048
|
+
},
|
|
1049
|
+
},
|
|
1050
|
+
],
|
|
1051
|
+
},
|
|
1052
|
+
},
|
|
1053
|
+
};
|
|
1054
|
+
|
|
1055
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1056
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1057
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1058
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1059
|
+
}
|
|
1060
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
1061
|
+
|
|
1062
|
+
const getTicket = {
|
|
1063
|
+
size: 10000,
|
|
1064
|
+
query: {
|
|
1065
|
+
bool: {
|
|
1066
|
+
must: [
|
|
1067
|
+
{
|
|
1068
|
+
term: {
|
|
1069
|
+
'storeId.keyword': inputData.storeId,
|
|
1070
|
+
},
|
|
1071
|
+
},
|
|
1072
|
+
{
|
|
1073
|
+
term: {
|
|
1074
|
+
'dateString': inputData.dateString,
|
|
1075
|
+
},
|
|
1076
|
+
},
|
|
1077
|
+
],
|
|
1078
|
+
},
|
|
1079
|
+
},
|
|
1080
|
+
};
|
|
1081
|
+
|
|
1082
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1083
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1084
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1085
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1086
|
+
}
|
|
1087
|
+
const record = {
|
|
1088
|
+
|
|
1089
|
+
status: 'Reviewer-Closed',
|
|
1090
|
+
revicedFootfall: revisedFootfall,
|
|
1091
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1092
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1093
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1094
|
+
// createdByEmail: req?.user?.email,
|
|
1095
|
+
// createdByUserName: req?.user?.userName,
|
|
1096
|
+
// createdByRole: req?.user?.role,
|
|
1097
|
+
|
|
1098
|
+
};
|
|
1099
|
+
|
|
1100
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1101
|
+
const temp = record.mappingInfo
|
|
1102
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1103
|
+
.map( ( item ) => ( {
|
|
1104
|
+
...item,
|
|
1105
|
+
mode: inputData.mode,
|
|
1106
|
+
revicedFootfall: revisedFootfall,
|
|
1107
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1108
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1109
|
+
count: tempAcc,
|
|
1110
|
+
revisedDetail: formattedTaggingData,
|
|
1111
|
+
status: 'Closed',
|
|
1112
|
+
createdByEmail: req?.user?.email,
|
|
1113
|
+
createdByUserName: req?.user?.userName,
|
|
1114
|
+
createdByRole: req?.user?.role,
|
|
1115
|
+
createdAt: new Date(),
|
|
1116
|
+
} ) );
|
|
1117
|
+
record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
|
|
1118
|
+
// If no review mapping existed, push a new one
|
|
1119
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1120
|
+
record.mappingInfo.push( {
|
|
1121
|
+
type: 'review',
|
|
1122
|
+
mode: inputData.mode,
|
|
1123
|
+
revicedFootfall: revisedFootfall,
|
|
1124
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1125
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1126
|
+
count: tempAcc,
|
|
1127
|
+
revisedDetail: formattedTaggingData,
|
|
1128
|
+
status: 'Closed',
|
|
1129
|
+
createdByEmail: req?.user?.email,
|
|
1130
|
+
createdByUserName: req?.user?.userName,
|
|
1131
|
+
createdByRole: req?.user?.role,
|
|
1132
|
+
createdAt: new Date(),
|
|
1133
|
+
} );
|
|
1134
|
+
}
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1139
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
1140
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1141
|
+
|
|
1142
|
+
|
|
1143
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1144
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1145
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1146
|
+
|
|
1147
|
+
|
|
1148
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1149
|
+
if (
|
|
1150
|
+
isAutoCloseEnable === true &&
|
|
1151
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1152
|
+
) {
|
|
1153
|
+
record.status = 'Reviewer-Closed';
|
|
1154
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1155
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1156
|
+
const temp = record.mappingInfo
|
|
1157
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1158
|
+
.map( ( item ) => ( {
|
|
1159
|
+
...item,
|
|
1160
|
+
mode: inputData.mode,
|
|
1161
|
+
revicedFootfall: revisedFootfall,
|
|
1162
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1163
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1164
|
+
count: tempAcc,
|
|
1165
|
+
revisedDetail: formattedTaggingData,
|
|
1166
|
+
status: 'Closed',
|
|
1167
|
+
createdByEmail: req?.user?.email,
|
|
1168
|
+
createdByUserName: req?.user?.userName,
|
|
1169
|
+
createdByRole: req?.user?.role,
|
|
1170
|
+
} ) );
|
|
1171
|
+
|
|
1172
|
+
const temp2 = record.mappingInfo
|
|
1173
|
+
.filter( ( item ) => item.type === 'tagging' )
|
|
1174
|
+
.map( ( item ) => ( {
|
|
1175
|
+
...item,
|
|
1176
|
+
mode: inputData.mode,
|
|
1177
|
+
// revicedFootfall: revisedFootfall,
|
|
1178
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1179
|
+
// count: tempAcc,
|
|
1180
|
+
// revisedDetail: formattedTaggingData,
|
|
1181
|
+
status: 'Closed',
|
|
1182
|
+
// createdByEmail: req?.user?.email,
|
|
1183
|
+
// createdByUserName: req?.user?.userName,
|
|
1184
|
+
// createdByRole: req?.user?.role,
|
|
1185
|
+
} ) );
|
|
1186
|
+
record.mappingInfo = [ ...temp2, ...temp ];
|
|
1187
|
+
// If no review mapping existed, push a new one
|
|
1188
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1189
|
+
record.mappingInfo.push( {
|
|
1190
|
+
type: 'review',
|
|
1191
|
+
mode: inputData.mode,
|
|
1192
|
+
revicedFootfall: revisedFootfall,
|
|
1193
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1194
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1195
|
+
count: tempAcc,
|
|
1196
|
+
revisedDetail: formattedTaggingData,
|
|
1197
|
+
status: 'Closed',
|
|
1198
|
+
createdByEmail: req?.user?.email,
|
|
1199
|
+
createdByUserName: req?.user?.userName,
|
|
1200
|
+
createdByRole: req?.user?.role,
|
|
1201
|
+
} );
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
record.mappingInfo.push(
|
|
1205
|
+
{
|
|
1206
|
+
type: 'finalRevision',
|
|
1207
|
+
mode: inputData.mode,
|
|
1208
|
+
revicedFootfall: revisedFootfall,
|
|
1209
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1210
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1211
|
+
count: tempAcc,
|
|
1212
|
+
revisedDetail: formattedTaggingData,
|
|
1213
|
+
status: 'Closed',
|
|
1214
|
+
createdByEmail: req?.user?.email,
|
|
1215
|
+
createdByUserName: req?.user?.userName,
|
|
1216
|
+
createdByRole: req?.user?.role,
|
|
1217
|
+
createdAt: new Date(),
|
|
1218
|
+
},
|
|
1219
|
+
);
|
|
1220
|
+
} else {
|
|
1221
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1222
|
+
let revisionArray = [];
|
|
1223
|
+
|
|
1224
|
+
|
|
1225
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
1226
|
+
|
|
1227
|
+
|
|
1228
|
+
// Default fallbacks
|
|
1229
|
+
let revisionMapping = null;
|
|
1230
|
+
let approverMapping = null;
|
|
1231
|
+
let tangoReviewMapping = null;
|
|
1232
|
+
|
|
1233
|
+
// Find out which roles have isChecked true
|
|
1234
|
+
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
1235
|
+
for ( const r of revisionArray ) {
|
|
1236
|
+
if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
1237
|
+
approverMapping = {
|
|
1238
|
+
type: 'approve',
|
|
1239
|
+
// revicedFootfall: revisedFootfall,
|
|
1240
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1241
|
+
count: tempAcc,
|
|
1242
|
+
revisedDetail: formattedTaggingData,
|
|
1243
|
+
status: 'Open',
|
|
1244
|
+
dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
1245
|
+
};
|
|
1246
|
+
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1247
|
+
tangoReviewMapping = {
|
|
1248
|
+
type: 'tangoreview',
|
|
1249
|
+
// revicedFootfall: revisedFootfall,
|
|
1250
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1251
|
+
count: tempAcc,
|
|
1252
|
+
revisedDetail: formattedTaggingData,
|
|
1253
|
+
status: 'Open',
|
|
1254
|
+
dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
1255
|
+
};
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1258
|
+
}
|
|
1259
|
+
|
|
1260
|
+
// Insert appropriate mappingInfo blocks
|
|
1261
|
+
if ( revisionMapping ) {
|
|
1262
|
+
// If reviewer and checked
|
|
1263
|
+
record.mappingInfo.push( revisionMapping );
|
|
1264
|
+
} else if ( approverMapping ) {
|
|
1265
|
+
// If approver and checked
|
|
1266
|
+
record.mappingInfo.push( approverMapping );
|
|
1267
|
+
} else if ( tangoReviewMapping ) {
|
|
1268
|
+
// If none above, then tangoReview
|
|
1269
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1270
|
+
}
|
|
1271
|
+
}
|
|
1272
|
+
|
|
1273
|
+
let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
|
|
1274
|
+
|
|
1275
|
+
|
|
1276
|
+
if ( checkreview.length > 0 ) {
|
|
1277
|
+
let userQuery = [
|
|
1278
|
+
{
|
|
1279
|
+
$match: {
|
|
1280
|
+
clientId: getstoreName.clientId,
|
|
1281
|
+
role: 'admin',
|
|
1282
|
+
isActive: true,
|
|
1283
|
+
},
|
|
1284
|
+
},
|
|
1285
|
+
];
|
|
1286
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1287
|
+
|
|
1288
|
+
|
|
1289
|
+
// return;
|
|
1290
|
+
for ( let userData of finduserList ) {
|
|
1291
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1292
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1293
|
+
let description = `Created on ${createdOn}`;
|
|
1294
|
+
|
|
1295
|
+
let Data = {
|
|
1296
|
+
'title': title,
|
|
1297
|
+
'body': description,
|
|
1298
|
+
'type': 'review',
|
|
1299
|
+
'date': record.dateString,
|
|
1300
|
+
'storeId': record.storeId,
|
|
1301
|
+
'clientId': record.clientId,
|
|
1302
|
+
'ticketId': record.ticketId,
|
|
1303
|
+
};
|
|
1304
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1305
|
+
|
|
1306
|
+
if ( ticketsFeature ) {
|
|
1307
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1308
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1309
|
+
const fcmToken = userData.fcmToken;
|
|
1310
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1311
|
+
}
|
|
1312
|
+
}
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1317
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1318
|
+
|
|
1319
|
+
if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
|
|
1320
|
+
if ( record.status = 'Closed' ) {
|
|
1321
|
+
const query = {
|
|
1322
|
+
storeId: inputData?.storeId,
|
|
1323
|
+
isVideoStream: true,
|
|
1324
|
+
};
|
|
1325
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
1326
|
+
const revopInfoQuery = {
|
|
1327
|
+
size: 10000,
|
|
1328
|
+
query: {
|
|
1329
|
+
bool: {
|
|
1330
|
+
must: [
|
|
1331
|
+
{
|
|
1332
|
+
term: {
|
|
1333
|
+
'storeId.keyword': inputData.storeId,
|
|
1334
|
+
},
|
|
1335
|
+
},
|
|
1336
|
+
{
|
|
1337
|
+
term: {
|
|
1338
|
+
'dateString': inputData.dateString,
|
|
1339
|
+
},
|
|
1340
|
+
},
|
|
1341
|
+
{
|
|
1342
|
+
term: {
|
|
1343
|
+
'isParent': false,
|
|
1344
|
+
},
|
|
1345
|
+
},
|
|
1346
|
+
{
|
|
1347
|
+
term: {
|
|
1348
|
+
isChecked: true,
|
|
1349
|
+
},
|
|
1350
|
+
},
|
|
1351
|
+
],
|
|
1352
|
+
},
|
|
1353
|
+
},
|
|
1354
|
+
_source: [ 'tempId' ],
|
|
1355
|
+
|
|
1356
|
+
};
|
|
1357
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
1358
|
+
// Get all tempIds from revopInfo response
|
|
1359
|
+
const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
1360
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
1361
|
+
const managerEyeZoneQuery = {
|
|
1362
|
+
size: 1,
|
|
1363
|
+
query: {
|
|
1364
|
+
bool: {
|
|
1365
|
+
must: [
|
|
1366
|
+
{
|
|
1367
|
+
term: {
|
|
1368
|
+
'storeId.keyword': inputData.storeId,
|
|
1369
|
+
},
|
|
1370
|
+
},
|
|
1371
|
+
{
|
|
1372
|
+
term: {
|
|
1373
|
+
'storeDate': inputData.dateString,
|
|
1374
|
+
},
|
|
1375
|
+
},
|
|
1376
|
+
],
|
|
1377
|
+
},
|
|
1378
|
+
},
|
|
1379
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
1380
|
+
};
|
|
1381
|
+
|
|
1382
|
+
// Query the managerEyeZone index for the matching document
|
|
1383
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
1384
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
1385
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
1386
|
+
const mapping =
|
|
1387
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
1388
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
1389
|
+
{};
|
|
1390
|
+
|
|
1391
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
1392
|
+
const temp = [];
|
|
1393
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
|
|
1394
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
1395
|
+
if ( isSendMessge == true ) {
|
|
1396
|
+
logger.info( '....1' );
|
|
1397
|
+
}
|
|
1398
|
+
}
|
|
1399
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1400
|
+
} else {
|
|
1401
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1402
|
+
}
|
|
1403
|
+
} catch ( error ) {
|
|
1404
|
+
const err = error.message || 'Internal Server Error';
|
|
1405
|
+
logger.error( { error: err, funtion: 'ticketreview' } );
|
|
1406
|
+
return res.sendError( err, 500 );
|
|
1407
|
+
}
|
|
1408
|
+
}
|
|
1409
|
+
|
|
1410
|
+
export async function ticketApprove( req, res, next ) {
|
|
1411
|
+
try {
|
|
1412
|
+
const inputData = req.body;
|
|
1413
|
+
if ( inputData?.type !== 'approve' ) {
|
|
1414
|
+
return next();
|
|
1415
|
+
}
|
|
1416
|
+
// check the createtion permission from the user permission
|
|
1417
|
+
const userInfo = req?.user;
|
|
1418
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1419
|
+
if ( !ticketsFeature ) {
|
|
1420
|
+
return res.sendError( 'Forbidden to Approve this Ticket', 403 );
|
|
1421
|
+
}
|
|
1422
|
+
|
|
1423
|
+
// get store info by the storeId into mongo db
|
|
1424
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
1425
|
+
|
|
1426
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
1427
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
1428
|
+
}
|
|
1429
|
+
|
|
1430
|
+
// get the footfall count from opensearch
|
|
1431
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1432
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
1433
|
+
const getQuery = {
|
|
1434
|
+
query: {
|
|
1435
|
+
terms: {
|
|
1436
|
+
_id: [ dateString ],
|
|
1437
|
+
},
|
|
1438
|
+
},
|
|
1439
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
1440
|
+
sort: [
|
|
1441
|
+
{
|
|
1442
|
+
date_iso: {
|
|
1443
|
+
order: 'desc',
|
|
1444
|
+
},
|
|
1445
|
+
},
|
|
1446
|
+
],
|
|
1447
|
+
};
|
|
1448
|
+
|
|
1449
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
1450
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
1451
|
+
logger.info( { hits } );
|
|
1452
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
1453
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
1454
|
+
}
|
|
1455
|
+
|
|
1456
|
+
// get category details from the client level configuration
|
|
1457
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
1458
|
+
if ( !getConfig || getConfig == null ) {
|
|
1459
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
1460
|
+
}
|
|
1461
|
+
|
|
1462
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
1463
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
1464
|
+
// Initialize count object from taggingLimitation
|
|
1465
|
+
const tempAcc = [];
|
|
1466
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
1467
|
+
if ( item?.type ) {
|
|
1468
|
+
// Convert type to camelCase with "Count" suffix
|
|
1469
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
1470
|
+
const typeLower = item.type.toLowerCase();
|
|
1471
|
+
let key;
|
|
1472
|
+
if ( typeLower === 'housekeeping' ) {
|
|
1473
|
+
key = 'houseKeepingCount';
|
|
1474
|
+
} else {
|
|
1475
|
+
// Convert first letter to lowercase and append "Count"
|
|
1476
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
1477
|
+
}
|
|
1478
|
+
|
|
1479
|
+
|
|
1480
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
1481
|
+
tempAcc.push( {
|
|
1482
|
+
name: item.name,
|
|
1483
|
+
value: 0,
|
|
1484
|
+
key: key,
|
|
1485
|
+
type: item.type,
|
|
1486
|
+
} );
|
|
1487
|
+
|
|
1488
|
+
|
|
1489
|
+
return acc;
|
|
1490
|
+
}
|
|
1491
|
+
}, {} ) || {};
|
|
1492
|
+
|
|
1493
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
1494
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
1495
|
+
const revopQuery = {
|
|
1496
|
+
size: 0,
|
|
1497
|
+
query: {
|
|
1498
|
+
bool: {
|
|
1499
|
+
must: [
|
|
1500
|
+
{
|
|
1501
|
+
term: {
|
|
1502
|
+
'storeId.keyword': inputData.storeId,
|
|
1503
|
+
},
|
|
1504
|
+
},
|
|
1505
|
+
{
|
|
1506
|
+
term: {
|
|
1507
|
+
'dateString': inputData.dateString,
|
|
1508
|
+
},
|
|
1509
|
+
},
|
|
1510
|
+
],
|
|
1511
|
+
},
|
|
1512
|
+
},
|
|
1513
|
+
aggs: {
|
|
1514
|
+
type_counts: {
|
|
1515
|
+
terms: {
|
|
1516
|
+
field: 'revopsType.keyword',
|
|
1517
|
+
size: 100,
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
},
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
1525
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
1526
|
+
|
|
1527
|
+
// Map OpenSearch revopsType values to count object keys
|
|
1528
|
+
buckets.forEach( ( bucket ) => {
|
|
1529
|
+
const revopsType = bucket.key;
|
|
1530
|
+
const count = bucket.doc_count || 0;
|
|
1531
|
+
|
|
1532
|
+
|
|
1533
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
1534
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
1535
|
+
const accMatch = tempAcc.find(
|
|
1536
|
+
( acc ) =>
|
|
1537
|
+
acc.type &&
|
|
1538
|
+
acc.type === revopsType,
|
|
1539
|
+
);
|
|
1540
|
+
|
|
1541
|
+
if ( accMatch && accMatch.key ) {
|
|
1542
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
1543
|
+
}
|
|
1544
|
+
}
|
|
1545
|
+
} );
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
|
|
1549
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1550
|
+
|
|
1551
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1552
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1553
|
+
0;
|
|
1554
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1555
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1556
|
+
logger.info( { footfallCount, revisedFootfall } );
|
|
1557
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
1558
|
+
return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1559
|
+
}
|
|
1560
|
+
|
|
1561
|
+
const taggingData = {
|
|
1562
|
+
size: 10000,
|
|
1563
|
+
query: {
|
|
1564
|
+
bool: {
|
|
1565
|
+
must: [
|
|
1566
|
+
{
|
|
1567
|
+
term: {
|
|
1568
|
+
'storeId.keyword': inputData.storeId,
|
|
1569
|
+
},
|
|
1570
|
+
},
|
|
1571
|
+
{
|
|
1572
|
+
term: {
|
|
1573
|
+
'dateString': inputData.dateString,
|
|
1574
|
+
},
|
|
1575
|
+
},
|
|
1576
|
+
{
|
|
1577
|
+
term: {
|
|
1578
|
+
'isParent': false,
|
|
1579
|
+
},
|
|
1580
|
+
},
|
|
1581
|
+
{
|
|
1582
|
+
term: {
|
|
1583
|
+
isChecked: true,
|
|
1584
|
+
},
|
|
1585
|
+
},
|
|
1586
|
+
],
|
|
1587
|
+
},
|
|
1588
|
+
},
|
|
1589
|
+
};
|
|
1590
|
+
|
|
1591
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1592
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1593
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1594
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1595
|
+
}
|
|
1596
|
+
|
|
1597
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
1598
|
+
|
|
1599
|
+
const getTicket = {
|
|
1600
|
+
size: 10000,
|
|
1601
|
+
query: {
|
|
1602
|
+
bool: {
|
|
1603
|
+
must: [
|
|
1604
|
+
{
|
|
1605
|
+
term: {
|
|
1606
|
+
'storeId.keyword': inputData.storeId,
|
|
1607
|
+
},
|
|
1608
|
+
},
|
|
1609
|
+
{
|
|
1610
|
+
term: {
|
|
1611
|
+
'dateString': inputData.dateString,
|
|
1612
|
+
},
|
|
1613
|
+
},
|
|
1614
|
+
],
|
|
1615
|
+
},
|
|
1616
|
+
},
|
|
1617
|
+
};
|
|
1618
|
+
|
|
1619
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1620
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1621
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1622
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1623
|
+
}
|
|
1624
|
+
logger.info( { ticketData, getFootfallticketData } );
|
|
1625
|
+
const record = {
|
|
1626
|
+
|
|
1627
|
+
status: 'Approver-Closed',
|
|
1628
|
+
revicedFootfall: revisedFootfall,
|
|
1629
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1630
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1631
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1632
|
+
// createdByEmail: req?.user?.email,
|
|
1633
|
+
// createdByUserName: req?.user?.userName,
|
|
1634
|
+
// createdByRole: req?.user?.role,
|
|
1635
|
+
|
|
1636
|
+
};
|
|
1637
|
+
|
|
1638
|
+
|
|
1639
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1640
|
+
let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
|
|
1641
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1642
|
+
|
|
1643
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1644
|
+
logger.info( { getNumber } );
|
|
1645
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1646
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1647
|
+
const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
|
|
1648
|
+
const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
|
|
1649
|
+
logger.info( { tangoReview, revised } );
|
|
1650
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1651
|
+
if (
|
|
1652
|
+
isAutoCloseEnable === true &&
|
|
1653
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1654
|
+
) {
|
|
1655
|
+
record.status = 'Approver-Closed';
|
|
1656
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1657
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1658
|
+
const temp = record.mappingInfo
|
|
1659
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1660
|
+
.map( ( item ) => ( {
|
|
1661
|
+
...item,
|
|
1662
|
+
|
|
1663
|
+
mode: inputData.mode,
|
|
1664
|
+
revicedFootfall: revisedFootfall,
|
|
1665
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1666
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1667
|
+
count: tempAcc,
|
|
1668
|
+
revisedDetail: formattedTaggingData,
|
|
1669
|
+
status: 'Closed',
|
|
1670
|
+
createdByEmail: req?.user?.email,
|
|
1671
|
+
createdByUserName: req?.user?.userName,
|
|
1672
|
+
createdByRole: req?.user?.role,
|
|
1673
|
+
} ) );
|
|
1674
|
+
|
|
1675
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1676
|
+
...temp ];
|
|
1677
|
+
// If updating the mapping config to mark [i].status as 'Closed'
|
|
1678
|
+
// Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
|
|
1679
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1680
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1681
|
+
return {
|
|
1682
|
+
...item,
|
|
1683
|
+
status: 'Closed',
|
|
1684
|
+
};
|
|
1685
|
+
} );
|
|
1686
|
+
}
|
|
1687
|
+
// If no review mapping existed, push a new one
|
|
1688
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1689
|
+
record.mappingInfo.push( {
|
|
1690
|
+
type: 'approve',
|
|
1691
|
+
mode: inputData.mode,
|
|
1692
|
+
revicedFootfall: revisedFootfall,
|
|
1693
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1694
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1695
|
+
count: tempAcc,
|
|
1696
|
+
revisedDetail: formattedTaggingData,
|
|
1697
|
+
status: 'Closed',
|
|
1698
|
+
createdByEmail: req?.user?.email,
|
|
1699
|
+
createdByUserName: req?.user?.userName,
|
|
1700
|
+
createdByRole: req?.user?.role,
|
|
1701
|
+
} );
|
|
1702
|
+
}
|
|
1703
|
+
}
|
|
1704
|
+
record.mappingInfo.push(
|
|
1705
|
+
{
|
|
1706
|
+
type: 'finalRevision',
|
|
1707
|
+
mode: inputData.mode,
|
|
1708
|
+
revicedFootfall: revisedFootfall,
|
|
1709
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1710
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1711
|
+
count: tempAcc,
|
|
1712
|
+
revisedDetail: formattedTaggingData,
|
|
1713
|
+
status: 'Closed',
|
|
1714
|
+
createdByEmail: req?.user?.email,
|
|
1715
|
+
createdByUserName: req?.user?.userName,
|
|
1716
|
+
createdByRole: req?.user?.role,
|
|
1717
|
+
createdAt: new Date(),
|
|
1718
|
+
},
|
|
1719
|
+
);
|
|
1720
|
+
} else if ( revised < tangoReview ) {
|
|
1721
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1722
|
+
|
|
1723
|
+
// Default fallbacks
|
|
1724
|
+
|
|
1725
|
+
let approverMapping = null;
|
|
1726
|
+
let tangoReviewMapping = null;
|
|
1727
|
+
|
|
1728
|
+
record.status = 'Approver-Closed';
|
|
1729
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1730
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1731
|
+
const temp = record.mappingInfo
|
|
1732
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1733
|
+
.map( ( item ) => ( {
|
|
1734
|
+
...item,
|
|
1735
|
+
mode: inputData.mode,
|
|
1736
|
+
revicedFootfall: revisedFootfall,
|
|
1737
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1738
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1739
|
+
count: tempAcc,
|
|
1740
|
+
revisedDetail: formattedTaggingData,
|
|
1741
|
+
status: 'Under Tango Review',
|
|
1742
|
+
createdByEmail: req?.user?.email,
|
|
1743
|
+
createdByUserName: req?.user?.userName,
|
|
1744
|
+
createdByRole: req?.user?.role,
|
|
1745
|
+
} ) );
|
|
1746
|
+
|
|
1747
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1748
|
+
...temp ];
|
|
1749
|
+
|
|
1750
|
+
// If no review mapping existed, push a new one
|
|
1751
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1752
|
+
record.mappingInfo.push( {
|
|
1753
|
+
type: 'approve',
|
|
1754
|
+
mode: inputData.mode,
|
|
1755
|
+
revicedFootfall: revisedFootfall,
|
|
1756
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1757
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1758
|
+
count: tempAcc,
|
|
1759
|
+
revisedDetail: formattedTaggingData,
|
|
1760
|
+
status: 'Closed',
|
|
1761
|
+
createdByEmail: req?.user?.email,
|
|
1762
|
+
createdByUserName: req?.user?.userName,
|
|
1763
|
+
createdByRole: req?.user?.role,
|
|
1764
|
+
} );
|
|
1765
|
+
}
|
|
1766
|
+
}
|
|
1767
|
+
|
|
1768
|
+
// Find out which roles have isChecked true
|
|
1769
|
+
|
|
1770
|
+
// for ( const r of revisionArray ) {
|
|
1771
|
+
// if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1772
|
+
tangoReviewMapping = {
|
|
1773
|
+
type: 'tangoreview',
|
|
1774
|
+
// revicedFootfall: revisedFootfall,
|
|
1775
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1776
|
+
count: tempAcc,
|
|
1777
|
+
revisedDetail: formattedTaggingData,
|
|
1778
|
+
status: 'Open',
|
|
1779
|
+
dueDate: new Date( Date.now() + 4 * 24 * 60 * 60 * 1000 ), // Current date plus 3 days
|
|
1780
|
+
};
|
|
1781
|
+
// }
|
|
1782
|
+
// }
|
|
1783
|
+
|
|
1784
|
+
|
|
1785
|
+
if ( approverMapping ) {
|
|
1786
|
+
// If approver and checked
|
|
1787
|
+
record.mappingInfo.push( approverMapping );
|
|
1788
|
+
} else if ( tangoReviewMapping ) {
|
|
1789
|
+
// If none above, then tangoReview
|
|
1790
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1791
|
+
}
|
|
1792
|
+
} else {
|
|
1793
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1794
|
+
const temp = record.mappingInfo
|
|
1795
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1796
|
+
.map( ( item ) => ( {
|
|
1797
|
+
...item,
|
|
1798
|
+
mode: inputData.mode,
|
|
1799
|
+
revicedFootfall: revisedFootfall,
|
|
1800
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1801
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1802
|
+
count: tempAcc,
|
|
1803
|
+
revisedDetail: formattedTaggingData,
|
|
1804
|
+
status: 'Closed',
|
|
1805
|
+
createdByEmail: req?.user?.email,
|
|
1806
|
+
createdByUserName: req?.user?.userName,
|
|
1807
|
+
createdByRole: req?.user?.role,
|
|
1808
|
+
} ) );
|
|
1809
|
+
|
|
1810
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1811
|
+
...temp ];
|
|
1812
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1813
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1814
|
+
return {
|
|
1815
|
+
...item,
|
|
1816
|
+
status: 'Closed',
|
|
1817
|
+
};
|
|
1818
|
+
} );
|
|
1819
|
+
}
|
|
1820
|
+
// If no review mapping existed, push a new one
|
|
1821
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1822
|
+
record.mappingInfo.push( {
|
|
1823
|
+
type: 'approve',
|
|
1824
|
+
mode: inputData.mode,
|
|
1825
|
+
revicedFootfall: revisedFootfall,
|
|
1826
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1827
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1828
|
+
count: tempAcc,
|
|
1829
|
+
revisedDetail: formattedTaggingData,
|
|
1830
|
+
status: 'Closed',
|
|
1831
|
+
createdByEmail: req?.user?.email,
|
|
1832
|
+
createdByUserName: req?.user?.userName,
|
|
1833
|
+
createdByRole: req?.user?.role,
|
|
1834
|
+
} );
|
|
1835
|
+
}
|
|
1836
|
+
}
|
|
1837
|
+
record.mappingInfo.push(
|
|
1838
|
+
{
|
|
1839
|
+
type: 'finalRevision',
|
|
1840
|
+
mode: inputData.mode,
|
|
1841
|
+
revicedFootfall: revisedFootfall,
|
|
1842
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1843
|
+
reviced: Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) ),
|
|
1844
|
+
count: tempAcc,
|
|
1845
|
+
revisedDetail: formattedTaggingData,
|
|
1846
|
+
status: 'Closed',
|
|
1847
|
+
createdByEmail: req?.user?.email,
|
|
1848
|
+
createdByUserName: req?.user?.userName,
|
|
1849
|
+
createdByRole: req?.user?.role,
|
|
1850
|
+
createdAt: new Date(),
|
|
1851
|
+
},
|
|
1852
|
+
);
|
|
1853
|
+
}
|
|
1854
|
+
console.log( req.body, getConfig.footfallDirectoryConfigs.revision );
|
|
1855
|
+
let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
|
|
1856
|
+
|
|
1857
|
+
|
|
1858
|
+
if ( checkapprove.length > 0 ) {
|
|
1859
|
+
let userQuery = [
|
|
1860
|
+
{
|
|
1861
|
+
$match: {
|
|
1862
|
+
clientId: getstoreName.clientId,
|
|
1863
|
+
role: 'admin',
|
|
1864
|
+
isActive: true,
|
|
1865
|
+
},
|
|
1866
|
+
},
|
|
1867
|
+
];
|
|
1868
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1869
|
+
|
|
1870
|
+
|
|
1871
|
+
for ( let userData of finduserList ) {
|
|
1872
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1873
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1874
|
+
let description = `Created on ${createdOn}`;
|
|
1875
|
+
|
|
1876
|
+
let Data = {
|
|
1877
|
+
'title': title,
|
|
1878
|
+
'body': description,
|
|
1879
|
+
'type': 'approve',
|
|
1880
|
+
'date': record.dateString,
|
|
1881
|
+
'storeId': record.storeId,
|
|
1882
|
+
'clientId': record.clientId,
|
|
1883
|
+
'ticketId': record.ticketId,
|
|
1884
|
+
};
|
|
1885
|
+
|
|
1886
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1887
|
+
|
|
1888
|
+
if ( ticketsFeature ) {
|
|
1889
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1890
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1891
|
+
const fcmToken = userData.fcmToken;
|
|
1892
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
}
|
|
1896
|
+
}
|
|
1897
|
+
|
|
1898
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1899
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1900
|
+
|
|
1901
|
+
|
|
1902
|
+
if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
|
|
1903
|
+
if ( record.status = 'Closed' ) {
|
|
1904
|
+
const query = {
|
|
1905
|
+
storeId: inputData?.storeId,
|
|
1906
|
+
isVideoStream: true,
|
|
1907
|
+
};
|
|
1908
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
1909
|
+
const revopInfoQuery = {
|
|
1910
|
+
size: 10000,
|
|
1911
|
+
query: {
|
|
1912
|
+
bool: {
|
|
1913
|
+
must: [
|
|
1914
|
+
{
|
|
1915
|
+
term: {
|
|
1916
|
+
'storeId.keyword': inputData.storeId,
|
|
1917
|
+
},
|
|
1918
|
+
},
|
|
1919
|
+
{
|
|
1920
|
+
term: {
|
|
1921
|
+
'dateString': inputData.dateString,
|
|
1922
|
+
},
|
|
1923
|
+
},
|
|
1924
|
+
{
|
|
1925
|
+
term: {
|
|
1926
|
+
'isParent': false,
|
|
1927
|
+
},
|
|
1928
|
+
},
|
|
1929
|
+
{
|
|
1930
|
+
term: {
|
|
1931
|
+
isChecked: true,
|
|
1932
|
+
},
|
|
1933
|
+
},
|
|
1934
|
+
],
|
|
1935
|
+
},
|
|
1936
|
+
},
|
|
1937
|
+
_source: [ 'tempId' ],
|
|
1938
|
+
|
|
1939
|
+
};
|
|
1940
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
1941
|
+
// Get all tempIds from revopInfo response
|
|
1942
|
+
const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
1943
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
1944
|
+
const managerEyeZoneQuery = {
|
|
1945
|
+
size: 1,
|
|
1946
|
+
query: {
|
|
1947
|
+
bool: {
|
|
1948
|
+
must: [
|
|
1949
|
+
{
|
|
1950
|
+
term: {
|
|
1951
|
+
'storeId.keyword': inputData.storeId,
|
|
1952
|
+
},
|
|
1953
|
+
},
|
|
1954
|
+
{
|
|
1955
|
+
term: {
|
|
1956
|
+
'storeDate': inputData.dateString,
|
|
1957
|
+
},
|
|
1958
|
+
},
|
|
1959
|
+
],
|
|
1960
|
+
},
|
|
1961
|
+
},
|
|
1962
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
1963
|
+
};
|
|
1964
|
+
|
|
1965
|
+
// Query the managerEyeZone index for the matching document
|
|
1966
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
1967
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
1968
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
1969
|
+
const mapping =
|
|
1970
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
1971
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
1972
|
+
{};
|
|
1973
|
+
|
|
1974
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
1975
|
+
const temp = [];
|
|
1976
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
|
|
1977
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
1978
|
+
if ( isSendMessge == true ) {
|
|
1979
|
+
logger.info( '....1' );
|
|
1980
|
+
}
|
|
1981
|
+
}
|
|
1982
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1983
|
+
} else {
|
|
1984
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1985
|
+
}
|
|
1986
|
+
} catch ( error ) {
|
|
1987
|
+
const err = error.message || 'Internal Server Error';
|
|
1988
|
+
logger.error( { error: err, funtion: 'ticketCreation' } );
|
|
1989
|
+
return res.sendError( err, 500 );
|
|
1990
|
+
}
|
|
1991
|
+
}
|
|
1992
|
+
|
|
1993
|
+
export async function getAssinedStore( user, storeId ) {
|
|
1994
|
+
if ( !user || user.userType !== 'client' || user.role === 'superadmin' ) {
|
|
1995
|
+
return;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
const clientId = user.clientId;
|
|
1999
|
+
const storeIds = new Set(
|
|
2000
|
+
user.assignedStores?.map( ( store ) => store.storeId ) ?? [],
|
|
2001
|
+
);
|
|
2002
|
+
|
|
2003
|
+
const addClusterStores = ( clusters ) => {
|
|
2004
|
+
if ( !clusters?.length ) return;
|
|
2005
|
+
for ( const cluster of clusters ) {
|
|
2006
|
+
cluster.stores?.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
2007
|
+
}
|
|
2008
|
+
};
|
|
2009
|
+
|
|
2010
|
+
// Fetch all top-level data in parallel
|
|
2011
|
+
const [ clustersList, teamsList, teamMemberList ] = await Promise.all( [
|
|
2012
|
+
findcluster( {
|
|
2013
|
+
clientId,
|
|
2014
|
+
Teamlead: { $elemMatch: { email: user.email } },
|
|
2015
|
+
} ),
|
|
2016
|
+
findteams( {
|
|
2017
|
+
clientId,
|
|
2018
|
+
Teamlead: { $elemMatch: { email: user.email } },
|
|
2019
|
+
} ),
|
|
2020
|
+
findteams( {
|
|
2021
|
+
clientId,
|
|
2022
|
+
users: { $elemMatch: { email: user.email } },
|
|
2023
|
+
} ),
|
|
2024
|
+
] );
|
|
2025
|
+
|
|
2026
|
+
// 1) Clusters where this user is Teamlead
|
|
2027
|
+
addClusterStores( clustersList );
|
|
2028
|
+
|
|
2029
|
+
// 2) Teams where this user is Teamlead → their users + their clusters
|
|
2030
|
+
if ( teamsList?.length ) {
|
|
2031
|
+
for ( const team of teamsList ) {
|
|
2032
|
+
if ( !team.users?.length ) continue;
|
|
2033
|
+
|
|
2034
|
+
await Promise.all(
|
|
2035
|
+
team.users.map( async ( teamUser ) => {
|
|
2036
|
+
const foundUser = await findOneUser( { _id: teamUser.userId } );
|
|
2037
|
+
if ( !foundUser ) return;
|
|
2038
|
+
|
|
2039
|
+
// Direct assigned stores of that user
|
|
2040
|
+
if ( foundUser.assignedStores?.length ) {
|
|
2041
|
+
foundUser.assignedStores.forEach( ( store ) =>
|
|
2042
|
+
storeIds.add( store.storeId ),
|
|
2043
|
+
);
|
|
2044
|
+
}
|
|
2045
|
+
|
|
2046
|
+
// Clusters where this user is Teamlead
|
|
2047
|
+
const userClustersList = await findcluster( {
|
|
2048
|
+
clientId,
|
|
2049
|
+
Teamlead: { $elemMatch: { email: foundUser.email } },
|
|
2050
|
+
} );
|
|
2051
|
+
addClusterStores( userClustersList );
|
|
2052
|
+
} ),
|
|
2053
|
+
);
|
|
2054
|
+
}
|
|
2055
|
+
}
|
|
2056
|
+
|
|
2057
|
+
// 3) Teams where this user is a member → clusters by teamName
|
|
2058
|
+
if ( teamMemberList?.length ) {
|
|
2059
|
+
for ( const team of teamMemberList ) {
|
|
2060
|
+
const clusterList = await findcluster( {
|
|
2061
|
+
clientId,
|
|
2062
|
+
teams: { $elemMatch: { name: team.teamName } },
|
|
2063
|
+
} );
|
|
2064
|
+
addClusterStores( clusterList );
|
|
2065
|
+
}
|
|
2066
|
+
}
|
|
2067
|
+
|
|
2068
|
+
const assignedStores = Array.from( storeIds );
|
|
2069
|
+
|
|
2070
|
+
// Previously you returned `true` in both branches.
|
|
2071
|
+
// Assuming you actually want to check membership:
|
|
2072
|
+
return assignedStores.includes( storeId );
|
|
2073
|
+
}
|
|
678
2074
|
|