tango-app-api-infra 3.9.5-vms.6 → 3.9.5-vms.61
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.
|
@@ -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,26 +233,26 @@ 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 ) ) ) );
|
|
231
|
-
logger.info( { ticketsFeature } );
|
|
243
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'creator' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
232
244
|
if ( !ticketsFeature ) {
|
|
233
245
|
return res.sendError( 'Forbidden to Create Ticket', 403 );
|
|
234
246
|
}
|
|
235
247
|
|
|
236
248
|
// get store info by the storeId into mongo db
|
|
237
249
|
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
238
|
-
|
|
250
|
+
|
|
239
251
|
if ( !getstoreName || getstoreName == null ) {
|
|
240
252
|
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
241
253
|
}
|
|
242
254
|
|
|
243
255
|
// get the footfall count from opensearch
|
|
244
|
-
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
245
256
|
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
246
257
|
const getQuery = {
|
|
247
258
|
query: {
|
|
@@ -261,23 +272,21 @@ export async function ticketCreation( req, res, next ) {
|
|
|
261
272
|
|
|
262
273
|
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
263
274
|
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
264
|
-
logger.info( { hits } );
|
|
265
275
|
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
266
276
|
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
267
277
|
}
|
|
268
278
|
|
|
269
279
|
// get category details from the client level configuration
|
|
270
|
-
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
271
|
-
logger.info( { getConfig, ta123: getConfig?.footfallDirectoryConfigs } );
|
|
280
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1, clientId: 1 } );
|
|
272
281
|
if ( !getConfig || getConfig == null ) {
|
|
273
282
|
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
274
283
|
}
|
|
275
284
|
|
|
276
285
|
// Get taggingLimitation from config (check both possible paths)
|
|
277
286
|
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
278
|
-
logger.info( { taggingLimitation, tagginngs: getConfig?.footfallDirectoryConfigs } );
|
|
279
287
|
// Initialize count object from taggingLimitation
|
|
280
|
-
const
|
|
288
|
+
const tempAcc = [];
|
|
289
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
281
290
|
if ( item?.type ) {
|
|
282
291
|
// Convert type to camelCase with "Count" suffix
|
|
283
292
|
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
@@ -289,11 +298,21 @@ export async function ticketCreation( req, res, next ) {
|
|
|
289
298
|
// Convert first letter to lowercase and append "Count"
|
|
290
299
|
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
291
300
|
}
|
|
292
|
-
|
|
301
|
+
|
|
302
|
+
|
|
303
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
304
|
+
tempAcc.push( {
|
|
305
|
+
name: item.name,
|
|
306
|
+
value: 0,
|
|
307
|
+
key: key,
|
|
308
|
+
type: item.type,
|
|
309
|
+
} );
|
|
310
|
+
|
|
311
|
+
|
|
312
|
+
return acc;
|
|
293
313
|
}
|
|
294
|
-
return acc;
|
|
295
314
|
}, {} ) || {};
|
|
296
|
-
|
|
315
|
+
|
|
297
316
|
// Query OpenSearch revop index to get actual counts for each type
|
|
298
317
|
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
299
318
|
const revopQuery = {
|
|
@@ -311,6 +330,16 @@ export async function ticketCreation( req, res, next ) {
|
|
|
311
330
|
'dateString': inputData.dateString,
|
|
312
331
|
},
|
|
313
332
|
},
|
|
333
|
+
{
|
|
334
|
+
term: {
|
|
335
|
+
'isParent': false,
|
|
336
|
+
},
|
|
337
|
+
},
|
|
338
|
+
{
|
|
339
|
+
term: {
|
|
340
|
+
isChecked: true,
|
|
341
|
+
},
|
|
342
|
+
},
|
|
314
343
|
],
|
|
315
344
|
},
|
|
316
345
|
},
|
|
@@ -324,51 +353,38 @@ export async function ticketCreation( req, res, next ) {
|
|
|
324
353
|
},
|
|
325
354
|
};
|
|
326
355
|
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
//
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
|
|
347
|
-
} else if ( revopsType === 'housekeeping' ) {
|
|
348
|
-
if ( getCategory.hasOwnProperty( 'houseKeepingCount' ) ) {
|
|
349
|
-
getCategory.houseKeepingCount = count;
|
|
350
|
-
}
|
|
351
|
-
} else if ( revopsType === 'junk' ) {
|
|
352
|
-
if ( getCategory.hasOwnProperty( 'junkCount' ) ) {
|
|
353
|
-
getCategory.junkCount = count;
|
|
354
|
-
}
|
|
356
|
+
|
|
357
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
358
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
359
|
+
|
|
360
|
+
// Map OpenSearch revopsType values to count object keys
|
|
361
|
+
buckets.forEach( ( bucket ) => {
|
|
362
|
+
const revopsType = bucket.key;
|
|
363
|
+
const count = bucket.doc_count || 0;
|
|
364
|
+
|
|
365
|
+
|
|
366
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
367
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
368
|
+
const accMatch = tempAcc.find(
|
|
369
|
+
( acc ) =>
|
|
370
|
+
acc.type &&
|
|
371
|
+
acc.type === revopsType,
|
|
372
|
+
);
|
|
373
|
+
|
|
374
|
+
if ( accMatch && accMatch.key ) {
|
|
375
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
355
376
|
}
|
|
356
|
-
}
|
|
357
|
-
}
|
|
358
|
-
logger.error( { error: error, message: 'Error fetching revop counts', function: 'footfallDirectoryTicket-ticketCreation' } );
|
|
359
|
-
// Continue with default 0 values if query fails
|
|
360
|
-
}
|
|
377
|
+
}
|
|
378
|
+
} );
|
|
361
379
|
}
|
|
362
380
|
|
|
363
|
-
logger.info( { getCategory: getCategory } );
|
|
364
381
|
|
|
365
382
|
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
383
|
+
|
|
384
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
385
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
386
|
+
0;
|
|
370
387
|
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
371
|
-
logger.info( { footfallCount, totalCount } );
|
|
372
388
|
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
373
389
|
if ( footfallCount - revisedFootfall == 0 ) {
|
|
374
390
|
return res.sendError( 'Cannot create a ticket because footfall hasn’t changed', 400 );
|
|
@@ -400,91 +416,92 @@ export async function ticketCreation( req, res, next ) {
|
|
|
400
416
|
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
401
417
|
}
|
|
402
418
|
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
403
|
-
logger.info( { revopTaggingData: formattedTaggingData } );
|
|
404
419
|
|
|
405
420
|
const record = {
|
|
406
421
|
storeId: inputData.storeId,
|
|
422
|
+
type: 'store',
|
|
407
423
|
dateString: inputData.dateString,
|
|
408
424
|
storeName: getstoreName?.storeName,
|
|
409
|
-
ticketName: inputData.ticketName|| 'footfall-directory',
|
|
425
|
+
ticketName: inputData.ticketName || 'footfall-directory',
|
|
410
426
|
footfallCount: footfallCount,
|
|
411
427
|
clientId: getstoreName?.clientId,
|
|
412
428
|
ticketId: 'TE_FDT_' + new Date().valueOf(),
|
|
413
429
|
createdAt: new Date(),
|
|
414
430
|
updatedAt: new Date(),
|
|
415
|
-
status: '
|
|
431
|
+
status: 'Raised',
|
|
432
|
+
comments: inputData?.comments || '',
|
|
416
433
|
revicedFootfall: revisedFootfall,
|
|
417
|
-
revicedPerc: Math.round( ( revisedFootfall/footfallCount ) * 100 || 0 ) + '%',
|
|
434
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
418
435
|
mappingInfo: [
|
|
419
436
|
{
|
|
420
437
|
type: 'tagging',
|
|
421
438
|
mode: inputData.mode,
|
|
422
439
|
revicedFootfall: revisedFootfall,
|
|
423
|
-
|
|
440
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
441
|
+
count: tempAcc,
|
|
424
442
|
revisedDetail: formattedTaggingData,
|
|
425
|
-
status: '
|
|
443
|
+
status: 'Raised',
|
|
426
444
|
createdByEmail: req?.user?.email,
|
|
427
445
|
createdByUserName: req?.user?.userName,
|
|
428
446
|
createdByRole: req?.user?.role,
|
|
447
|
+
createdAt: new Date(),
|
|
429
448
|
},
|
|
430
449
|
],
|
|
431
450
|
};
|
|
432
451
|
|
|
433
452
|
|
|
434
453
|
// Retrieve client footfallDirectoryConfigs revision
|
|
435
|
-
let isAutoCloseEnable =
|
|
436
|
-
let autoCloseAccuracy =
|
|
437
|
-
try {
|
|
438
|
-
const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
|
|
439
|
-
if ( clientData?.footfallDirectoryConfigs ) {
|
|
440
|
-
isAutoCloseEnable = clientData.footfallDirectoryConfigs.isAutoCloseEnable ?? false;
|
|
441
|
-
autoCloseAccuracy = clientData.footfallDirectoryConfigs.autoCloseAccuracy || '95%';
|
|
442
|
-
}
|
|
443
|
-
} catch ( e ) {
|
|
444
|
-
isAutoCloseEnable = false;
|
|
445
|
-
autoCloseAccuracy = '95%';
|
|
446
|
-
}
|
|
454
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
455
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy; ;
|
|
447
456
|
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
}
|
|
457
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
458
|
+
|
|
459
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
460
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
453
461
|
|
|
454
462
|
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
455
463
|
if (
|
|
456
464
|
isAutoCloseEnable === true &&
|
|
457
465
|
revisedPercentage >= autoCloseAccuracyValue
|
|
458
466
|
) {
|
|
459
|
-
record.status = '
|
|
467
|
+
record.status = 'Closed';
|
|
460
468
|
record.mappingInfo = [
|
|
461
469
|
{
|
|
462
470
|
type: 'tagging',
|
|
463
471
|
mode: inputData.mode,
|
|
464
472
|
revicedFootfall: revisedFootfall,
|
|
465
|
-
|
|
473
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
474
|
+
count: tempAcc,
|
|
475
|
+
revisedDetail: formattedTaggingData,
|
|
476
|
+
status: 'Closed',
|
|
477
|
+
createdByEmail: req?.user?.email,
|
|
478
|
+
createdByUserName: req?.user?.userName,
|
|
479
|
+
createdByRole: req?.user?.role,
|
|
480
|
+
createdAt: new Date(),
|
|
481
|
+
},
|
|
482
|
+
{
|
|
483
|
+
type: 'finalRevision',
|
|
484
|
+
mode: inputData.mode,
|
|
485
|
+
revicedFootfall: revisedFootfall,
|
|
486
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
487
|
+
count: tempAcc,
|
|
466
488
|
revisedDetail: formattedTaggingData,
|
|
467
|
-
status: '
|
|
489
|
+
status: 'Closed',
|
|
468
490
|
createdByEmail: req?.user?.email,
|
|
469
491
|
createdByUserName: req?.user?.userName,
|
|
470
492
|
createdByRole: req?.user?.role,
|
|
493
|
+
createdAt: new Date(),
|
|
471
494
|
},
|
|
472
495
|
];
|
|
473
496
|
} else {
|
|
474
|
-
|
|
497
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
475
498
|
let revisionArray = [];
|
|
476
|
-
try {
|
|
477
|
-
const clientData = await clientModel.findOne( { clientId: getstoreName?.clientId } ).lean();
|
|
478
|
-
revisionArray = clientData?.footfallDirectoryConfigs?.revision || [];
|
|
479
|
-
} catch ( e ) {
|
|
480
|
-
revisionArray = [];
|
|
481
|
-
}
|
|
482
499
|
|
|
500
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
483
501
|
// Default fallbacks
|
|
484
502
|
let revisionMapping = null;
|
|
485
503
|
let approverMapping = null;
|
|
486
504
|
let tangoReviewMapping = null;
|
|
487
|
-
|
|
488
505
|
// Find out which roles have isChecked true
|
|
489
506
|
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
490
507
|
for ( const r of revisionArray ) {
|
|
@@ -492,25 +509,28 @@ export async function ticketCreation( req, res, next ) {
|
|
|
492
509
|
revisionMapping = {
|
|
493
510
|
type: 'review',
|
|
494
511
|
revicedFootfall: revisedFootfall,
|
|
495
|
-
|
|
512
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
513
|
+
count: tempAcc,
|
|
496
514
|
revisedDetail: formattedTaggingData,
|
|
497
|
-
status: '
|
|
515
|
+
status: 'Open',
|
|
498
516
|
};
|
|
499
517
|
} else if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
500
518
|
approverMapping = {
|
|
501
|
-
type: '
|
|
519
|
+
type: 'approve',
|
|
502
520
|
revicedFootfall: revisedFootfall,
|
|
503
|
-
|
|
521
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
522
|
+
count: tempAcc,
|
|
504
523
|
revisedDetail: formattedTaggingData,
|
|
505
|
-
status: '
|
|
524
|
+
status: 'Open',
|
|
506
525
|
};
|
|
507
526
|
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
508
527
|
tangoReviewMapping = {
|
|
509
|
-
type: '
|
|
528
|
+
type: 'tangoreview',
|
|
510
529
|
revicedFootfall: revisedFootfall,
|
|
511
|
-
|
|
530
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
531
|
+
count: tempAcc,
|
|
512
532
|
revisedDetail: formattedTaggingData,
|
|
513
|
-
status: '
|
|
533
|
+
status: 'Open',
|
|
514
534
|
};
|
|
515
535
|
}
|
|
516
536
|
}
|
|
@@ -518,36 +538,161 @@ export async function ticketCreation( req, res, next ) {
|
|
|
518
538
|
|
|
519
539
|
// Insert appropriate mappingInfo blocks
|
|
520
540
|
if ( revisionMapping ) {
|
|
521
|
-
|
|
541
|
+
// If reviewer and checked
|
|
522
542
|
record.mappingInfo.push( revisionMapping );
|
|
523
543
|
} else if ( approverMapping ) {
|
|
524
|
-
|
|
544
|
+
// If approver and checked
|
|
525
545
|
record.mappingInfo.push( approverMapping );
|
|
526
546
|
} else if ( tangoReviewMapping ) {
|
|
527
|
-
|
|
547
|
+
// If none above, then tangoReview
|
|
528
548
|
record.mappingInfo.push( tangoReviewMapping );
|
|
529
549
|
}
|
|
530
550
|
}
|
|
531
551
|
|
|
552
|
+
let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
|
|
553
|
+
let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
|
|
554
|
+
|
|
555
|
+
if ( checkreview.length > 0 || checkapprove.length > 0 ) {
|
|
556
|
+
let userQuery = [
|
|
557
|
+
{
|
|
558
|
+
$match: {
|
|
559
|
+
clientId: getstoreName.clientId,
|
|
560
|
+
role: 'admin',
|
|
561
|
+
},
|
|
562
|
+
},
|
|
563
|
+
];
|
|
564
|
+
let finduserList = await aggregateUser( userQuery );
|
|
565
|
+
|
|
566
|
+
|
|
567
|
+
for ( let userData of finduserList ) {
|
|
568
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
569
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
570
|
+
let description = `Created on ${createdOn}`;
|
|
571
|
+
let Data = {
|
|
572
|
+
'title': title,
|
|
573
|
+
'body': description,
|
|
574
|
+
'type': 'create',
|
|
575
|
+
'date': record.dateString,
|
|
576
|
+
'storeId': record.storeId,
|
|
577
|
+
'clientId': record.clientId,
|
|
578
|
+
'ticketId': record.ticketId,
|
|
579
|
+
};
|
|
580
|
+
|
|
581
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
582
|
+
|
|
583
|
+
if ( ticketsFeature ) {
|
|
584
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
585
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
586
|
+
const fcmToken = userData.fcmToken;
|
|
587
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
588
|
+
}
|
|
589
|
+
}
|
|
590
|
+
}
|
|
591
|
+
}
|
|
592
|
+
|
|
532
593
|
|
|
533
594
|
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
534
595
|
const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
|
|
535
596
|
if ( insertResult && insertResult.statusCode === 201 ) {
|
|
536
|
-
|
|
537
|
-
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
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
|
|
597
|
+
// After successful ticket creation, update status to "submitted" in revop index for the relevant records
|
|
598
|
+
|
|
599
|
+
|
|
600
|
+
const bulkUpdateBody = taggingImages.map( ( img ) => [
|
|
601
|
+
{ update: { _index: openSearch.revop, _id: img._id } },
|
|
602
|
+
{ doc: { status: 'submitted' } },
|
|
603
|
+
] ).flat();
|
|
604
|
+
|
|
605
|
+
if ( bulkUpdateBody.length > 0 ) {
|
|
606
|
+
await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
|
|
549
607
|
}
|
|
550
608
|
|
|
609
|
+
if ( record.status = 'Closed' ) {
|
|
610
|
+
const query = {
|
|
611
|
+
storeId: inputData?.storeId,
|
|
612
|
+
isVideoStream: true,
|
|
613
|
+
};
|
|
614
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
615
|
+
const revopInfoQuery = {
|
|
616
|
+
size: 10000,
|
|
617
|
+
query: {
|
|
618
|
+
bool: {
|
|
619
|
+
must: [
|
|
620
|
+
{
|
|
621
|
+
term: {
|
|
622
|
+
'storeId.keyword': inputData.storeId,
|
|
623
|
+
},
|
|
624
|
+
},
|
|
625
|
+
{
|
|
626
|
+
term: {
|
|
627
|
+
'dateString': inputData.dateString,
|
|
628
|
+
},
|
|
629
|
+
},
|
|
630
|
+
{
|
|
631
|
+
term: {
|
|
632
|
+
'isParent': false,
|
|
633
|
+
},
|
|
634
|
+
},
|
|
635
|
+
{
|
|
636
|
+
term: {
|
|
637
|
+
isChecked: true,
|
|
638
|
+
},
|
|
639
|
+
},
|
|
640
|
+
],
|
|
641
|
+
},
|
|
642
|
+
},
|
|
643
|
+
_source: [ 'tempId' ],
|
|
644
|
+
|
|
645
|
+
};
|
|
646
|
+
|
|
647
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
648
|
+
|
|
649
|
+
// Get all tempIds from revopInfo response
|
|
650
|
+
const tempIds =
|
|
651
|
+
revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
652
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
653
|
+
const managerEyeZoneQuery = {
|
|
654
|
+
size: 1,
|
|
655
|
+
query: {
|
|
656
|
+
bool: {
|
|
657
|
+
must: [
|
|
658
|
+
{
|
|
659
|
+
term: {
|
|
660
|
+
'storeId.keyword': inputData.storeId,
|
|
661
|
+
},
|
|
662
|
+
},
|
|
663
|
+
{
|
|
664
|
+
term: {
|
|
665
|
+
'storeDate': inputData.dateString,
|
|
666
|
+
},
|
|
667
|
+
},
|
|
668
|
+
],
|
|
669
|
+
},
|
|
670
|
+
},
|
|
671
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
672
|
+
};
|
|
673
|
+
|
|
674
|
+
// Query the managerEyeZone index for the matching document
|
|
675
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
676
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
677
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
678
|
+
const mapping =
|
|
679
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
680
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
681
|
+
{};
|
|
682
|
+
|
|
683
|
+
// If you want to compare or find matching tempIds in the mapping
|
|
684
|
+
// The mapping is { "1": tempId1, ... }, so get values as array of tempIds
|
|
685
|
+
// const managerMappedTempIds = Object.values( mapping );
|
|
686
|
+
|
|
687
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
688
|
+
const temp = [];
|
|
689
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
|
|
690
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
691
|
+
if ( isSendMessge == true ) {
|
|
692
|
+
logger.info( '....1' );
|
|
693
|
+
// return true; // res.sendSuccess( 'Ticket has been updated successfully' );
|
|
694
|
+
} // Example: log or use these tempIds for further logic
|
|
695
|
+
}
|
|
551
696
|
// Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
|
|
552
697
|
|
|
553
698
|
if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
|
|
@@ -557,7 +702,7 @@ export async function ticketCreation( req, res, next ) {
|
|
|
557
702
|
// req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
|
|
558
703
|
const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
|
|
559
704
|
const storeId = inputData.storeId;
|
|
560
|
-
const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
705
|
+
const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
561
706
|
|
|
562
707
|
|
|
563
708
|
const formatDate = ( d ) =>
|
|
@@ -573,14 +718,14 @@ export async function ticketCreation( req, res, next ) {
|
|
|
573
718
|
const startDateObj = new Date( currentDateObj );
|
|
574
719
|
|
|
575
720
|
if ( breachDays === 30 ) {
|
|
576
|
-
|
|
721
|
+
// Consider within this month
|
|
577
722
|
startDateObj.setDate( 1 ); // First day of current month
|
|
578
723
|
} else if ( breachDays === 60 ) {
|
|
579
|
-
|
|
724
|
+
// Consider this month and last month
|
|
580
725
|
startDateObj.setMonth( startDateObj.getMonth() - 1 );
|
|
581
726
|
startDateObj.setDate( 1 ); // First day of last month
|
|
582
727
|
} else {
|
|
583
|
-
|
|
728
|
+
// For other values, calculate months from days
|
|
584
729
|
const breachMonths = Math.ceil( breachDays / 30 );
|
|
585
730
|
startDateObj.setMonth( startDateObj.getMonth() - breachMonths + 1 );
|
|
586
731
|
startDateObj.setDate( 1 );
|
|
@@ -628,21 +773,21 @@ export async function ticketCreation( req, res, next ) {
|
|
|
628
773
|
}
|
|
629
774
|
|
|
630
775
|
if ( breachTicketsCount >= breachCount ) {
|
|
631
|
-
|
|
776
|
+
// Calculate remaining future days in the config period
|
|
632
777
|
const futureDates = [];
|
|
633
778
|
|
|
634
779
|
// Calculate end date of config period
|
|
635
780
|
const configEndDateObj = new Date( currentDateObj );
|
|
636
781
|
if ( breachDays === 30 ) {
|
|
637
|
-
|
|
782
|
+
// End of current month
|
|
638
783
|
configEndDateObj.setMonth( configEndDateObj.getMonth() + 1 );
|
|
639
784
|
configEndDateObj.setDate( 0 ); // Last day of current month
|
|
640
785
|
} else if ( breachDays === 60 ) {
|
|
641
|
-
|
|
786
|
+
// End of next month
|
|
642
787
|
configEndDateObj.setMonth( configEndDateObj.getMonth() + 2 );
|
|
643
788
|
configEndDateObj.setDate( 0 ); // Last day of next month
|
|
644
789
|
} else {
|
|
645
|
-
|
|
790
|
+
// For other values, add the remaining days
|
|
646
791
|
const remainingDays = breachDays - ( Math.floor( ( currentDateObj - startDateObj ) / ( 1000 * 60 * 60 * 24 ) ) );
|
|
647
792
|
configEndDateObj.setDate( configEndDateObj.getDate() + remainingDays );
|
|
648
793
|
}
|
|
@@ -671,14 +816,1048 @@ export async function ticketCreation( req, res, next ) {
|
|
|
671
816
|
}
|
|
672
817
|
}
|
|
673
818
|
}
|
|
819
|
+
const sqsName = sqs.vmsPickleExtention;
|
|
820
|
+
const sqsProduceQueue = {
|
|
821
|
+
QueueUrl: `${sqs.url}${sqsName}`,
|
|
822
|
+
MessageBody: JSON.stringify( {
|
|
823
|
+
store_id: inputData?.storeId,
|
|
824
|
+
store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
|
|
825
|
+
primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
826
|
+
time: new Date(),
|
|
827
|
+
} ),
|
|
828
|
+
MessageGroupId: 'revops-pickle',
|
|
829
|
+
MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
830
|
+
};
|
|
831
|
+
const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
|
|
832
|
+
|
|
833
|
+
if ( sqsQueue.statusCode ) {
|
|
834
|
+
logger.error( {
|
|
835
|
+
error: `${sqsQueue}`,
|
|
836
|
+
type: 'SQS_NOT_SEND_ERROR',
|
|
837
|
+
} );
|
|
838
|
+
}
|
|
839
|
+
|
|
674
840
|
|
|
675
841
|
return res.sendSuccess( 'Ticket raised successfully' );
|
|
676
842
|
}
|
|
677
843
|
} catch ( error ) {
|
|
678
844
|
const err = error.message || 'Internal Server Error';
|
|
679
|
-
logger.error( { error:
|
|
845
|
+
logger.error( { error: error, funtion: 'ticketCreation' } );
|
|
680
846
|
return res.sendError( err, 500 );
|
|
681
847
|
}
|
|
682
848
|
}
|
|
683
849
|
|
|
850
|
+
export async function ticketReview( req, res, next ) {
|
|
851
|
+
try {
|
|
852
|
+
const inputData = req.body;
|
|
853
|
+
if ( inputData?.type !== 'review' ) {
|
|
854
|
+
return next();
|
|
855
|
+
}
|
|
856
|
+
// check the createtion permission from the user permission
|
|
857
|
+
const userInfo = req?.user;
|
|
858
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
859
|
+
if ( !ticketsFeature ) {
|
|
860
|
+
return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
|
|
861
|
+
}
|
|
862
|
+
|
|
863
|
+
// get store info by the storeId into mongo db
|
|
864
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
865
|
+
|
|
866
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
867
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
868
|
+
}
|
|
869
|
+
|
|
870
|
+
// get the footfall count from opensearch
|
|
871
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
872
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
873
|
+
const getQuery = {
|
|
874
|
+
query: {
|
|
875
|
+
terms: {
|
|
876
|
+
_id: [ dateString ],
|
|
877
|
+
},
|
|
878
|
+
},
|
|
879
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
880
|
+
sort: [
|
|
881
|
+
{
|
|
882
|
+
date_iso: {
|
|
883
|
+
order: 'desc',
|
|
884
|
+
},
|
|
885
|
+
},
|
|
886
|
+
],
|
|
887
|
+
};
|
|
888
|
+
|
|
889
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
890
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
891
|
+
logger.info( { hits } );
|
|
892
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
893
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
894
|
+
}
|
|
895
|
+
|
|
896
|
+
// get category details from the client level configuration
|
|
897
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
898
|
+
if ( !getConfig || getConfig == null ) {
|
|
899
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
903
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
904
|
+
// Initialize count object from taggingLimitation
|
|
905
|
+
const tempAcc = [];
|
|
906
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
907
|
+
if ( item?.type ) {
|
|
908
|
+
// Convert type to camelCase with "Count" suffix
|
|
909
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
910
|
+
const typeLower = item.type.toLowerCase();
|
|
911
|
+
let key;
|
|
912
|
+
if ( typeLower === 'housekeeping' ) {
|
|
913
|
+
key = 'houseKeepingCount';
|
|
914
|
+
} else {
|
|
915
|
+
// Convert first letter to lowercase and append "Count"
|
|
916
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
917
|
+
}
|
|
918
|
+
|
|
919
|
+
|
|
920
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
921
|
+
tempAcc.push( {
|
|
922
|
+
name: item.name,
|
|
923
|
+
value: 0,
|
|
924
|
+
key: key,
|
|
925
|
+
type: item.type,
|
|
926
|
+
} );
|
|
927
|
+
|
|
928
|
+
|
|
929
|
+
return acc;
|
|
930
|
+
}
|
|
931
|
+
}, {} ) || {};
|
|
932
|
+
|
|
933
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
934
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
935
|
+
const revopQuery = {
|
|
936
|
+
size: 0,
|
|
937
|
+
query: {
|
|
938
|
+
bool: {
|
|
939
|
+
must: [
|
|
940
|
+
{
|
|
941
|
+
term: {
|
|
942
|
+
'storeId.keyword': inputData.storeId,
|
|
943
|
+
},
|
|
944
|
+
},
|
|
945
|
+
{
|
|
946
|
+
term: {
|
|
947
|
+
'dateString': inputData.dateString,
|
|
948
|
+
},
|
|
949
|
+
},
|
|
950
|
+
{
|
|
951
|
+
term: {
|
|
952
|
+
'isParent': false,
|
|
953
|
+
},
|
|
954
|
+
},
|
|
955
|
+
{
|
|
956
|
+
term: {
|
|
957
|
+
isChecked: true,
|
|
958
|
+
},
|
|
959
|
+
},
|
|
960
|
+
],
|
|
961
|
+
},
|
|
962
|
+
},
|
|
963
|
+
aggs: {
|
|
964
|
+
type_counts: {
|
|
965
|
+
terms: {
|
|
966
|
+
field: 'revopsType.keyword',
|
|
967
|
+
size: 100,
|
|
968
|
+
},
|
|
969
|
+
},
|
|
970
|
+
},
|
|
971
|
+
};
|
|
972
|
+
|
|
973
|
+
|
|
974
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
975
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
976
|
+
|
|
977
|
+
// Map OpenSearch revopsType values to count object keys
|
|
978
|
+
buckets.forEach( ( bucket ) => {
|
|
979
|
+
const revopsType = bucket.key;
|
|
980
|
+
const count = bucket.doc_count || 0;
|
|
981
|
+
|
|
982
|
+
|
|
983
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
984
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
985
|
+
const accMatch = tempAcc.find(
|
|
986
|
+
( acc ) =>
|
|
987
|
+
acc.type &&
|
|
988
|
+
acc.type === revopsType,
|
|
989
|
+
);
|
|
990
|
+
|
|
991
|
+
if ( accMatch && accMatch.key ) {
|
|
992
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
} );
|
|
996
|
+
}
|
|
997
|
+
|
|
998
|
+
|
|
999
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1000
|
+
|
|
1001
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1002
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1003
|
+
0;
|
|
1004
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1005
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1006
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
1007
|
+
return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1008
|
+
}
|
|
1009
|
+
const taggingData = {
|
|
1010
|
+
size: 10000,
|
|
1011
|
+
query: {
|
|
1012
|
+
bool: {
|
|
1013
|
+
must: [
|
|
1014
|
+
{
|
|
1015
|
+
term: {
|
|
1016
|
+
'storeId.keyword': inputData.storeId,
|
|
1017
|
+
},
|
|
1018
|
+
},
|
|
1019
|
+
{
|
|
1020
|
+
term: {
|
|
1021
|
+
'dateString': inputData.dateString,
|
|
1022
|
+
},
|
|
1023
|
+
},
|
|
1024
|
+
],
|
|
1025
|
+
},
|
|
1026
|
+
},
|
|
1027
|
+
};
|
|
1028
|
+
|
|
1029
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1030
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1031
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1032
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1033
|
+
}
|
|
1034
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
684
1035
|
|
|
1036
|
+
const getTicket = {
|
|
1037
|
+
size: 10000,
|
|
1038
|
+
query: {
|
|
1039
|
+
bool: {
|
|
1040
|
+
must: [
|
|
1041
|
+
{
|
|
1042
|
+
term: {
|
|
1043
|
+
'storeId.keyword': inputData.storeId,
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
{
|
|
1047
|
+
term: {
|
|
1048
|
+
'dateString': inputData.dateString,
|
|
1049
|
+
},
|
|
1050
|
+
},
|
|
1051
|
+
],
|
|
1052
|
+
},
|
|
1053
|
+
},
|
|
1054
|
+
};
|
|
1055
|
+
|
|
1056
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1057
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1058
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1059
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1060
|
+
}
|
|
1061
|
+
const record = {
|
|
1062
|
+
|
|
1063
|
+
status: 'Reviewer-Closed',
|
|
1064
|
+
revicedFootfall: revisedFootfall,
|
|
1065
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1066
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1067
|
+
createdByEmail: req?.user?.email,
|
|
1068
|
+
createdByUserName: req?.user?.userName,
|
|
1069
|
+
createdByRole: req?.user?.role,
|
|
1070
|
+
|
|
1071
|
+
};
|
|
1072
|
+
|
|
1073
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1074
|
+
const temp = record.mappingInfo
|
|
1075
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1076
|
+
.map( ( item ) => ( {
|
|
1077
|
+
...item,
|
|
1078
|
+
mode: inputData.mode,
|
|
1079
|
+
revicedFootfall: revisedFootfall,
|
|
1080
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1081
|
+
count: tempAcc,
|
|
1082
|
+
revisedDetail: formattedTaggingData,
|
|
1083
|
+
status: 'Closed',
|
|
1084
|
+
createdByEmail: req?.user?.email,
|
|
1085
|
+
createdByUserName: req?.user?.userName,
|
|
1086
|
+
createdByRole: req?.user?.role,
|
|
1087
|
+
createdAt: new Date(),
|
|
1088
|
+
} ) );
|
|
1089
|
+
record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
|
|
1090
|
+
// If no review mapping existed, push a new one
|
|
1091
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1092
|
+
record.mappingInfo.push( {
|
|
1093
|
+
type: 'review',
|
|
1094
|
+
mode: inputData.mode,
|
|
1095
|
+
revicedFootfall: revisedFootfall,
|
|
1096
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1097
|
+
count: tempAcc,
|
|
1098
|
+
revisedDetail: formattedTaggingData,
|
|
1099
|
+
status: 'Closed',
|
|
1100
|
+
createdByEmail: req?.user?.email,
|
|
1101
|
+
createdByUserName: req?.user?.userName,
|
|
1102
|
+
createdByRole: req?.user?.role,
|
|
1103
|
+
createdAt: new Date(),
|
|
1104
|
+
} );
|
|
1105
|
+
}
|
|
1106
|
+
}
|
|
1107
|
+
|
|
1108
|
+
|
|
1109
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1110
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
1111
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1112
|
+
|
|
1113
|
+
|
|
1114
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1115
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1116
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1117
|
+
|
|
1118
|
+
|
|
1119
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1120
|
+
if (
|
|
1121
|
+
isAutoCloseEnable === true &&
|
|
1122
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1123
|
+
) {
|
|
1124
|
+
record.status = 'Reviewer-Closed';
|
|
1125
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1126
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1127
|
+
const temp = record.mappingInfo
|
|
1128
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1129
|
+
.map( ( item ) => ( {
|
|
1130
|
+
...item,
|
|
1131
|
+
mode: inputData.mode,
|
|
1132
|
+
revicedFootfall: revisedFootfall,
|
|
1133
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1134
|
+
count: tempAcc,
|
|
1135
|
+
revisedDetail: formattedTaggingData,
|
|
1136
|
+
status: 'Closed',
|
|
1137
|
+
createdByEmail: req?.user?.email,
|
|
1138
|
+
createdByUserName: req?.user?.userName,
|
|
1139
|
+
createdByRole: req?.user?.role,
|
|
1140
|
+
} ) );
|
|
1141
|
+
|
|
1142
|
+
const temp2 = record.mappingInfo
|
|
1143
|
+
.filter( ( item ) => item.type === 'tagging' )
|
|
1144
|
+
.map( ( item ) => ( {
|
|
1145
|
+
...item,
|
|
1146
|
+
mode: inputData.mode,
|
|
1147
|
+
revicedFootfall: revisedFootfall,
|
|
1148
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1149
|
+
count: tempAcc,
|
|
1150
|
+
revisedDetail: formattedTaggingData,
|
|
1151
|
+
status: 'Closed',
|
|
1152
|
+
createdByEmail: req?.user?.email,
|
|
1153
|
+
createdByUserName: req?.user?.userName,
|
|
1154
|
+
createdByRole: req?.user?.role,
|
|
1155
|
+
} ) );
|
|
1156
|
+
record.mappingInfo = [ ...temp2, ...temp ];
|
|
1157
|
+
// If no review mapping existed, push a new one
|
|
1158
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1159
|
+
record.mappingInfo.push( {
|
|
1160
|
+
type: 'review',
|
|
1161
|
+
mode: inputData.mode,
|
|
1162
|
+
revicedFootfall: revisedFootfall,
|
|
1163
|
+
revicedPerc: 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
|
+
}
|
|
1173
|
+
record.mappingInfo.push(
|
|
1174
|
+
{
|
|
1175
|
+
type: 'finalRevision',
|
|
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
|
+
createdAt: new Date(),
|
|
1186
|
+
},
|
|
1187
|
+
);
|
|
1188
|
+
} else {
|
|
1189
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1190
|
+
let revisionArray = [];
|
|
1191
|
+
|
|
1192
|
+
|
|
1193
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
1194
|
+
|
|
1195
|
+
|
|
1196
|
+
// Default fallbacks
|
|
1197
|
+
let revisionMapping = null;
|
|
1198
|
+
let approverMapping = null;
|
|
1199
|
+
let tangoReviewMapping = null;
|
|
1200
|
+
|
|
1201
|
+
// Find out which roles have isChecked true
|
|
1202
|
+
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
1203
|
+
for ( const r of revisionArray ) {
|
|
1204
|
+
if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
1205
|
+
approverMapping = {
|
|
1206
|
+
type: 'approve',
|
|
1207
|
+
revicedFootfall: revisedFootfall,
|
|
1208
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1209
|
+
count: tempAcc,
|
|
1210
|
+
revisedDetail: formattedTaggingData,
|
|
1211
|
+
status: 'Open',
|
|
1212
|
+
};
|
|
1213
|
+
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1214
|
+
tangoReviewMapping = {
|
|
1215
|
+
type: 'tangoreview',
|
|
1216
|
+
revicedFootfall: revisedFootfall,
|
|
1217
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1218
|
+
count: tempAcc,
|
|
1219
|
+
revisedDetail: formattedTaggingData,
|
|
1220
|
+
status: 'Open',
|
|
1221
|
+
};
|
|
1222
|
+
}
|
|
1223
|
+
}
|
|
1224
|
+
}
|
|
1225
|
+
|
|
1226
|
+
// Insert appropriate mappingInfo blocks
|
|
1227
|
+
if ( revisionMapping ) {
|
|
1228
|
+
// If reviewer and checked
|
|
1229
|
+
record.mappingInfo.push( revisionMapping );
|
|
1230
|
+
} else if ( approverMapping ) {
|
|
1231
|
+
// If approver and checked
|
|
1232
|
+
record.mappingInfo.push( approverMapping );
|
|
1233
|
+
} else if ( tangoReviewMapping ) {
|
|
1234
|
+
// If none above, then tangoReview
|
|
1235
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
|
|
1240
|
+
|
|
1241
|
+
|
|
1242
|
+
if ( checkreview.length > 0 ) {
|
|
1243
|
+
let userQuery = [
|
|
1244
|
+
{
|
|
1245
|
+
$match: {
|
|
1246
|
+
clientId: getstoreName.clientId,
|
|
1247
|
+
role: 'admin',
|
|
1248
|
+
},
|
|
1249
|
+
},
|
|
1250
|
+
];
|
|
1251
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1252
|
+
console.log( '🚀 ~ ticketReview ~ finduserList:', finduserList );
|
|
1253
|
+
|
|
1254
|
+
// return;
|
|
1255
|
+
for ( let userData of finduserList ) {
|
|
1256
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1257
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1258
|
+
let description = `Created on ${createdOn}`;
|
|
1259
|
+
|
|
1260
|
+
let Data = {
|
|
1261
|
+
'title': title,
|
|
1262
|
+
'body': description,
|
|
1263
|
+
'type': 'review',
|
|
1264
|
+
'date': record.dateString,
|
|
1265
|
+
'storeId': record.storeId,
|
|
1266
|
+
'clientId': record.clientId,
|
|
1267
|
+
'ticketId': record.ticketId,
|
|
1268
|
+
};
|
|
1269
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1270
|
+
|
|
1271
|
+
if ( ticketsFeature ) {
|
|
1272
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1273
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1274
|
+
const fcmToken = userData.fcmToken;
|
|
1275
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1276
|
+
}
|
|
1277
|
+
}
|
|
1278
|
+
}
|
|
1279
|
+
}
|
|
1280
|
+
|
|
1281
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1282
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1283
|
+
|
|
1284
|
+
if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
|
|
1285
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1286
|
+
} else {
|
|
1287
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1288
|
+
}
|
|
1289
|
+
} catch ( error ) {
|
|
1290
|
+
const err = error.message || 'Internal Server Error';
|
|
1291
|
+
logger.error( { error: err, funtion: 'ticketreview' } );
|
|
1292
|
+
return res.sendError( err, 500 );
|
|
1293
|
+
}
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
export async function ticketApprove( req, res, next ) {
|
|
1297
|
+
try {
|
|
1298
|
+
const inputData = req.body;
|
|
1299
|
+
if ( inputData?.type !== 'approve' ) {
|
|
1300
|
+
return next();
|
|
1301
|
+
}
|
|
1302
|
+
// check the createtion permission from the user permission
|
|
1303
|
+
const userInfo = req?.user;
|
|
1304
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1305
|
+
if ( !ticketsFeature ) {
|
|
1306
|
+
return res.sendError( 'Forbidden to Approve this Ticket', 403 );
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
// get store info by the storeId into mongo db
|
|
1310
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
1311
|
+
|
|
1312
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
1313
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
// get the footfall count from opensearch
|
|
1317
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1318
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
1319
|
+
const getQuery = {
|
|
1320
|
+
query: {
|
|
1321
|
+
terms: {
|
|
1322
|
+
_id: [ dateString ],
|
|
1323
|
+
},
|
|
1324
|
+
},
|
|
1325
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
1326
|
+
sort: [
|
|
1327
|
+
{
|
|
1328
|
+
date_iso: {
|
|
1329
|
+
order: 'desc',
|
|
1330
|
+
},
|
|
1331
|
+
},
|
|
1332
|
+
],
|
|
1333
|
+
};
|
|
1334
|
+
|
|
1335
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
1336
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
1337
|
+
logger.info( { hits } );
|
|
1338
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
1339
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
1340
|
+
}
|
|
1341
|
+
|
|
1342
|
+
// get category details from the client level configuration
|
|
1343
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
1344
|
+
if ( !getConfig || getConfig == null ) {
|
|
1345
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
1346
|
+
}
|
|
1347
|
+
|
|
1348
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
1349
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
1350
|
+
// Initialize count object from taggingLimitation
|
|
1351
|
+
const tempAcc = [];
|
|
1352
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
1353
|
+
if ( item?.type ) {
|
|
1354
|
+
// Convert type to camelCase with "Count" suffix
|
|
1355
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
1356
|
+
const typeLower = item.type.toLowerCase();
|
|
1357
|
+
let key;
|
|
1358
|
+
if ( typeLower === 'housekeeping' ) {
|
|
1359
|
+
key = 'houseKeepingCount';
|
|
1360
|
+
} else {
|
|
1361
|
+
// Convert first letter to lowercase and append "Count"
|
|
1362
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
|
|
1366
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
1367
|
+
tempAcc.push( {
|
|
1368
|
+
name: item.name,
|
|
1369
|
+
value: 0,
|
|
1370
|
+
key: key,
|
|
1371
|
+
type: item.type,
|
|
1372
|
+
} );
|
|
1373
|
+
|
|
1374
|
+
|
|
1375
|
+
return acc;
|
|
1376
|
+
}
|
|
1377
|
+
}, {} ) || {};
|
|
1378
|
+
|
|
1379
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
1380
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
1381
|
+
const revopQuery = {
|
|
1382
|
+
size: 0,
|
|
1383
|
+
query: {
|
|
1384
|
+
bool: {
|
|
1385
|
+
must: [
|
|
1386
|
+
{
|
|
1387
|
+
term: {
|
|
1388
|
+
'storeId.keyword': inputData.storeId,
|
|
1389
|
+
},
|
|
1390
|
+
},
|
|
1391
|
+
{
|
|
1392
|
+
term: {
|
|
1393
|
+
'dateString': inputData.dateString,
|
|
1394
|
+
},
|
|
1395
|
+
},
|
|
1396
|
+
],
|
|
1397
|
+
},
|
|
1398
|
+
},
|
|
1399
|
+
aggs: {
|
|
1400
|
+
type_counts: {
|
|
1401
|
+
terms: {
|
|
1402
|
+
field: 'revopsType.keyword',
|
|
1403
|
+
size: 100,
|
|
1404
|
+
},
|
|
1405
|
+
},
|
|
1406
|
+
},
|
|
1407
|
+
};
|
|
1408
|
+
|
|
1409
|
+
|
|
1410
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
1411
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
1412
|
+
|
|
1413
|
+
// Map OpenSearch revopsType values to count object keys
|
|
1414
|
+
buckets.forEach( ( bucket ) => {
|
|
1415
|
+
const revopsType = bucket.key;
|
|
1416
|
+
const count = bucket.doc_count || 0;
|
|
1417
|
+
|
|
1418
|
+
|
|
1419
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
1420
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
1421
|
+
const accMatch = tempAcc.find(
|
|
1422
|
+
( acc ) =>
|
|
1423
|
+
acc.type &&
|
|
1424
|
+
acc.type === revopsType,
|
|
1425
|
+
);
|
|
1426
|
+
|
|
1427
|
+
if ( accMatch && accMatch.key ) {
|
|
1428
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
1429
|
+
}
|
|
1430
|
+
}
|
|
1431
|
+
} );
|
|
1432
|
+
}
|
|
1433
|
+
|
|
1434
|
+
|
|
1435
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1436
|
+
|
|
1437
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1438
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1439
|
+
0;
|
|
1440
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1441
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1442
|
+
logger.info( { footfallCount, revisedFootfall } );
|
|
1443
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
1444
|
+
return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1445
|
+
}
|
|
1446
|
+
|
|
1447
|
+
const taggingData = {
|
|
1448
|
+
size: 10000,
|
|
1449
|
+
query: {
|
|
1450
|
+
bool: {
|
|
1451
|
+
must: [
|
|
1452
|
+
{
|
|
1453
|
+
term: {
|
|
1454
|
+
'storeId.keyword': inputData.storeId,
|
|
1455
|
+
},
|
|
1456
|
+
},
|
|
1457
|
+
{
|
|
1458
|
+
term: {
|
|
1459
|
+
'dateString': inputData.dateString,
|
|
1460
|
+
},
|
|
1461
|
+
},
|
|
1462
|
+
{
|
|
1463
|
+
term: {
|
|
1464
|
+
'isParent': false,
|
|
1465
|
+
},
|
|
1466
|
+
},
|
|
1467
|
+
{
|
|
1468
|
+
term: {
|
|
1469
|
+
isChecked: true,
|
|
1470
|
+
},
|
|
1471
|
+
},
|
|
1472
|
+
],
|
|
1473
|
+
},
|
|
1474
|
+
},
|
|
1475
|
+
};
|
|
1476
|
+
|
|
1477
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1478
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1479
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1480
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1481
|
+
}
|
|
1482
|
+
logger.info( { taggingImages } );
|
|
1483
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
1484
|
+
|
|
1485
|
+
const getTicket = {
|
|
1486
|
+
size: 10000,
|
|
1487
|
+
query: {
|
|
1488
|
+
bool: {
|
|
1489
|
+
must: [
|
|
1490
|
+
{
|
|
1491
|
+
term: {
|
|
1492
|
+
'storeId.keyword': inputData.storeId,
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
{
|
|
1496
|
+
term: {
|
|
1497
|
+
'dateString': inputData.dateString,
|
|
1498
|
+
},
|
|
1499
|
+
},
|
|
1500
|
+
],
|
|
1501
|
+
},
|
|
1502
|
+
},
|
|
1503
|
+
};
|
|
1504
|
+
|
|
1505
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1506
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1507
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1508
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1509
|
+
}
|
|
1510
|
+
logger.info( { ticketData, getFootfallticketData } );
|
|
1511
|
+
const record = {
|
|
1512
|
+
|
|
1513
|
+
status: 'Approver-Closed',
|
|
1514
|
+
revicedFootfall: revisedFootfall,
|
|
1515
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1516
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1517
|
+
createdByEmail: req?.user?.email,
|
|
1518
|
+
createdByUserName: req?.user?.userName,
|
|
1519
|
+
createdByRole: req?.user?.role,
|
|
1520
|
+
|
|
1521
|
+
};
|
|
1522
|
+
|
|
1523
|
+
|
|
1524
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1525
|
+
let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
|
|
1526
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1527
|
+
|
|
1528
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1529
|
+
logger.info( { getNumber } );
|
|
1530
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1531
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1532
|
+
const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
|
|
1533
|
+
const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
|
|
1534
|
+
logger.info( { tangoReview, revised } );
|
|
1535
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1536
|
+
if (
|
|
1537
|
+
isAutoCloseEnable === true &&
|
|
1538
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1539
|
+
) {
|
|
1540
|
+
record.status = 'Approver-Closed';
|
|
1541
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1542
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1543
|
+
const temp = record.mappingInfo
|
|
1544
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1545
|
+
.map( ( item ) => ( {
|
|
1546
|
+
...item,
|
|
1547
|
+
|
|
1548
|
+
mode: inputData.mode,
|
|
1549
|
+
revicedFootfall: revisedFootfall,
|
|
1550
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1551
|
+
count: tempAcc,
|
|
1552
|
+
revisedDetail: formattedTaggingData,
|
|
1553
|
+
status: 'Closed',
|
|
1554
|
+
createdByEmail: req?.user?.email,
|
|
1555
|
+
createdByUserName: req?.user?.userName,
|
|
1556
|
+
createdByRole: req?.user?.role,
|
|
1557
|
+
} ) );
|
|
1558
|
+
|
|
1559
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1560
|
+
...temp ];
|
|
1561
|
+
// If updating the mapping config to mark [i].status as 'Closed'
|
|
1562
|
+
// Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
|
|
1563
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1564
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1565
|
+
return {
|
|
1566
|
+
...item,
|
|
1567
|
+
status: 'Closed',
|
|
1568
|
+
};
|
|
1569
|
+
} );
|
|
1570
|
+
}
|
|
1571
|
+
// If no review mapping existed, push a new one
|
|
1572
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1573
|
+
record.mappingInfo.push( {
|
|
1574
|
+
type: 'approve',
|
|
1575
|
+
mode: inputData.mode,
|
|
1576
|
+
revicedFootfall: revisedFootfall,
|
|
1577
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1578
|
+
count: tempAcc,
|
|
1579
|
+
revisedDetail: formattedTaggingData,
|
|
1580
|
+
status: 'Closed',
|
|
1581
|
+
createdByEmail: req?.user?.email,
|
|
1582
|
+
createdByUserName: req?.user?.userName,
|
|
1583
|
+
createdByRole: req?.user?.role,
|
|
1584
|
+
} );
|
|
1585
|
+
}
|
|
1586
|
+
}
|
|
1587
|
+
record.mappingInfo.push(
|
|
1588
|
+
{
|
|
1589
|
+
type: 'finalRevision',
|
|
1590
|
+
mode: inputData.mode,
|
|
1591
|
+
revicedFootfall: revisedFootfall,
|
|
1592
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1593
|
+
count: tempAcc,
|
|
1594
|
+
revisedDetail: formattedTaggingData,
|
|
1595
|
+
status: 'Closed',
|
|
1596
|
+
createdByEmail: req?.user?.email,
|
|
1597
|
+
createdByUserName: req?.user?.userName,
|
|
1598
|
+
createdByRole: req?.user?.role,
|
|
1599
|
+
createdAt: new Date(),
|
|
1600
|
+
},
|
|
1601
|
+
);
|
|
1602
|
+
} else if ( revised < tangoReview ) {
|
|
1603
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1604
|
+
|
|
1605
|
+
// Default fallbacks
|
|
1606
|
+
|
|
1607
|
+
let approverMapping = null;
|
|
1608
|
+
let tangoReviewMapping = null;
|
|
1609
|
+
|
|
1610
|
+
record.status = 'Approver-Closed';
|
|
1611
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1612
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1613
|
+
const temp = record.mappingInfo
|
|
1614
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1615
|
+
.map( ( item ) => ( {
|
|
1616
|
+
...item,
|
|
1617
|
+
mode: inputData.mode,
|
|
1618
|
+
revicedFootfall: revisedFootfall,
|
|
1619
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1620
|
+
count: tempAcc,
|
|
1621
|
+
revisedDetail: formattedTaggingData,
|
|
1622
|
+
status: 'Closed',
|
|
1623
|
+
createdByEmail: req?.user?.email,
|
|
1624
|
+
createdByUserName: req?.user?.userName,
|
|
1625
|
+
createdByRole: req?.user?.role,
|
|
1626
|
+
} ) );
|
|
1627
|
+
|
|
1628
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1629
|
+
...temp ];
|
|
1630
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1631
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1632
|
+
return {
|
|
1633
|
+
...item,
|
|
1634
|
+
status: 'Closed',
|
|
1635
|
+
};
|
|
1636
|
+
} );
|
|
1637
|
+
}
|
|
1638
|
+
// If no review mapping existed, push a new one
|
|
1639
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1640
|
+
record.mappingInfo.push( {
|
|
1641
|
+
type: 'approve',
|
|
1642
|
+
mode: inputData.mode,
|
|
1643
|
+
revicedFootfall: revisedFootfall,
|
|
1644
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1645
|
+
count: tempAcc,
|
|
1646
|
+
revisedDetail: formattedTaggingData,
|
|
1647
|
+
status: 'Closed',
|
|
1648
|
+
createdByEmail: req?.user?.email,
|
|
1649
|
+
createdByUserName: req?.user?.userName,
|
|
1650
|
+
createdByRole: req?.user?.role,
|
|
1651
|
+
} );
|
|
1652
|
+
}
|
|
1653
|
+
}
|
|
1654
|
+
|
|
1655
|
+
// Find out which roles have isChecked true
|
|
1656
|
+
|
|
1657
|
+
// for ( const r of revisionArray ) {
|
|
1658
|
+
// if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1659
|
+
tangoReviewMapping = {
|
|
1660
|
+
type: 'tangoreview',
|
|
1661
|
+
revicedFootfall: revisedFootfall,
|
|
1662
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1663
|
+
count: tempAcc,
|
|
1664
|
+
revisedDetail: formattedTaggingData,
|
|
1665
|
+
status: 'Open',
|
|
1666
|
+
};
|
|
1667
|
+
// }
|
|
1668
|
+
// }
|
|
1669
|
+
|
|
1670
|
+
|
|
1671
|
+
if ( approverMapping ) {
|
|
1672
|
+
// If approver and checked
|
|
1673
|
+
record.mappingInfo.push( approverMapping );
|
|
1674
|
+
} else if ( tangoReviewMapping ) {
|
|
1675
|
+
// If none above, then tangoReview
|
|
1676
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1677
|
+
}
|
|
1678
|
+
} else {
|
|
1679
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1680
|
+
const temp = record.mappingInfo
|
|
1681
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1682
|
+
.map( ( item ) => ( {
|
|
1683
|
+
...item,
|
|
1684
|
+
mode: inputData.mode,
|
|
1685
|
+
revicedFootfall: revisedFootfall,
|
|
1686
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1687
|
+
count: tempAcc,
|
|
1688
|
+
revisedDetail: formattedTaggingData,
|
|
1689
|
+
status: 'Closed',
|
|
1690
|
+
createdByEmail: req?.user?.email,
|
|
1691
|
+
createdByUserName: req?.user?.userName,
|
|
1692
|
+
createdByRole: req?.user?.role,
|
|
1693
|
+
} ) );
|
|
1694
|
+
|
|
1695
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1696
|
+
...temp ];
|
|
1697
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1698
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1699
|
+
return {
|
|
1700
|
+
...item,
|
|
1701
|
+
status: 'Closed',
|
|
1702
|
+
};
|
|
1703
|
+
} );
|
|
1704
|
+
}
|
|
1705
|
+
// If no review mapping existed, push a new one
|
|
1706
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1707
|
+
record.mappingInfo.push( {
|
|
1708
|
+
type: 'approve',
|
|
1709
|
+
mode: inputData.mode,
|
|
1710
|
+
revicedFootfall: revisedFootfall,
|
|
1711
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1712
|
+
count: tempAcc,
|
|
1713
|
+
revisedDetail: formattedTaggingData,
|
|
1714
|
+
status: 'Closed',
|
|
1715
|
+
createdByEmail: req?.user?.email,
|
|
1716
|
+
createdByUserName: req?.user?.userName,
|
|
1717
|
+
createdByRole: req?.user?.role,
|
|
1718
|
+
} );
|
|
1719
|
+
}
|
|
1720
|
+
}
|
|
1721
|
+
record.mappingInfo.push(
|
|
1722
|
+
{
|
|
1723
|
+
type: 'finalRevision',
|
|
1724
|
+
mode: inputData.mode,
|
|
1725
|
+
revicedFootfall: revisedFootfall,
|
|
1726
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1727
|
+
count: tempAcc,
|
|
1728
|
+
revisedDetail: formattedTaggingData,
|
|
1729
|
+
status: 'Closed',
|
|
1730
|
+
createdByEmail: req?.user?.email,
|
|
1731
|
+
createdByUserName: req?.user?.userName,
|
|
1732
|
+
createdByRole: req?.user?.role,
|
|
1733
|
+
createdAt: new Date(),
|
|
1734
|
+
},
|
|
1735
|
+
);
|
|
1736
|
+
}
|
|
1737
|
+
console.log( req.body, getConfig.footfallDirectoryConfigs.revision );
|
|
1738
|
+
let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
|
|
1739
|
+
|
|
1740
|
+
|
|
1741
|
+
if ( checkapprove.length > 0 ) {
|
|
1742
|
+
let userQuery = [
|
|
1743
|
+
{
|
|
1744
|
+
$match: {
|
|
1745
|
+
clientId: getstoreName.clientId,
|
|
1746
|
+
role: 'admin',
|
|
1747
|
+
},
|
|
1748
|
+
},
|
|
1749
|
+
];
|
|
1750
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1751
|
+
console.log( '🚀 ~ ticketReview ~ finduserList:', finduserList );
|
|
1752
|
+
|
|
1753
|
+
for ( let userData of finduserList ) {
|
|
1754
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1755
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1756
|
+
let description = `Created on ${createdOn}`;
|
|
1757
|
+
console.log( '🚀 ~ ticketCreation ~ userData.role:', userData.email );
|
|
1758
|
+
let Data = {
|
|
1759
|
+
'title': title,
|
|
1760
|
+
'body': description,
|
|
1761
|
+
'type': 'approve',
|
|
1762
|
+
'date': record.dateString,
|
|
1763
|
+
'storeId': record.storeId,
|
|
1764
|
+
'clientId': record.clientId,
|
|
1765
|
+
'ticketId': record.ticketId,
|
|
1766
|
+
};
|
|
1767
|
+
|
|
1768
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1769
|
+
console.log( ticketsFeature );
|
|
1770
|
+
if ( ticketsFeature ) {
|
|
1771
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1772
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1773
|
+
const fcmToken = userData.fcmToken;
|
|
1774
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1775
|
+
}
|
|
1776
|
+
}
|
|
1777
|
+
}
|
|
1778
|
+
}
|
|
1779
|
+
|
|
1780
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1781
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1782
|
+
|
|
1783
|
+
logger.info( { insertResult, record, id } );
|
|
1784
|
+
if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
|
|
1785
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1786
|
+
} else {
|
|
1787
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1788
|
+
}
|
|
1789
|
+
} catch ( error ) {
|
|
1790
|
+
const err = error.message || 'Internal Server Error';
|
|
1791
|
+
logger.error( { error: err, funtion: 'ticketCreation' } );
|
|
1792
|
+
return res.sendError( err, 500 );
|
|
1793
|
+
}
|
|
1794
|
+
}
|
|
1795
|
+
|
|
1796
|
+
|
|
1797
|
+
export async function getAssinedStore( user, storeId ) {
|
|
1798
|
+
if ( user && user.userType === 'client' && user.role !== 'superadmin' ) {
|
|
1799
|
+
let storeIds = new Set( user.assignedStores?.map( ( store ) => store.storeId ) );
|
|
1800
|
+
|
|
1801
|
+
// Fetch clusters and teams in parallel
|
|
1802
|
+
const [ clustersList, teamsList ] = await Promise.all( [
|
|
1803
|
+
findcluster( { clientId: user.clientId, Teamlead: { $elemMatch: { email: user.email } } } ),
|
|
1804
|
+
findteams( { clientId: user.clientId, Teamlead: { $elemMatch: { email: user.email } } } ),
|
|
1805
|
+
] );
|
|
1806
|
+
|
|
1807
|
+
// Process clusters
|
|
1808
|
+
if ( clustersList.length > 0 ) {
|
|
1809
|
+
for ( let cluster of clustersList ) {
|
|
1810
|
+
cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
1811
|
+
}
|
|
1812
|
+
}
|
|
1813
|
+
|
|
1814
|
+
// Process teams
|
|
1815
|
+
if ( teamsList.length > 0 ) {
|
|
1816
|
+
for ( let team of teamsList ) {
|
|
1817
|
+
for ( let user of team.users ) {
|
|
1818
|
+
let findUser = await findOneUser( { _id: user.userId } );
|
|
1819
|
+
if ( findUser && findUser.assignedStores?.length > 0 ) {
|
|
1820
|
+
findUser.assignedStores.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
1821
|
+
}
|
|
1822
|
+
|
|
1823
|
+
// Fetch clusters for the user
|
|
1824
|
+
let userClustersList = await findcluster( { clientId: user.clientId, Teamlead: { $elemMatch: { email: findUser.email } } } );
|
|
1825
|
+
if ( userClustersList.length > 0 ) {
|
|
1826
|
+
for ( let cluster of userClustersList ) {
|
|
1827
|
+
cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
1828
|
+
}
|
|
1829
|
+
}
|
|
1830
|
+
}
|
|
1831
|
+
}
|
|
1832
|
+
}
|
|
1833
|
+
let TeamMember = await findteams( { clientId: user.clientId, users: { $elemMatch: { email: user.email } } } );
|
|
1834
|
+
if ( TeamMember && TeamMember.length > 0 ) {
|
|
1835
|
+
for ( let team of TeamMember ) {
|
|
1836
|
+
let clusterList = await findcluster( { clientId: user.clientId, teams: { $elemMatch: { name: team.teamName } } } );
|
|
1837
|
+
if ( clusterList.length > 0 ) {
|
|
1838
|
+
for ( let cluster of clusterList ) {
|
|
1839
|
+
cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
1840
|
+
}
|
|
1841
|
+
}
|
|
1842
|
+
}
|
|
1843
|
+
}
|
|
1844
|
+
let TeamLeader = await findteams( { clientId: user.clientId, Teamlead: { $elemMatch: { email: user.email } } } );
|
|
1845
|
+
if ( TeamLeader && TeamLeader.length > 0 ) {
|
|
1846
|
+
for ( let team of TeamLeader ) {
|
|
1847
|
+
let clusterList = await findcluster( { clientId: user.clientId, teams: { $elemMatch: { name: team.teamName } } } );
|
|
1848
|
+
if ( clusterList.length > 0 ) {
|
|
1849
|
+
for ( let cluster of clusterList ) {
|
|
1850
|
+
cluster.stores.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
1851
|
+
}
|
|
1852
|
+
}
|
|
1853
|
+
}
|
|
1854
|
+
}
|
|
1855
|
+
// Convert Set back to Array if needed
|
|
1856
|
+
let assignedStores = Array.from( storeIds );
|
|
1857
|
+
if ( assignedStores.includes( storeId ) ) {
|
|
1858
|
+
return true;
|
|
1859
|
+
} else {
|
|
1860
|
+
return true;
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
}
|