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