tango-app-api-infra 3.9.5-vms.7 → 3.9.5-vms.70
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,117 +416,122 @@ 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 ) {
|
|
491
508
|
if ( r.actionType === 'reviewer' && r.isChecked === true ) {
|
|
492
509
|
revisionMapping = {
|
|
493
510
|
type: 'review',
|
|
494
|
-
revicedFootfall: revisedFootfall,
|
|
495
|
-
|
|
511
|
+
// revicedFootfall: revisedFootfall,
|
|
512
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
513
|
+
count: tempAcc,
|
|
496
514
|
revisedDetail: formattedTaggingData,
|
|
497
|
-
status: '
|
|
515
|
+
status: 'Open',
|
|
516
|
+
|
|
498
517
|
};
|
|
499
518
|
} else if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
500
519
|
approverMapping = {
|
|
501
|
-
type: '
|
|
502
|
-
revicedFootfall: revisedFootfall,
|
|
503
|
-
|
|
520
|
+
type: 'approve',
|
|
521
|
+
// revicedFootfall: revisedFootfall,
|
|
522
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
523
|
+
count: tempAcc,
|
|
504
524
|
revisedDetail: formattedTaggingData,
|
|
505
|
-
status: '
|
|
525
|
+
status: 'Open',
|
|
506
526
|
};
|
|
507
527
|
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
508
528
|
tangoReviewMapping = {
|
|
509
|
-
type: '
|
|
510
|
-
revicedFootfall: revisedFootfall,
|
|
511
|
-
|
|
529
|
+
type: 'tangoreview',
|
|
530
|
+
// revicedFootfall: revisedFootfall,
|
|
531
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
532
|
+
count: tempAcc,
|
|
512
533
|
revisedDetail: formattedTaggingData,
|
|
513
|
-
status: '
|
|
534
|
+
status: 'Open',
|
|
514
535
|
};
|
|
515
536
|
}
|
|
516
537
|
}
|
|
@@ -518,36 +539,179 @@ export async function ticketCreation( req, res, next ) {
|
|
|
518
539
|
|
|
519
540
|
// Insert appropriate mappingInfo blocks
|
|
520
541
|
if ( revisionMapping ) {
|
|
521
|
-
|
|
542
|
+
// If reviewer and checked
|
|
522
543
|
record.mappingInfo.push( revisionMapping );
|
|
523
544
|
} else if ( approverMapping ) {
|
|
524
|
-
|
|
545
|
+
// If approver and checked
|
|
525
546
|
record.mappingInfo.push( approverMapping );
|
|
526
547
|
} else if ( tangoReviewMapping ) {
|
|
527
|
-
|
|
548
|
+
// If none above, then tangoReview
|
|
528
549
|
record.mappingInfo.push( tangoReviewMapping );
|
|
529
550
|
}
|
|
530
551
|
}
|
|
531
552
|
|
|
553
|
+
const revision = getConfig.footfallDirectoryConfigs?.revision ?? [];
|
|
554
|
+
|
|
555
|
+
const hasReviewer = revision.some(
|
|
556
|
+
( data ) => data.actionType === 'reviewer' && data.isChecked === true,
|
|
557
|
+
);
|
|
558
|
+
const hasApprover = revision.some(
|
|
559
|
+
( data ) => data.actionType === 'approver' && data.isChecked === true,
|
|
560
|
+
);
|
|
561
|
+
|
|
562
|
+
if ( hasReviewer || hasApprover ) {
|
|
563
|
+
const userQuery = [
|
|
564
|
+
{
|
|
565
|
+
$match: {
|
|
566
|
+
clientId: getstoreName.clientId,
|
|
567
|
+
role: 'admin',
|
|
568
|
+
isActive: true,
|
|
569
|
+
},
|
|
570
|
+
},
|
|
571
|
+
];
|
|
572
|
+
|
|
573
|
+
const finduserList = await aggregateUser( userQuery );
|
|
574
|
+
|
|
575
|
+
|
|
576
|
+
const createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
577
|
+
const title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
578
|
+
const description = `Created on ${createdOn}`;
|
|
579
|
+
|
|
580
|
+
const Data = {
|
|
581
|
+
title,
|
|
582
|
+
body: description,
|
|
583
|
+
type: 'create',
|
|
584
|
+
date: record.dateString,
|
|
585
|
+
storeId: record.storeId,
|
|
586
|
+
clientId: record.clientId,
|
|
587
|
+
ticketId: record.ticketId,
|
|
588
|
+
};
|
|
589
|
+
|
|
590
|
+
await Promise.all(
|
|
591
|
+
( finduserList || [] ).map( async ( userData ) => {
|
|
592
|
+
const ticketsFeature = userData?.rolespermission?.some(
|
|
593
|
+
( f ) =>
|
|
594
|
+
f.featureName === 'FootfallDirectory' &&
|
|
595
|
+
f.modules?.some(
|
|
596
|
+
( m ) =>
|
|
597
|
+
m.name === 'reviewer' && ( m.isAdd === true || m.isEdit === true ),
|
|
598
|
+
),
|
|
599
|
+
);
|
|
600
|
+
|
|
601
|
+
|
|
602
|
+
if ( !ticketsFeature ) return;
|
|
603
|
+
|
|
604
|
+
const notifyUser = await getAssinedStore( userData, req.body.storeId );
|
|
605
|
+
if ( !notifyUser || !userData?.fcmToken ) return;
|
|
606
|
+
|
|
607
|
+
await sendPushNotification( title, description, userData.fcmToken, Data );
|
|
608
|
+
} ),
|
|
609
|
+
);
|
|
610
|
+
}
|
|
611
|
+
|
|
532
612
|
|
|
533
613
|
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
534
614
|
const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
|
|
535
615
|
if ( insertResult && insertResult.statusCode === 201 ) {
|
|
536
616
|
// After successful ticket creation, update status to "submitted" in revop index for the relevant records
|
|
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
|
|
617
|
+
|
|
618
|
+
|
|
619
|
+
const bulkUpdateBody = taggingImages.map( ( img ) => [
|
|
620
|
+
{ update: { _index: openSearch.revop, _id: img._id } },
|
|
621
|
+
{ doc: { status: 'submitted' } },
|
|
622
|
+
] ).flat();
|
|
623
|
+
|
|
624
|
+
if ( bulkUpdateBody.length > 0 ) {
|
|
625
|
+
await bulkUpdate( bulkUpdateBody ); // Optionally use a dedicated bulk helper if available
|
|
549
626
|
}
|
|
550
627
|
|
|
628
|
+
if ( record.status = 'Closed' ) {
|
|
629
|
+
const query = {
|
|
630
|
+
storeId: inputData?.storeId,
|
|
631
|
+
isVideoStream: true,
|
|
632
|
+
};
|
|
633
|
+
const getStoreType = await countDocumnetsCamera( query );
|
|
634
|
+
const revopInfoQuery = {
|
|
635
|
+
size: 10000,
|
|
636
|
+
query: {
|
|
637
|
+
bool: {
|
|
638
|
+
must: [
|
|
639
|
+
{
|
|
640
|
+
term: {
|
|
641
|
+
'storeId.keyword': inputData.storeId,
|
|
642
|
+
},
|
|
643
|
+
},
|
|
644
|
+
{
|
|
645
|
+
term: {
|
|
646
|
+
'dateString': inputData.dateString,
|
|
647
|
+
},
|
|
648
|
+
},
|
|
649
|
+
{
|
|
650
|
+
term: {
|
|
651
|
+
'isParent': false,
|
|
652
|
+
},
|
|
653
|
+
},
|
|
654
|
+
{
|
|
655
|
+
term: {
|
|
656
|
+
isChecked: true,
|
|
657
|
+
},
|
|
658
|
+
},
|
|
659
|
+
],
|
|
660
|
+
},
|
|
661
|
+
},
|
|
662
|
+
_source: [ 'tempId' ],
|
|
663
|
+
|
|
664
|
+
};
|
|
665
|
+
|
|
666
|
+
const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
|
|
667
|
+
|
|
668
|
+
// Get all tempIds from revopInfo response
|
|
669
|
+
const tempIds =
|
|
670
|
+
revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
|
|
671
|
+
// Prepare management eyeZone query based on storeId and dateString
|
|
672
|
+
const managerEyeZoneQuery = {
|
|
673
|
+
size: 1,
|
|
674
|
+
query: {
|
|
675
|
+
bool: {
|
|
676
|
+
must: [
|
|
677
|
+
{
|
|
678
|
+
term: {
|
|
679
|
+
'storeId.keyword': inputData.storeId,
|
|
680
|
+
},
|
|
681
|
+
},
|
|
682
|
+
{
|
|
683
|
+
term: {
|
|
684
|
+
'storeDate': inputData.dateString,
|
|
685
|
+
},
|
|
686
|
+
},
|
|
687
|
+
],
|
|
688
|
+
},
|
|
689
|
+
},
|
|
690
|
+
_source: [ 'originalToTrackerCustomerMapping' ],
|
|
691
|
+
};
|
|
692
|
+
|
|
693
|
+
// Query the managerEyeZone index for the matching document
|
|
694
|
+
const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
|
|
695
|
+
const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
|
|
696
|
+
// Extract originalToTrackerCustomerMapping if it exists
|
|
697
|
+
const mapping =
|
|
698
|
+
managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
|
|
699
|
+
managerEyeZoneHit.originalToTrackerCustomerMapping :
|
|
700
|
+
{};
|
|
701
|
+
|
|
702
|
+
// If you want to compare or find matching tempIds in the mapping
|
|
703
|
+
// The mapping is { "1": tempId1, ... }, so get values as array of tempIds
|
|
704
|
+
// const managerMappedTempIds = Object.values( mapping );
|
|
705
|
+
|
|
706
|
+
// Find tempIds that exist in both revopInfo results and manager mapping
|
|
707
|
+
const temp = [];
|
|
708
|
+
tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
|
|
709
|
+
const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
|
|
710
|
+
if ( isSendMessge == true ) {
|
|
711
|
+
logger.info( '....1' );
|
|
712
|
+
// return true; // res.sendSuccess( 'Ticket has been updated successfully' );
|
|
713
|
+
} // Example: log or use these tempIds for further logic
|
|
714
|
+
}
|
|
551
715
|
// Check if ticketCount exceeds breach limit within config months and revised footfall percentage > config accuracy
|
|
552
716
|
|
|
553
717
|
if ( req.accuracyBreach && req.accuracyBreach.ticketCount && req.accuracyBreach.days && req.accuracyBreach.accuracy ) {
|
|
@@ -557,7 +721,7 @@ export async function ticketCreation( req, res, next ) {
|
|
|
557
721
|
// req.accuracyBreach.accuracy is a string like "95%", so remove "%" and convert to Number
|
|
558
722
|
const breachAccuracy = Number( req.accuracyBreach.accuracy.replace( '%', '' ) );
|
|
559
723
|
const storeId = inputData.storeId;
|
|
560
|
-
const ticketName =inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
724
|
+
const ticketName = inputData.ticketName || 'footfall-directory'; // adjust if ticket name variable differs
|
|
561
725
|
|
|
562
726
|
|
|
563
727
|
const formatDate = ( d ) =>
|
|
@@ -671,14 +835,1064 @@ export async function ticketCreation( req, res, next ) {
|
|
|
671
835
|
}
|
|
672
836
|
}
|
|
673
837
|
}
|
|
838
|
+
const sqsName = sqs.vmsPickleExtention;
|
|
839
|
+
const sqsProduceQueue = {
|
|
840
|
+
QueueUrl: `${sqs.url}${sqsName}`,
|
|
841
|
+
MessageBody: JSON.stringify( {
|
|
842
|
+
store_id: inputData?.storeId,
|
|
843
|
+
store_date: inputData?.dateString?.split( '-' ).reverse().join( '-' ),
|
|
844
|
+
primary: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
845
|
+
time: new Date(),
|
|
846
|
+
} ),
|
|
847
|
+
MessageGroupId: 'revops-pickle',
|
|
848
|
+
MessageDeduplicationId: `${inputData?.storeId}_${inputData?.dateString}_${Date.now()}`,
|
|
849
|
+
};
|
|
850
|
+
const sqsQueue = await sendMessageToFIFOQueue( sqsProduceQueue );
|
|
851
|
+
|
|
852
|
+
if ( sqsQueue.statusCode ) {
|
|
853
|
+
logger.error( {
|
|
854
|
+
error: `${sqsQueue}`,
|
|
855
|
+
type: 'SQS_NOT_SEND_ERROR',
|
|
856
|
+
} );
|
|
857
|
+
}
|
|
858
|
+
|
|
674
859
|
|
|
675
860
|
return res.sendSuccess( 'Ticket raised successfully' );
|
|
676
861
|
}
|
|
677
862
|
} catch ( error ) {
|
|
678
863
|
const err = error.message || 'Internal Server Error';
|
|
679
|
-
logger.error( { error:
|
|
864
|
+
logger.error( { error: error, funtion: 'ticketCreation' } );
|
|
680
865
|
return res.sendError( err, 500 );
|
|
681
866
|
}
|
|
682
867
|
}
|
|
683
868
|
|
|
869
|
+
export async function ticketReview( req, res, next ) {
|
|
870
|
+
try {
|
|
871
|
+
const inputData = req.body;
|
|
872
|
+
if ( inputData?.type !== 'review' ) {
|
|
873
|
+
return next();
|
|
874
|
+
}
|
|
875
|
+
// check the createtion permission from the user permission
|
|
876
|
+
const userInfo = req?.user;
|
|
877
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
878
|
+
if ( !ticketsFeature ) {
|
|
879
|
+
return res.sendError( 'Forbidden to Reiew this Ticket', 403 );
|
|
880
|
+
}
|
|
881
|
+
|
|
882
|
+
// get store info by the storeId into mongo db
|
|
883
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
884
|
+
|
|
885
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
886
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
887
|
+
}
|
|
888
|
+
|
|
889
|
+
// get the footfall count from opensearch
|
|
890
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
891
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
892
|
+
const getQuery = {
|
|
893
|
+
query: {
|
|
894
|
+
terms: {
|
|
895
|
+
_id: [ dateString ],
|
|
896
|
+
},
|
|
897
|
+
},
|
|
898
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
899
|
+
sort: [
|
|
900
|
+
{
|
|
901
|
+
date_iso: {
|
|
902
|
+
order: 'desc',
|
|
903
|
+
},
|
|
904
|
+
},
|
|
905
|
+
],
|
|
906
|
+
};
|
|
907
|
+
|
|
908
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
909
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
910
|
+
logger.info( { hits } );
|
|
911
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
912
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
913
|
+
}
|
|
914
|
+
|
|
915
|
+
// get category details from the client level configuration
|
|
916
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
917
|
+
if ( !getConfig || getConfig == null ) {
|
|
918
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
919
|
+
}
|
|
920
|
+
|
|
921
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
922
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
923
|
+
// Initialize count object from taggingLimitation
|
|
924
|
+
const tempAcc = [];
|
|
925
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
926
|
+
if ( item?.type ) {
|
|
927
|
+
// Convert type to camelCase with "Count" suffix
|
|
928
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
929
|
+
const typeLower = item.type.toLowerCase();
|
|
930
|
+
let key;
|
|
931
|
+
if ( typeLower === 'housekeeping' ) {
|
|
932
|
+
key = 'houseKeepingCount';
|
|
933
|
+
} else {
|
|
934
|
+
// Convert first letter to lowercase and append "Count"
|
|
935
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
|
|
939
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
940
|
+
tempAcc.push( {
|
|
941
|
+
name: item.name,
|
|
942
|
+
value: 0,
|
|
943
|
+
key: key,
|
|
944
|
+
type: item.type,
|
|
945
|
+
} );
|
|
946
|
+
|
|
947
|
+
|
|
948
|
+
return acc;
|
|
949
|
+
}
|
|
950
|
+
}, {} ) || {};
|
|
951
|
+
|
|
952
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
953
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
954
|
+
const revopQuery = {
|
|
955
|
+
size: 0,
|
|
956
|
+
query: {
|
|
957
|
+
bool: {
|
|
958
|
+
must: [
|
|
959
|
+
{
|
|
960
|
+
term: {
|
|
961
|
+
'storeId.keyword': inputData.storeId,
|
|
962
|
+
},
|
|
963
|
+
},
|
|
964
|
+
{
|
|
965
|
+
term: {
|
|
966
|
+
'dateString': inputData.dateString,
|
|
967
|
+
},
|
|
968
|
+
},
|
|
969
|
+
{
|
|
970
|
+
term: {
|
|
971
|
+
'isParent': false,
|
|
972
|
+
},
|
|
973
|
+
},
|
|
974
|
+
{
|
|
975
|
+
term: {
|
|
976
|
+
isChecked: true,
|
|
977
|
+
},
|
|
978
|
+
},
|
|
979
|
+
],
|
|
980
|
+
},
|
|
981
|
+
},
|
|
982
|
+
aggs: {
|
|
983
|
+
type_counts: {
|
|
984
|
+
terms: {
|
|
985
|
+
field: 'revopsType.keyword',
|
|
986
|
+
size: 100,
|
|
987
|
+
},
|
|
988
|
+
},
|
|
989
|
+
},
|
|
990
|
+
};
|
|
991
|
+
|
|
992
|
+
|
|
993
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
994
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
995
|
+
|
|
996
|
+
// Map OpenSearch revopsType values to count object keys
|
|
997
|
+
buckets.forEach( ( bucket ) => {
|
|
998
|
+
const revopsType = bucket.key;
|
|
999
|
+
const count = bucket.doc_count || 0;
|
|
1000
|
+
|
|
1001
|
+
|
|
1002
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
1003
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
1004
|
+
const accMatch = tempAcc.find(
|
|
1005
|
+
( acc ) =>
|
|
1006
|
+
acc.type &&
|
|
1007
|
+
acc.type === revopsType,
|
|
1008
|
+
);
|
|
1009
|
+
|
|
1010
|
+
if ( accMatch && accMatch.key ) {
|
|
1011
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
1012
|
+
}
|
|
1013
|
+
}
|
|
1014
|
+
} );
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
|
|
1018
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1019
|
+
|
|
1020
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1021
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1022
|
+
0;
|
|
1023
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1024
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1025
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
1026
|
+
return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1027
|
+
}
|
|
1028
|
+
const taggingData = {
|
|
1029
|
+
size: 10000,
|
|
1030
|
+
query: {
|
|
1031
|
+
bool: {
|
|
1032
|
+
must: [
|
|
1033
|
+
{
|
|
1034
|
+
term: {
|
|
1035
|
+
'storeId.keyword': inputData.storeId,
|
|
1036
|
+
},
|
|
1037
|
+
},
|
|
1038
|
+
{
|
|
1039
|
+
term: {
|
|
1040
|
+
'dateString': inputData.dateString,
|
|
1041
|
+
},
|
|
1042
|
+
},
|
|
1043
|
+
],
|
|
1044
|
+
},
|
|
1045
|
+
},
|
|
1046
|
+
};
|
|
1047
|
+
|
|
1048
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1049
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1050
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1051
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1052
|
+
}
|
|
1053
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
1054
|
+
|
|
1055
|
+
const getTicket = {
|
|
1056
|
+
size: 10000,
|
|
1057
|
+
query: {
|
|
1058
|
+
bool: {
|
|
1059
|
+
must: [
|
|
1060
|
+
{
|
|
1061
|
+
term: {
|
|
1062
|
+
'storeId.keyword': inputData.storeId,
|
|
1063
|
+
},
|
|
1064
|
+
},
|
|
1065
|
+
{
|
|
1066
|
+
term: {
|
|
1067
|
+
'dateString': inputData.dateString,
|
|
1068
|
+
},
|
|
1069
|
+
},
|
|
1070
|
+
],
|
|
1071
|
+
},
|
|
1072
|
+
},
|
|
1073
|
+
};
|
|
1074
|
+
|
|
1075
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1076
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1077
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1078
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1079
|
+
}
|
|
1080
|
+
const record = {
|
|
1081
|
+
|
|
1082
|
+
status: 'Reviewer-Closed',
|
|
1083
|
+
revicedFootfall: revisedFootfall,
|
|
1084
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1085
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1086
|
+
// createdByEmail: req?.user?.email,
|
|
1087
|
+
// createdByUserName: req?.user?.userName,
|
|
1088
|
+
// createdByRole: req?.user?.role,
|
|
1089
|
+
|
|
1090
|
+
};
|
|
1091
|
+
|
|
1092
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1093
|
+
const temp = record.mappingInfo
|
|
1094
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1095
|
+
.map( ( item ) => ( {
|
|
1096
|
+
...item,
|
|
1097
|
+
mode: inputData.mode,
|
|
1098
|
+
revicedFootfall: revisedFootfall,
|
|
1099
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1100
|
+
count: tempAcc,
|
|
1101
|
+
revisedDetail: formattedTaggingData,
|
|
1102
|
+
status: 'Closed',
|
|
1103
|
+
createdByEmail: req?.user?.email,
|
|
1104
|
+
createdByUserName: req?.user?.userName,
|
|
1105
|
+
createdByRole: req?.user?.role,
|
|
1106
|
+
createdAt: new Date(),
|
|
1107
|
+
} ) );
|
|
1108
|
+
record.mappingInfo = [ ticketData?.[0]?._source?.mappingInfo[0], ...temp ];
|
|
1109
|
+
// If no review mapping existed, push a new one
|
|
1110
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1111
|
+
record.mappingInfo.push( {
|
|
1112
|
+
type: 'review',
|
|
1113
|
+
mode: inputData.mode,
|
|
1114
|
+
revicedFootfall: revisedFootfall,
|
|
1115
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1116
|
+
count: tempAcc,
|
|
1117
|
+
revisedDetail: formattedTaggingData,
|
|
1118
|
+
status: 'Closed',
|
|
1119
|
+
createdByEmail: req?.user?.email,
|
|
1120
|
+
createdByUserName: req?.user?.userName,
|
|
1121
|
+
createdByRole: req?.user?.role,
|
|
1122
|
+
createdAt: new Date(),
|
|
1123
|
+
} );
|
|
1124
|
+
}
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
|
|
1128
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1129
|
+
let isAutoCloseEnable = getConfig?.footfallDirectoryConfigs?.isAutoCloseEnable; ;
|
|
1130
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1131
|
+
|
|
1132
|
+
|
|
1133
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1134
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1135
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1136
|
+
|
|
1137
|
+
|
|
1138
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1139
|
+
if (
|
|
1140
|
+
isAutoCloseEnable === true &&
|
|
1141
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1142
|
+
) {
|
|
1143
|
+
record.status = 'Reviewer-Closed';
|
|
1144
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1145
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1146
|
+
const temp = record.mappingInfo
|
|
1147
|
+
.filter( ( item ) => item.type === 'review' )
|
|
1148
|
+
.map( ( item ) => ( {
|
|
1149
|
+
...item,
|
|
1150
|
+
mode: inputData.mode,
|
|
1151
|
+
revicedFootfall: revisedFootfall,
|
|
1152
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1153
|
+
count: tempAcc,
|
|
1154
|
+
revisedDetail: formattedTaggingData,
|
|
1155
|
+
status: 'Closed',
|
|
1156
|
+
createdByEmail: req?.user?.email,
|
|
1157
|
+
createdByUserName: req?.user?.userName,
|
|
1158
|
+
createdByRole: req?.user?.role,
|
|
1159
|
+
} ) );
|
|
1160
|
+
|
|
1161
|
+
const temp2 = record.mappingInfo
|
|
1162
|
+
.filter( ( item ) => item.type === 'tagging' )
|
|
1163
|
+
.map( ( item ) => ( {
|
|
1164
|
+
...item,
|
|
1165
|
+
mode: inputData.mode,
|
|
1166
|
+
// revicedFootfall: revisedFootfall,
|
|
1167
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1168
|
+
// count: tempAcc,
|
|
1169
|
+
// revisedDetail: formattedTaggingData,
|
|
1170
|
+
status: 'Closed',
|
|
1171
|
+
// createdByEmail: req?.user?.email,
|
|
1172
|
+
// createdByUserName: req?.user?.userName,
|
|
1173
|
+
// createdByRole: req?.user?.role,
|
|
1174
|
+
} ) );
|
|
1175
|
+
record.mappingInfo = [ ...temp2, ...temp ];
|
|
1176
|
+
// If no review mapping existed, push a new one
|
|
1177
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1178
|
+
record.mappingInfo.push( {
|
|
1179
|
+
type: 'review',
|
|
1180
|
+
mode: inputData.mode,
|
|
1181
|
+
revicedFootfall: revisedFootfall,
|
|
1182
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1183
|
+
count: tempAcc,
|
|
1184
|
+
revisedDetail: formattedTaggingData,
|
|
1185
|
+
status: 'Closed',
|
|
1186
|
+
createdByEmail: req?.user?.email,
|
|
1187
|
+
createdByUserName: req?.user?.userName,
|
|
1188
|
+
createdByRole: req?.user?.role,
|
|
1189
|
+
} );
|
|
1190
|
+
}
|
|
1191
|
+
}
|
|
1192
|
+
record.mappingInfo.push(
|
|
1193
|
+
{
|
|
1194
|
+
type: 'finalRevision',
|
|
1195
|
+
mode: inputData.mode,
|
|
1196
|
+
revicedFootfall: revisedFootfall,
|
|
1197
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1198
|
+
count: tempAcc,
|
|
1199
|
+
revisedDetail: formattedTaggingData,
|
|
1200
|
+
status: 'Closed',
|
|
1201
|
+
createdByEmail: req?.user?.email,
|
|
1202
|
+
createdByUserName: req?.user?.userName,
|
|
1203
|
+
createdByRole: req?.user?.role,
|
|
1204
|
+
createdAt: new Date(),
|
|
1205
|
+
},
|
|
1206
|
+
);
|
|
1207
|
+
} else {
|
|
1208
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1209
|
+
let revisionArray = [];
|
|
1210
|
+
|
|
1211
|
+
|
|
1212
|
+
revisionArray = getConfig?.footfallDirectoryConfigs?.revision || [];
|
|
1213
|
+
|
|
1214
|
+
|
|
1215
|
+
// Default fallbacks
|
|
1216
|
+
let revisionMapping = null;
|
|
1217
|
+
let approverMapping = null;
|
|
1218
|
+
let tangoReviewMapping = null;
|
|
1219
|
+
|
|
1220
|
+
// Find out which roles have isChecked true
|
|
1221
|
+
if ( Array.isArray( revisionArray ) && revisionArray.length > 0 ) {
|
|
1222
|
+
for ( const r of revisionArray ) {
|
|
1223
|
+
if ( r.actionType === 'approver' && r.isChecked === true ) {
|
|
1224
|
+
approverMapping = {
|
|
1225
|
+
type: 'approve',
|
|
1226
|
+
// revicedFootfall: revisedFootfall,
|
|
1227
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1228
|
+
count: tempAcc,
|
|
1229
|
+
revisedDetail: formattedTaggingData,
|
|
1230
|
+
status: 'Open',
|
|
1231
|
+
};
|
|
1232
|
+
} else if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1233
|
+
tangoReviewMapping = {
|
|
1234
|
+
type: 'tangoreview',
|
|
1235
|
+
// revicedFootfall: revisedFootfall,
|
|
1236
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1237
|
+
count: tempAcc,
|
|
1238
|
+
revisedDetail: formattedTaggingData,
|
|
1239
|
+
status: 'Open',
|
|
1240
|
+
};
|
|
1241
|
+
}
|
|
1242
|
+
}
|
|
1243
|
+
}
|
|
1244
|
+
|
|
1245
|
+
// Insert appropriate mappingInfo blocks
|
|
1246
|
+
if ( revisionMapping ) {
|
|
1247
|
+
// If reviewer and checked
|
|
1248
|
+
record.mappingInfo.push( revisionMapping );
|
|
1249
|
+
} else if ( approverMapping ) {
|
|
1250
|
+
// If approver and checked
|
|
1251
|
+
record.mappingInfo.push( approverMapping );
|
|
1252
|
+
} else if ( tangoReviewMapping ) {
|
|
1253
|
+
// If none above, then tangoReview
|
|
1254
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1255
|
+
}
|
|
1256
|
+
}
|
|
1257
|
+
|
|
1258
|
+
let checkreview = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'reviewer' && data.isChecked === true );
|
|
1259
|
+
|
|
1260
|
+
|
|
1261
|
+
if ( checkreview.length > 0 ) {
|
|
1262
|
+
let userQuery = [
|
|
1263
|
+
{
|
|
1264
|
+
$match: {
|
|
1265
|
+
clientId: getstoreName.clientId,
|
|
1266
|
+
role: 'admin',
|
|
1267
|
+
isActive: true,
|
|
1268
|
+
},
|
|
1269
|
+
},
|
|
1270
|
+
];
|
|
1271
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1272
|
+
|
|
1273
|
+
|
|
1274
|
+
// return;
|
|
1275
|
+
for ( let userData of finduserList ) {
|
|
1276
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1277
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1278
|
+
let description = `Created on ${createdOn}`;
|
|
1279
|
+
|
|
1280
|
+
let Data = {
|
|
1281
|
+
'title': title,
|
|
1282
|
+
'body': description,
|
|
1283
|
+
'type': 'review',
|
|
1284
|
+
'date': record.dateString,
|
|
1285
|
+
'storeId': record.storeId,
|
|
1286
|
+
'clientId': record.clientId,
|
|
1287
|
+
'ticketId': record.ticketId,
|
|
1288
|
+
};
|
|
1289
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1290
|
+
|
|
1291
|
+
if ( ticketsFeature ) {
|
|
1292
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1293
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1294
|
+
const fcmToken = userData.fcmToken;
|
|
1295
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1296
|
+
}
|
|
1297
|
+
}
|
|
1298
|
+
}
|
|
1299
|
+
}
|
|
1300
|
+
|
|
1301
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1302
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1303
|
+
|
|
1304
|
+
if ( insertResult && insertResult.statusCode === 201 || insertResult.statusCode === 200 ) {
|
|
1305
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1306
|
+
} else {
|
|
1307
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1308
|
+
}
|
|
1309
|
+
} catch ( error ) {
|
|
1310
|
+
const err = error.message || 'Internal Server Error';
|
|
1311
|
+
logger.error( { error: err, funtion: 'ticketreview' } );
|
|
1312
|
+
return res.sendError( err, 500 );
|
|
1313
|
+
}
|
|
1314
|
+
}
|
|
1315
|
+
|
|
1316
|
+
export async function ticketApprove( req, res, next ) {
|
|
1317
|
+
try {
|
|
1318
|
+
const inputData = req.body;
|
|
1319
|
+
if ( inputData?.type !== 'approve' ) {
|
|
1320
|
+
return next();
|
|
1321
|
+
}
|
|
1322
|
+
// check the createtion permission from the user permission
|
|
1323
|
+
const userInfo = req?.user;
|
|
1324
|
+
const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1325
|
+
if ( !ticketsFeature ) {
|
|
1326
|
+
return res.sendError( 'Forbidden to Approve this Ticket', 403 );
|
|
1327
|
+
}
|
|
1328
|
+
|
|
1329
|
+
// get store info by the storeId into mongo db
|
|
1330
|
+
const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
|
|
1331
|
+
|
|
1332
|
+
if ( !getstoreName || getstoreName == null ) {
|
|
1333
|
+
return res.sendError( 'The store ID is either inActive or not found', 400 );
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
// get the footfall count from opensearch
|
|
1337
|
+
const openSearch = JSON.parse( process.env.OPENSEARCH );
|
|
1338
|
+
const dateString = `${inputData.storeId}_${inputData.dateString}`;
|
|
1339
|
+
const getQuery = {
|
|
1340
|
+
query: {
|
|
1341
|
+
terms: {
|
|
1342
|
+
_id: [ dateString ],
|
|
1343
|
+
},
|
|
1344
|
+
},
|
|
1345
|
+
_source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
|
|
1346
|
+
sort: [
|
|
1347
|
+
{
|
|
1348
|
+
date_iso: {
|
|
1349
|
+
order: 'desc',
|
|
1350
|
+
},
|
|
1351
|
+
},
|
|
1352
|
+
],
|
|
1353
|
+
};
|
|
1354
|
+
|
|
1355
|
+
const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
|
|
1356
|
+
const hits = getFootfallCount?.body?.hits?.hits || [];
|
|
1357
|
+
logger.info( { hits } );
|
|
1358
|
+
if ( hits?.[0]?._source?.footfall_count <= 0 ) {
|
|
1359
|
+
return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
|
|
1360
|
+
}
|
|
1361
|
+
|
|
1362
|
+
// get category details from the client level configuration
|
|
1363
|
+
const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
|
|
1364
|
+
if ( !getConfig || getConfig == null ) {
|
|
1365
|
+
return res.sendError( 'The Client ID is either not configured or not found', 400 );
|
|
1366
|
+
}
|
|
1367
|
+
|
|
1368
|
+
// Get taggingLimitation from config (check both possible paths)
|
|
1369
|
+
const taggingLimitation = getConfig?.footfallDirectoryConfigs?.taggingLimitation;
|
|
1370
|
+
// Initialize count object from taggingLimitation
|
|
1371
|
+
const tempAcc = [];
|
|
1372
|
+
taggingLimitation?.reduce( ( acc, item ) => {
|
|
1373
|
+
if ( item?.type ) {
|
|
1374
|
+
// Convert type to camelCase with "Count" suffix
|
|
1375
|
+
// e.g., "duplicate" -> "duplicateCount", "housekeeping" -> "houseKeepingCount"
|
|
1376
|
+
const typeLower = item.type.toLowerCase();
|
|
1377
|
+
let key;
|
|
1378
|
+
if ( typeLower === 'housekeeping' ) {
|
|
1379
|
+
key = 'houseKeepingCount';
|
|
1380
|
+
} else {
|
|
1381
|
+
// Convert first letter to lowercase and append "Count"
|
|
1382
|
+
key = typeLower.charAt( 0 ) + typeLower.slice( 1 ) + 'Count';
|
|
1383
|
+
}
|
|
1384
|
+
|
|
1385
|
+
|
|
1386
|
+
// To change from an object to the desired array structure, assemble an array of objects:
|
|
1387
|
+
tempAcc.push( {
|
|
1388
|
+
name: item.name,
|
|
1389
|
+
value: 0,
|
|
1390
|
+
key: key,
|
|
1391
|
+
type: item.type,
|
|
1392
|
+
} );
|
|
1393
|
+
|
|
1394
|
+
|
|
1395
|
+
return acc;
|
|
1396
|
+
}
|
|
1397
|
+
}, {} ) || {};
|
|
1398
|
+
|
|
1399
|
+
// Query OpenSearch revop index to get actual counts for each type
|
|
1400
|
+
if ( taggingLimitation && taggingLimitation.length > 0 ) {
|
|
1401
|
+
const revopQuery = {
|
|
1402
|
+
size: 0,
|
|
1403
|
+
query: {
|
|
1404
|
+
bool: {
|
|
1405
|
+
must: [
|
|
1406
|
+
{
|
|
1407
|
+
term: {
|
|
1408
|
+
'storeId.keyword': inputData.storeId,
|
|
1409
|
+
},
|
|
1410
|
+
},
|
|
1411
|
+
{
|
|
1412
|
+
term: {
|
|
1413
|
+
'dateString': inputData.dateString,
|
|
1414
|
+
},
|
|
1415
|
+
},
|
|
1416
|
+
],
|
|
1417
|
+
},
|
|
1418
|
+
},
|
|
1419
|
+
aggs: {
|
|
1420
|
+
type_counts: {
|
|
1421
|
+
terms: {
|
|
1422
|
+
field: 'revopsType.keyword',
|
|
1423
|
+
size: 100,
|
|
1424
|
+
},
|
|
1425
|
+
},
|
|
1426
|
+
},
|
|
1427
|
+
};
|
|
1428
|
+
|
|
1429
|
+
|
|
1430
|
+
const revopData = await getOpenSearchData( openSearch.revop, revopQuery );
|
|
1431
|
+
const buckets = revopData?.body?.aggregations?.type_counts?.buckets || [];
|
|
1432
|
+
|
|
1433
|
+
// Map OpenSearch revopsType values to count object keys
|
|
1434
|
+
buckets.forEach( ( bucket ) => {
|
|
1435
|
+
const revopsType = bucket.key;
|
|
1436
|
+
const count = bucket.doc_count || 0;
|
|
1437
|
+
|
|
1438
|
+
|
|
1439
|
+
if ( Array.isArray( tempAcc ) ) {
|
|
1440
|
+
// Find the tempAcc entry whose type (case-insensitive) matches revopsType
|
|
1441
|
+
const accMatch = tempAcc.find(
|
|
1442
|
+
( acc ) =>
|
|
1443
|
+
acc.type &&
|
|
1444
|
+
acc.type === revopsType,
|
|
1445
|
+
);
|
|
1446
|
+
|
|
1447
|
+
if ( accMatch && accMatch.key ) {
|
|
1448
|
+
tempAcc.find( ( a ) => a.key === accMatch.key ).value = count;
|
|
1449
|
+
}
|
|
1450
|
+
}
|
|
1451
|
+
} );
|
|
1452
|
+
}
|
|
1453
|
+
|
|
1454
|
+
|
|
1455
|
+
// Calculate revisedFootfall: footfallCount - (sum of all counts)
|
|
1456
|
+
|
|
1457
|
+
const totalCount = Array.isArray( tempAcc ) ?
|
|
1458
|
+
tempAcc.reduce( ( sum, acc ) => sum + ( acc.value || 0 ), 0 ) :
|
|
1459
|
+
0;
|
|
1460
|
+
const footfallCount = hits?.[0]?._source?.footfall_count || 0;
|
|
1461
|
+
const revisedFootfall = Math.max( 0, footfallCount - totalCount );
|
|
1462
|
+
logger.info( { footfallCount, revisedFootfall } );
|
|
1463
|
+
if ( footfallCount - revisedFootfall == 0 ) {
|
|
1464
|
+
return res.sendError( 'Cannot review a ticket because footfall hasn’t changed', 400 );
|
|
1465
|
+
}
|
|
1466
|
+
|
|
1467
|
+
const taggingData = {
|
|
1468
|
+
size: 10000,
|
|
1469
|
+
query: {
|
|
1470
|
+
bool: {
|
|
1471
|
+
must: [
|
|
1472
|
+
{
|
|
1473
|
+
term: {
|
|
1474
|
+
'storeId.keyword': inputData.storeId,
|
|
1475
|
+
},
|
|
1476
|
+
},
|
|
1477
|
+
{
|
|
1478
|
+
term: {
|
|
1479
|
+
'dateString': inputData.dateString,
|
|
1480
|
+
},
|
|
1481
|
+
},
|
|
1482
|
+
{
|
|
1483
|
+
term: {
|
|
1484
|
+
'isParent': false,
|
|
1485
|
+
},
|
|
1486
|
+
},
|
|
1487
|
+
{
|
|
1488
|
+
term: {
|
|
1489
|
+
isChecked: true,
|
|
1490
|
+
},
|
|
1491
|
+
},
|
|
1492
|
+
],
|
|
1493
|
+
},
|
|
1494
|
+
},
|
|
1495
|
+
};
|
|
1496
|
+
|
|
1497
|
+
const revopTaggingData = await getOpenSearchData( openSearch.revop, taggingData );
|
|
1498
|
+
const taggingImages = revopTaggingData?.body?.hits?.hits;
|
|
1499
|
+
if ( !taggingImages || taggingImages?.length == 0 ) {
|
|
1500
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1501
|
+
}
|
|
1502
|
+
|
|
1503
|
+
const formattedTaggingData = formatRevopTaggingHits( taggingImages );
|
|
1504
|
+
|
|
1505
|
+
const getTicket = {
|
|
1506
|
+
size: 10000,
|
|
1507
|
+
query: {
|
|
1508
|
+
bool: {
|
|
1509
|
+
must: [
|
|
1510
|
+
{
|
|
1511
|
+
term: {
|
|
1512
|
+
'storeId.keyword': inputData.storeId,
|
|
1513
|
+
},
|
|
1514
|
+
},
|
|
1515
|
+
{
|
|
1516
|
+
term: {
|
|
1517
|
+
'dateString': inputData.dateString,
|
|
1518
|
+
},
|
|
1519
|
+
},
|
|
1520
|
+
],
|
|
1521
|
+
},
|
|
1522
|
+
},
|
|
1523
|
+
};
|
|
1524
|
+
|
|
1525
|
+
const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
|
|
1526
|
+
const ticketData = getFootfallticketData?.body?.hits?.hits;
|
|
1527
|
+
if ( !ticketData || ticketData?.length == 0 ) {
|
|
1528
|
+
return res.sendError( 'You don’t have any tagged images right now', 400 );
|
|
1529
|
+
}
|
|
1530
|
+
logger.info( { ticketData, getFootfallticketData } );
|
|
1531
|
+
const record = {
|
|
1532
|
+
|
|
1533
|
+
status: 'Approver-Closed',
|
|
1534
|
+
revicedFootfall: revisedFootfall,
|
|
1535
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1536
|
+
mappingInfo: ticketData?.[0]?._source?.mappingInfo,
|
|
1537
|
+
// createdByEmail: req?.user?.email,
|
|
1538
|
+
// createdByUserName: req?.user?.userName,
|
|
1539
|
+
// createdByRole: req?.user?.role,
|
|
1540
|
+
|
|
1541
|
+
};
|
|
1542
|
+
|
|
1543
|
+
|
|
1544
|
+
// Retrieve client footfallDirectoryConfigs revision
|
|
1545
|
+
let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
|
|
1546
|
+
let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
|
|
1547
|
+
|
|
1548
|
+
const getNumber = autoCloseAccuracy.split( '%' )[0];
|
|
1549
|
+
logger.info( { getNumber } );
|
|
1550
|
+
let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
|
|
1551
|
+
let revisedPercentage = Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 );
|
|
1552
|
+
const revised = Number( Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) );
|
|
1553
|
+
const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
|
|
1554
|
+
logger.info( { tangoReview, revised } );
|
|
1555
|
+
// If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
|
|
1556
|
+
if (
|
|
1557
|
+
isAutoCloseEnable === true &&
|
|
1558
|
+
revisedPercentage >= autoCloseAccuracyValue
|
|
1559
|
+
) {
|
|
1560
|
+
record.status = 'Approver-Closed';
|
|
1561
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1562
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1563
|
+
const temp = record.mappingInfo
|
|
1564
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1565
|
+
.map( ( item ) => ( {
|
|
1566
|
+
...item,
|
|
1567
|
+
|
|
1568
|
+
mode: inputData.mode,
|
|
1569
|
+
revicedFootfall: revisedFootfall,
|
|
1570
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1571
|
+
count: tempAcc,
|
|
1572
|
+
revisedDetail: formattedTaggingData,
|
|
1573
|
+
status: 'Closed',
|
|
1574
|
+
createdByEmail: req?.user?.email,
|
|
1575
|
+
createdByUserName: req?.user?.userName,
|
|
1576
|
+
createdByRole: req?.user?.role,
|
|
1577
|
+
} ) );
|
|
1578
|
+
|
|
1579
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1580
|
+
...temp ];
|
|
1581
|
+
// If updating the mapping config to mark [i].status as 'Closed'
|
|
1582
|
+
// Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
|
|
1583
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1584
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1585
|
+
return {
|
|
1586
|
+
...item,
|
|
1587
|
+
status: 'Closed',
|
|
1588
|
+
};
|
|
1589
|
+
} );
|
|
1590
|
+
}
|
|
1591
|
+
// If no review mapping existed, push a new one
|
|
1592
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1593
|
+
record.mappingInfo.push( {
|
|
1594
|
+
type: 'approve',
|
|
1595
|
+
mode: inputData.mode,
|
|
1596
|
+
revicedFootfall: revisedFootfall,
|
|
1597
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1598
|
+
count: tempAcc,
|
|
1599
|
+
revisedDetail: formattedTaggingData,
|
|
1600
|
+
status: 'Closed',
|
|
1601
|
+
createdByEmail: req?.user?.email,
|
|
1602
|
+
createdByUserName: req?.user?.userName,
|
|
1603
|
+
createdByRole: req?.user?.role,
|
|
1604
|
+
} );
|
|
1605
|
+
}
|
|
1606
|
+
}
|
|
1607
|
+
record.mappingInfo.push(
|
|
1608
|
+
{
|
|
1609
|
+
type: 'finalRevision',
|
|
1610
|
+
mode: inputData.mode,
|
|
1611
|
+
revicedFootfall: revisedFootfall,
|
|
1612
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1613
|
+
count: tempAcc,
|
|
1614
|
+
revisedDetail: formattedTaggingData,
|
|
1615
|
+
status: 'Closed',
|
|
1616
|
+
createdByEmail: req?.user?.email,
|
|
1617
|
+
createdByUserName: req?.user?.userName,
|
|
1618
|
+
createdByRole: req?.user?.role,
|
|
1619
|
+
createdAt: new Date(),
|
|
1620
|
+
},
|
|
1621
|
+
);
|
|
1622
|
+
} else if ( revised < tangoReview ) {
|
|
1623
|
+
// If ticket is closed, do not proceed with revision mapping
|
|
1624
|
+
|
|
1625
|
+
// Default fallbacks
|
|
1626
|
+
|
|
1627
|
+
let approverMapping = null;
|
|
1628
|
+
let tangoReviewMapping = null;
|
|
1629
|
+
|
|
1630
|
+
record.status = 'Approver-Closed';
|
|
1631
|
+
// Only keep or modify mappingInfo items with type "review"
|
|
1632
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1633
|
+
const temp = record.mappingInfo
|
|
1634
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1635
|
+
.map( ( item ) => ( {
|
|
1636
|
+
...item,
|
|
1637
|
+
mode: inputData.mode,
|
|
1638
|
+
revicedFootfall: revisedFootfall,
|
|
1639
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1640
|
+
count: tempAcc,
|
|
1641
|
+
revisedDetail: formattedTaggingData,
|
|
1642
|
+
status: 'Closed',
|
|
1643
|
+
createdByEmail: req?.user?.email,
|
|
1644
|
+
createdByUserName: req?.user?.userName,
|
|
1645
|
+
createdByRole: req?.user?.role,
|
|
1646
|
+
} ) );
|
|
1647
|
+
|
|
1648
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1649
|
+
...temp ];
|
|
1650
|
+
// if ( Array.isArray( record.mappingInfo ) ) {
|
|
1651
|
+
// record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1652
|
+
// return {
|
|
1653
|
+
// ...item,
|
|
1654
|
+
// status: 'Closed',
|
|
1655
|
+
// };
|
|
1656
|
+
// } );
|
|
1657
|
+
// }
|
|
1658
|
+
// If no review mapping existed, push a new one
|
|
1659
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1660
|
+
record.mappingInfo.push( {
|
|
1661
|
+
type: 'approve',
|
|
1662
|
+
mode: inputData.mode,
|
|
1663
|
+
revicedFootfall: revisedFootfall,
|
|
1664
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1665
|
+
count: tempAcc,
|
|
1666
|
+
revisedDetail: formattedTaggingData,
|
|
1667
|
+
status: 'Closed',
|
|
1668
|
+
createdByEmail: req?.user?.email,
|
|
1669
|
+
createdByUserName: req?.user?.userName,
|
|
1670
|
+
createdByRole: req?.user?.role,
|
|
1671
|
+
} );
|
|
1672
|
+
}
|
|
1673
|
+
}
|
|
1674
|
+
|
|
1675
|
+
// Find out which roles have isChecked true
|
|
1676
|
+
|
|
1677
|
+
// for ( const r of revisionArray ) {
|
|
1678
|
+
// if ( r.actionType === 'tango' && r.isChecked === true ) {
|
|
1679
|
+
tangoReviewMapping = {
|
|
1680
|
+
type: 'tangoreview',
|
|
1681
|
+
// revicedFootfall: revisedFootfall,
|
|
1682
|
+
// revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1683
|
+
count: tempAcc,
|
|
1684
|
+
revisedDetail: formattedTaggingData,
|
|
1685
|
+
status: 'Open',
|
|
1686
|
+
};
|
|
1687
|
+
// }
|
|
1688
|
+
// }
|
|
1689
|
+
|
|
1690
|
+
|
|
1691
|
+
if ( approverMapping ) {
|
|
1692
|
+
// If approver and checked
|
|
1693
|
+
record.mappingInfo.push( approverMapping );
|
|
1694
|
+
} else if ( tangoReviewMapping ) {
|
|
1695
|
+
// If none above, then tangoReview
|
|
1696
|
+
record.mappingInfo.push( tangoReviewMapping );
|
|
1697
|
+
}
|
|
1698
|
+
} else {
|
|
1699
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1700
|
+
const temp = record.mappingInfo
|
|
1701
|
+
.filter( ( item ) => item.type === 'approve' )
|
|
1702
|
+
.map( ( item ) => ( {
|
|
1703
|
+
...item,
|
|
1704
|
+
mode: inputData.mode,
|
|
1705
|
+
revicedFootfall: revisedFootfall,
|
|
1706
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1707
|
+
count: tempAcc,
|
|
1708
|
+
revisedDetail: formattedTaggingData,
|
|
1709
|
+
status: 'Closed',
|
|
1710
|
+
createdByEmail: req?.user?.email,
|
|
1711
|
+
createdByUserName: req?.user?.userName,
|
|
1712
|
+
createdByRole: req?.user?.role,
|
|
1713
|
+
} ) );
|
|
1714
|
+
|
|
1715
|
+
record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
|
|
1716
|
+
...temp ];
|
|
1717
|
+
if ( Array.isArray( record.mappingInfo ) ) {
|
|
1718
|
+
record.mappingInfo = record.mappingInfo.map( ( item ) => {
|
|
1719
|
+
return {
|
|
1720
|
+
...item,
|
|
1721
|
+
status: 'Closed',
|
|
1722
|
+
};
|
|
1723
|
+
} );
|
|
1724
|
+
}
|
|
1725
|
+
// If no review mapping existed, push a new one
|
|
1726
|
+
if ( record.mappingInfo.length === 0 ) {
|
|
1727
|
+
record.mappingInfo.push( {
|
|
1728
|
+
type: 'approve',
|
|
1729
|
+
mode: inputData.mode,
|
|
1730
|
+
revicedFootfall: revisedFootfall,
|
|
1731
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1732
|
+
count: tempAcc,
|
|
1733
|
+
revisedDetail: formattedTaggingData,
|
|
1734
|
+
status: 'Closed',
|
|
1735
|
+
createdByEmail: req?.user?.email,
|
|
1736
|
+
createdByUserName: req?.user?.userName,
|
|
1737
|
+
createdByRole: req?.user?.role,
|
|
1738
|
+
} );
|
|
1739
|
+
}
|
|
1740
|
+
}
|
|
1741
|
+
record.mappingInfo.push(
|
|
1742
|
+
{
|
|
1743
|
+
type: 'finalRevision',
|
|
1744
|
+
mode: inputData.mode,
|
|
1745
|
+
revicedFootfall: revisedFootfall,
|
|
1746
|
+
revicedPerc: Math.round( ( revisedFootfall / footfallCount ) * 100 || 0 ) + '%',
|
|
1747
|
+
count: tempAcc,
|
|
1748
|
+
revisedDetail: formattedTaggingData,
|
|
1749
|
+
status: 'Closed',
|
|
1750
|
+
createdByEmail: req?.user?.email,
|
|
1751
|
+
createdByUserName: req?.user?.userName,
|
|
1752
|
+
createdByRole: req?.user?.role,
|
|
1753
|
+
createdAt: new Date(),
|
|
1754
|
+
},
|
|
1755
|
+
);
|
|
1756
|
+
}
|
|
1757
|
+
console.log( req.body, getConfig.footfallDirectoryConfigs.revision );
|
|
1758
|
+
let checkapprove = getConfig.footfallDirectoryConfigs.revision.filter( ( data ) => data.actionType === 'approver' && data.isChecked === true );
|
|
1759
|
+
|
|
1760
|
+
|
|
1761
|
+
if ( checkapprove.length > 0 ) {
|
|
1762
|
+
let userQuery = [
|
|
1763
|
+
{
|
|
1764
|
+
$match: {
|
|
1765
|
+
clientId: getstoreName.clientId,
|
|
1766
|
+
role: 'admin',
|
|
1767
|
+
isActive: true,
|
|
1768
|
+
},
|
|
1769
|
+
},
|
|
1770
|
+
];
|
|
1771
|
+
let finduserList = await aggregateUser( userQuery );
|
|
1772
|
+
|
|
1773
|
+
|
|
1774
|
+
for ( let userData of finduserList ) {
|
|
1775
|
+
let title = `${getstoreName?.storeName} Have raised a ticket for a Footfall Mismatch`;
|
|
1776
|
+
let createdOn = dayjs().format( 'DD MMM YYYY' );
|
|
1777
|
+
let description = `Created on ${createdOn}`;
|
|
1778
|
+
|
|
1779
|
+
let Data = {
|
|
1780
|
+
'title': title,
|
|
1781
|
+
'body': description,
|
|
1782
|
+
'type': 'approve',
|
|
1783
|
+
'date': record.dateString,
|
|
1784
|
+
'storeId': record.storeId,
|
|
1785
|
+
'clientId': record.clientId,
|
|
1786
|
+
'ticketId': record.ticketId,
|
|
1787
|
+
};
|
|
1788
|
+
|
|
1789
|
+
const ticketsFeature = userData?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
|
|
1790
|
+
|
|
1791
|
+
if ( ticketsFeature ) {
|
|
1792
|
+
let notifyuser = await getAssinedStore( userData, req.body.storeId );
|
|
1793
|
+
if ( userData && userData.fcmToken && notifyuser ) {
|
|
1794
|
+
const fcmToken = userData.fcmToken;
|
|
1795
|
+
await sendPushNotification( title, description, fcmToken, Data );
|
|
1796
|
+
}
|
|
1797
|
+
}
|
|
1798
|
+
}
|
|
1799
|
+
}
|
|
1800
|
+
|
|
1801
|
+
const id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
|
|
1802
|
+
const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
|
|
1803
|
+
|
|
1804
|
+
|
|
1805
|
+
if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
|
|
1806
|
+
return res.sendSuccess( 'Ticket closed successfully' );
|
|
1807
|
+
} else {
|
|
1808
|
+
return res.sendError( 'Internal Server Error', 500 );
|
|
1809
|
+
}
|
|
1810
|
+
} catch ( error ) {
|
|
1811
|
+
const err = error.message || 'Internal Server Error';
|
|
1812
|
+
logger.error( { error: err, funtion: 'ticketCreation' } );
|
|
1813
|
+
return res.sendError( err, 500 );
|
|
1814
|
+
}
|
|
1815
|
+
}
|
|
1816
|
+
|
|
1817
|
+
export async function getAssinedStore( user, storeId ) {
|
|
1818
|
+
if ( !user || user.userType !== 'client' || user.role === 'superadmin' ) {
|
|
1819
|
+
return;
|
|
1820
|
+
}
|
|
1821
|
+
|
|
1822
|
+
const clientId = user.clientId;
|
|
1823
|
+
const storeIds = new Set(
|
|
1824
|
+
user.assignedStores?.map( ( store ) => store.storeId ) ?? [],
|
|
1825
|
+
);
|
|
1826
|
+
|
|
1827
|
+
const addClusterStores = ( clusters ) => {
|
|
1828
|
+
if ( !clusters?.length ) return;
|
|
1829
|
+
for ( const cluster of clusters ) {
|
|
1830
|
+
cluster.stores?.forEach( ( store ) => storeIds.add( store.storeId ) );
|
|
1831
|
+
}
|
|
1832
|
+
};
|
|
1833
|
+
|
|
1834
|
+
// Fetch all top-level data in parallel
|
|
1835
|
+
const [ clustersList, teamsList, teamMemberList ] = await Promise.all( [
|
|
1836
|
+
findcluster( {
|
|
1837
|
+
clientId,
|
|
1838
|
+
Teamlead: { $elemMatch: { email: user.email } },
|
|
1839
|
+
} ),
|
|
1840
|
+
findteams( {
|
|
1841
|
+
clientId,
|
|
1842
|
+
Teamlead: { $elemMatch: { email: user.email } },
|
|
1843
|
+
} ),
|
|
1844
|
+
findteams( {
|
|
1845
|
+
clientId,
|
|
1846
|
+
users: { $elemMatch: { email: user.email } },
|
|
1847
|
+
} ),
|
|
1848
|
+
] );
|
|
1849
|
+
|
|
1850
|
+
// 1) Clusters where this user is Teamlead
|
|
1851
|
+
addClusterStores( clustersList );
|
|
1852
|
+
|
|
1853
|
+
// 2) Teams where this user is Teamlead → their users + their clusters
|
|
1854
|
+
if ( teamsList?.length ) {
|
|
1855
|
+
for ( const team of teamsList ) {
|
|
1856
|
+
if ( !team.users?.length ) continue;
|
|
1857
|
+
|
|
1858
|
+
await Promise.all(
|
|
1859
|
+
team.users.map( async ( teamUser ) => {
|
|
1860
|
+
const foundUser = await findOneUser( { _id: teamUser.userId } );
|
|
1861
|
+
if ( !foundUser ) return;
|
|
1862
|
+
|
|
1863
|
+
// Direct assigned stores of that user
|
|
1864
|
+
if ( foundUser.assignedStores?.length ) {
|
|
1865
|
+
foundUser.assignedStores.forEach( ( store ) =>
|
|
1866
|
+
storeIds.add( store.storeId ),
|
|
1867
|
+
);
|
|
1868
|
+
}
|
|
1869
|
+
|
|
1870
|
+
// Clusters where this user is Teamlead
|
|
1871
|
+
const userClustersList = await findcluster( {
|
|
1872
|
+
clientId,
|
|
1873
|
+
Teamlead: { $elemMatch: { email: foundUser.email } },
|
|
1874
|
+
} );
|
|
1875
|
+
addClusterStores( userClustersList );
|
|
1876
|
+
} ),
|
|
1877
|
+
);
|
|
1878
|
+
}
|
|
1879
|
+
}
|
|
1880
|
+
|
|
1881
|
+
// 3) Teams where this user is a member → clusters by teamName
|
|
1882
|
+
if ( teamMemberList?.length ) {
|
|
1883
|
+
for ( const team of teamMemberList ) {
|
|
1884
|
+
const clusterList = await findcluster( {
|
|
1885
|
+
clientId,
|
|
1886
|
+
teams: { $elemMatch: { name: team.teamName } },
|
|
1887
|
+
} );
|
|
1888
|
+
addClusterStores( clusterList );
|
|
1889
|
+
}
|
|
1890
|
+
}
|
|
1891
|
+
|
|
1892
|
+
const assignedStores = Array.from( storeIds );
|
|
1893
|
+
|
|
1894
|
+
// Previously you returned `true` in both branches.
|
|
1895
|
+
// Assuming you actually want to check membership:
|
|
1896
|
+
return assignedStores.includes( storeId );
|
|
1897
|
+
}
|
|
684
1898
|
|