tango-app-api-infra 3.9.5-vms.9 → 3.9.5-vms.91

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,13 +1,15 @@
1
1
  import { chunkArray, download, logger, sendMessageToFIFOQueue, sendMessageToQueue } from 'tango-app-api-middleware';
2
- import { bulkUpdate, getOpenSearchById, getOpenSearchCount, getOpenSearchData, insertWithId, updateOpenSearchData, upsertOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
2
+ import { bulkUpdate, getOpenSearchById, getOpenSearchCount, getOpenSearchData, insertWithId, updateOpenSearchData } from 'tango-app-api-middleware/src/utils/openSearch.js';
3
3
  import { findOneStore } from '../services/store.service.js';
4
4
  import { countDocumnetsCamera } from '../services/camera.service.js';
5
5
  import { findOneRevopDownload, upsertRevopDownload } from '../services/revopDownload.service.js';
6
6
  import dayjs from 'dayjs';
7
7
  import utc from 'dayjs/plugin/utc.js';
8
8
  import timezone from 'dayjs/plugin/timezone.js';
9
- import { findUser } from '../services/user.service.js';
10
-
9
+ import { findOneClient } from '../services/client.service.js';
10
+ import { findUser, findOneUser } from '../services/user.service.js';
11
+ import { sendPushNotification } from 'tango-app-api-middleware';
12
+ import { findStoreAccuracIssues, upsertStoreAccuracIssues } from '../services/storeAccuracyIssues.service.js';
11
13
  dayjs.extend( utc );
12
14
  dayjs.extend( timezone );
13
15
 
@@ -24,7 +26,7 @@ export async function createTicket( req, res ) {
24
26
  inputData.userName = req?.user?.userName;
25
27
  inputData.email = req?.user?.email;
26
28
  inputData.role = req?.user?.role;
27
- inputData.status = 'open';
29
+ inputData.status = 'Open';
28
30
  inputData.duplicateACCount = 0;
29
31
  inputData.employeeACCount = 0;
30
32
  inputData.houseKeepingACCount = 0;
@@ -60,6 +62,643 @@ export async function createTicket( req, res ) {
60
62
  }
61
63
  }
62
64
 
65
+ export async function createinternalTicket( req, res ) {
66
+ try {
67
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
68
+ let inputData = req.body;
69
+ let record = {
70
+
71
+ storeId: inputData.storeId,
72
+ type: 'internal',
73
+ dateString: inputData.dateString,
74
+ storeName: inputData?.storeName,
75
+ ticketName: inputData.ticketName || 'footfall-directory',
76
+ footfallCount: inputData.footfallCount,
77
+ clientId: inputData?.clientId,
78
+ ticketId: 'TE_FDT_' + new Date().valueOf(),
79
+ createdAt: new Date(),
80
+ updatedAt: new Date(),
81
+ status: 'Open',
82
+ comments: inputData?.comments || '',
83
+ createdByEmail: req?.user?.email,
84
+ createdByUserName: req?.user?.userName,
85
+ createdByRole: req?.user?.role,
86
+ mappingInfo: [],
87
+ };
88
+ const id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
89
+ let getExistingOne = await getOpenSearchById( openSearch.footfallDirectory, id );
90
+ if ( getExistingOne?.body?._source ) {
91
+ return res.sendError( 'Ticket Already Exists', 500 );
92
+ }
93
+ const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
94
+ if ( insertResult && insertResult.statusCode === 201 ) {
95
+ return res.sendSuccess( 'Ticket raised successfully' );
96
+ }
97
+ } catch ( error ) {
98
+ const err = error.message || 'Internal Server Error';
99
+ logger.error( { error: error, funtion: 'createinternalTicket' } );
100
+ return res.sendError( err, 500 );
101
+ }
102
+ }
103
+ export async function tangoReviewTicket( req, res ) {
104
+ try {
105
+ const inputData = req.body;
106
+
107
+ logger.info( { inputData, msg: '...........1' } );
108
+ // get store info by the storeId into mongo db
109
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
110
+ logger.info( { getstoreName, msg: '...........2' } );
111
+ if ( !getstoreName || getstoreName == null ) {
112
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
113
+ }
114
+
115
+ // get the footfall count from opensearch
116
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
117
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
118
+ logger.info( { dateString, msg: '...........3' } );
119
+ const getQuery = {
120
+ query: {
121
+ terms: {
122
+ _id: [ dateString ],
123
+ },
124
+ },
125
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
126
+ sort: [
127
+ {
128
+ date_iso: {
129
+ order: 'desc',
130
+ },
131
+ },
132
+ ],
133
+ };
134
+
135
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
136
+ logger.info( { getFootfallCount, msg: '...........4' } );
137
+ const hits = getFootfallCount?.body?.hits?.hits || [];
138
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
139
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
140
+ }
141
+ logger.info( { hits, msg: '...........5' } );
142
+ // get category details from the client level configuration
143
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
144
+ logger.info( { getConfig, msg: '...........6' } );
145
+ if ( !getConfig || getConfig == null ) {
146
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
147
+ }
148
+
149
+ let findQuery = {
150
+ size: 10000,
151
+ query: {
152
+ bool: {
153
+ must: [
154
+ {
155
+ term: {
156
+ 'storeId.keyword': inputData.storeId,
157
+ },
158
+ },
159
+ {
160
+ term: {
161
+ 'dateString': inputData.dateString,
162
+ },
163
+ },
164
+ ],
165
+ },
166
+ },
167
+ };
168
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
169
+ logger.info( { findTicket, msg: '...........7' } );
170
+ let Ticket = findTicket.body?.hits?.hits;
171
+ logger.info( { Ticket, msg: '...........8' } );
172
+ if ( Ticket.length === 0 ) {
173
+ return res.sendError( 'Ticket not found', 400 );
174
+ }
175
+ const getTicket = {
176
+ size: 10000,
177
+ query: {
178
+ bool: {
179
+ must: [
180
+ {
181
+ term: {
182
+ 'storeId.keyword': inputData.storeId,
183
+ },
184
+ },
185
+ {
186
+ term: {
187
+ 'dateString': inputData.dateString,
188
+ },
189
+ },
190
+
191
+ ],
192
+ },
193
+ },
194
+ };
195
+ if ( Ticket[0]?._source?.type !== 'internal' ) {
196
+ getTicket.query.bool.must.push(
197
+ {
198
+ nested: {
199
+ path: 'mappingInfo',
200
+ query: {
201
+ bool: {
202
+ must: [
203
+ {
204
+ term: {
205
+ 'mappingInfo.type': 'tangoreview',
206
+ },
207
+ },
208
+
209
+ ],
210
+ },
211
+ },
212
+ },
213
+ },
214
+ );
215
+ }
216
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
217
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
218
+ logger.info( { ticketData, msg: '...........9' } );
219
+ if ( !ticketData || ticketData?.length == 0 ) {
220
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
221
+ }
222
+
223
+ const record = {
224
+
225
+ status: parseInt( inputData?.mappingInfo?.[0]?.revicedPerc || 0 ) < parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview|| 0 ) ? 'Open - Accuracy Issue' : 'Closed',
226
+ revicedFootfall: inputData.mappingInfo?.[0]?.revicedFootfall,
227
+ revicedPerc: inputData.mappingInfo?.[0]?.revicedPerc,
228
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
229
+ createdByEmail: req?.user?.email,
230
+ createdByUserName: req?.user?.userName,
231
+ createdByRole: req?.user?.role,
232
+
233
+ };
234
+ logger.info( { record, msg: '...........10' } );
235
+
236
+ // Retrieve client footfallDirectoryConfigs revision
237
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
238
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
239
+ logger.info( { isAutoCloseEnable, autoCloseAccuracy, msg: '...........11' } );
240
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
241
+ logger.info( { getNumber, msg: '...........12' } );
242
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
243
+ logger.info( { autoCloseAccuracyValue, msg: '...........13' } );
244
+ let revisedPercentage = inputData.mappingInfo?.revicedPerc;
245
+ logger.info( { revisedPercentage, msg: '...........14' } );
246
+ const revised = Number( revisedPercentage?.split( '%' )[0] );
247
+ logger.info( { revised, msg: '...........15' } );
248
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
249
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
250
+ logger.info( { tangoReview, msg: '...........16' } );
251
+ if (
252
+ isAutoCloseEnable === true &&
253
+ revisedPercentage >= autoCloseAccuracyValue
254
+ ) {
255
+ logger.info( { isAutoCloseEnable, revisedPercentage, autoCloseAccuracyValue, msg: '...........17' } );
256
+ record.status = 'Closed';
257
+ // Only keep or modify mappingInfo items with type "review"
258
+ if ( Array.isArray( record.mappingInfo ) ) {
259
+ logger.info( { msg: '...........18' } );
260
+ const temp = record.mappingInfo
261
+ .filter( ( item ) => item.type === 'tangoreview' )
262
+ .map( ( item ) => ( {
263
+ ...item,
264
+
265
+ mode: inputData.mappingInfo?.mode,
266
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
267
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
268
+ count: inputData.mappingInfo?.count,
269
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
270
+ status: 'Closed',
271
+ createdByEmail: req?.user?.email,
272
+ createdByUserName: req?.user?.userName,
273
+ createdByRole: req?.user?.role,
274
+ createdAt: new Date(),
275
+ } ) );
276
+
277
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
278
+ ...temp ];
279
+ // If updating the mapping config to mark [i].status as 'Closed'
280
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
281
+ if ( Array.isArray( record.mappingInfo ) ) {
282
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
283
+ return {
284
+ ...item,
285
+ status: item.type === 'approve'? 'Tango Review Done':'Closed',
286
+ };
287
+ } );
288
+ }
289
+ }
290
+ record.mappingInfo.push(
291
+ {
292
+ type: 'finalRevision',
293
+ mode: inputData.mappingInfo?.mode,
294
+ revicedFootfall: revisedFootfall,
295
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
296
+ count: inputData.mappingInfo?.count,
297
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
298
+ status: 'Closed',
299
+ createdByEmail: req?.user?.email,
300
+ createdByUserName: req?.user?.userName,
301
+ createdByRole: req?.user?.role,
302
+ createdAt: new Date(),
303
+ },
304
+ );
305
+ } else if ( revised < tangoReview ) {
306
+ logger.info( { msg: '...........19', revised, tangoReview, status: record.status } );
307
+ // If ticket is closed, do not proceed with revision mapping
308
+
309
+ record.status = 'Open - Accuracy Issue';
310
+ // Only keep or modify mappingInfo items with type "review"
311
+ if ( Array.isArray( record.mappingInfo ) ) {
312
+ logger.info( { msg: '...........20', status: record.status } );
313
+ const temp = record.mappingInfo
314
+ .filter( ( item ) => item.type === 'tangoreview' )
315
+ .map( ( item ) => ( {
316
+ ...item,
317
+ mode: inputData.mappingInfo?.mode,
318
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
319
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
320
+ reviced: parseInt( inputData.mappingInfo?.revicedPerc ),
321
+ count: inputData.mappingInfo?.count,
322
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
323
+ status: 'Open - Accuracy Issue',
324
+ createdByEmail: req?.user?.email,
325
+ createdByUserName: req?.user?.userName,
326
+ createdByRole: req?.user?.role,
327
+ createdAt: new Date(),
328
+ } ) );
329
+
330
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
331
+ ...temp ];
332
+
333
+ if ( Array.isArray( record.mappingInfo ) ) {
334
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
335
+ return {
336
+ ...item,
337
+ status: item.type === 'approve'? 'Tango Review Done': item.type === 'tangoreview'? 'Open - Accuracy Issue': 'Closed',
338
+ };
339
+ } );
340
+ }
341
+
342
+ record.mappingInfo.push(
343
+ {
344
+ type: 'finalRevision',
345
+ mode: inputData.mappingInfo?.mode,
346
+ revicedFootfall: inputData.mappingInfo?.revisedFootfall,
347
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
348
+ reviced: parseInt( inputData.mappingInfo?.revicedPerc ),
349
+ count: inputData.mappingInfo?.count,
350
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
351
+ status: 'Closed',
352
+ createdByEmail: req?.user?.email,
353
+ createdByUserName: req?.user?.userName,
354
+ createdByRole: req?.user?.role,
355
+ createdAt: new Date(),
356
+ },
357
+
358
+
359
+ );
360
+ }
361
+ } else {
362
+ if ( Array.isArray( record.mappingInfo ) ) {
363
+ logger.info( { msg: '...........21', status: record.status } );
364
+ const temp = record.mappingInfo
365
+ .filter( ( item ) => item.type === 'tangoreview' )
366
+ .map( ( item ) => ( {
367
+ ...item,
368
+ mode: inputData.mappingInfo?.mode,
369
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
370
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
371
+ reviced: parseInt( inputData.mappingInfo?.revicedPerc ),
372
+ count: inputData.mappingInfo?.count,
373
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
374
+ status: 'Closed',
375
+ createdByEmail: req?.user?.email,
376
+ createdByUserName: req?.user?.userName,
377
+ createdByRole: req?.user?.role,
378
+ createdAt: new Date(),
379
+ } ) );
380
+
381
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
382
+ ...temp ];
383
+ if ( Array.isArray( record.mappingInfo ) ) {
384
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
385
+ return {
386
+ ...item,
387
+ status: item.type === 'approve'?'Tango Review Done': 'Closed',
388
+ };
389
+ } );
390
+ }
391
+ }
392
+ record.mappingInfo.push(
393
+ {
394
+ type: 'finalRevision',
395
+ mode: inputData.mappingInfo?.mode,
396
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
397
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
398
+ reviced: parseInt( inputData.mappingInfo?.revicedPerc ),
399
+ count: inputData.mappingInfo?.count,
400
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
401
+ status: 'Closed',
402
+ createdByEmail: req?.user?.email,
403
+ createdByUserName: req?.user?.userName,
404
+ createdByRole: req?.user?.role,
405
+ createdAt: new Date(),
406
+ },
407
+ );
408
+ }
409
+
410
+ if ( Ticket[0]?._source?.type==='store' ) {
411
+ let findTagging = Ticket[0]?._source?.mappingInfo.filter( ( data ) => data.type==='tagging' );
412
+ if ( findTagging?.length>0&&findTagging[0].createdByEmail!='' ) {
413
+ let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
414
+ let title = `Received response for the Footfall ticket raised.`;
415
+ let createdOn = dayjs( Ticket[0]?._source?.dateString ).format( 'DD MMM YYYY' );
416
+ let description = `Raised on ${createdOn}`;
417
+
418
+ let Data = {
419
+ 'title': title,
420
+ 'body': description,
421
+ 'type': 'closed',
422
+ 'date': Ticket[0]?._source?.dateString,
423
+ 'storeId': Ticket[0]?._source?.storeId,
424
+ 'clientId': Ticket[0]?._source?.clientId,
425
+ 'ticketId': Ticket[0]?._source?.ticketId,
426
+ };
427
+ if ( userData && userData.fcmToken ) {
428
+ const fcmToken = userData.fcmToken;
429
+ await sendPushNotification( title, description, fcmToken, Data );
430
+ }
431
+ }
432
+ }
433
+ // return;
434
+
435
+ let id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
436
+ let getExistingOne = await getOpenSearchById( openSearch.footfallDirectory, id );
437
+ if ( inputData.ticketType === 'internal' &&!getExistingOne?.body?._source ) {
438
+ id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
439
+ }
440
+
441
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
442
+
443
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
444
+ if ( ( record.status === 'Closed' || record.status === 'Open - Accuracy Issue' ) && inputData.ticketType !== 'internal' ) {
445
+ const query = {
446
+ storeId: inputData?.storeId,
447
+ isVideoStream: true,
448
+ };
449
+ const getStoreType = await countDocumnetsCamera( query );
450
+
451
+ // Get all tempIds from revopInfo response
452
+ const temp = [];
453
+ inputData?.mappingInfo?.revisedDetail?.map( ( hit ) => temp.push( { tempId: hit?.tempId } ) ) || [];
454
+ logger.info( { msg: '...........22', temp, revisedDetails: inputData?.revisedDetail } );
455
+ // Prepare management eyeZone query based on storeId and dateString
456
+
457
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
458
+ if ( isSendMessge === true ) {
459
+ logger.info( '....1' );
460
+ }
461
+ }
462
+ return res.sendSuccess( 'Ticket closed successfully' );
463
+ } else {
464
+ return res.sendError( 'Internal Server Error', 500 );
465
+ }
466
+ } catch ( error ) {
467
+ const err = error.message || 'Internal Server Error';
468
+ logger.error( { error: error, funtion: 'tangoReviewTicket' } );
469
+ return res.sendError( err, 500 );
470
+ }
471
+ }
472
+
473
+ export async function tangoReviewAccuracyClosedTicket( req, res ) {
474
+ try {
475
+ const inputData = req.body;
476
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
477
+
478
+ let findQuery = {
479
+ size: 10000,
480
+ query: {
481
+ bool: {
482
+ must: [
483
+ {
484
+ term: {
485
+ 'storeId.keyword': inputData.storeId,
486
+ },
487
+ },
488
+ {
489
+ term: {
490
+ 'dateString': inputData.dateString,
491
+ },
492
+ },
493
+ ],
494
+ },
495
+ },
496
+ };
497
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
498
+ let Ticket = findTicket.body?.hits?.hits;
499
+
500
+ if ( Ticket.length === 0 ) {
501
+ return res.sendError( 'Ticket not found', 400 );
502
+ }
503
+ const getTicket = {
504
+ size: 10000,
505
+ query: {
506
+ bool: {
507
+ must: [
508
+ {
509
+ term: {
510
+ 'storeId.keyword': inputData.storeId,
511
+ },
512
+ },
513
+ {
514
+ term: {
515
+ 'dateString': inputData.dateString,
516
+ },
517
+ },
518
+
519
+ ],
520
+ },
521
+ },
522
+ };
523
+ if ( Ticket[0]?._source?.type !== 'internal' ) {
524
+ getTicket.query.bool.must.push(
525
+ {
526
+ nested: {
527
+ path: 'mappingInfo',
528
+ query: {
529
+ bool: {
530
+ must: [
531
+ {
532
+ term: {
533
+ 'mappingInfo.type': 'tangoreview',
534
+ },
535
+ },
536
+
537
+ ],
538
+ },
539
+ },
540
+ },
541
+ },
542
+ );
543
+ }
544
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
545
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
546
+
547
+ if ( !ticketData || ticketData?.length == 0 ) {
548
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
549
+ }
550
+
551
+ const record = {
552
+
553
+ status: 'Closed - Accuracy Issue',
554
+ createdByEmail: req?.user?.email,
555
+ createdByUserName: req?.user?.userName,
556
+ createdByRole: req?.user?.role,
557
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
558
+ };
559
+
560
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
561
+
562
+ if ( Array.isArray( record.mappingInfo ) ) {
563
+ const temp = record.mappingInfo
564
+ .filter( ( item ) => item.type === 'tangoreview' )
565
+ .map( ( item ) => ( {
566
+ ...item,
567
+ status: 'Closed - Accuracy Issue',
568
+ createdByEmail: req?.user?.email,
569
+ createdByUserName: req?.user?.userName,
570
+ createdByRole: req?.user?.role,
571
+ comments: inputData?.comments || '',
572
+ subComments: inputData?.subComments ||'',
573
+ } ) );
574
+
575
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
576
+ ...temp ];
577
+ // if ( Array.isArray( record.mappingInfo ) ) {
578
+ // record.mappingInfo = record.mappingInfo.map( ( item ) => {
579
+ // return {
580
+ // ...item,
581
+ // status: 'Closed',
582
+ // };
583
+ // } );
584
+ // }
585
+
586
+ record.mappingInfo.push(
587
+ {
588
+ type: 'finalRevision',
589
+ mode: 'web',
590
+ revicedFootfall: temp?.[0]?.revicedFootfall,
591
+ revicedPerc: temp?.[0].revicedPerc,
592
+ count: temp?.[0].count,
593
+ revisedDetail: temp?.[0]?.revisedDetail,
594
+ status: 'Closed',
595
+ createdByEmail: req?.user?.email,
596
+ createdByUserName: req?.user?.userName,
597
+ createdByRole: req?.user?.role,
598
+ createdAt: new Date(),
599
+ comments: inputData?.comments || '',
600
+ subComments: inputData?.subComments || '',
601
+ },
602
+ );
603
+ }
604
+
605
+
606
+ // return;
607
+
608
+ let id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
609
+
610
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
611
+
612
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
613
+ const query = {
614
+ storeId: inputData?.storeId,
615
+ isVideoStream: true,
616
+ };
617
+ const getStoreType = await countDocumnetsCamera( query );
618
+ const revopInfoQuery = {
619
+ size: 10000,
620
+ query: {
621
+ bool: {
622
+ must: [
623
+ {
624
+ term: {
625
+ 'storeId.keyword': inputData.storeId,
626
+ },
627
+ },
628
+ {
629
+ term: {
630
+ 'dateString': inputData.dateString,
631
+ },
632
+ },
633
+ {
634
+ term: {
635
+ 'isParent': false,
636
+ },
637
+ },
638
+ {
639
+ term: {
640
+ isChecked: true,
641
+ },
642
+ },
643
+ ],
644
+ },
645
+ },
646
+ _source: [ 'tempId' ],
647
+
648
+ };
649
+ const revopInfo = await getOpenSearchData( openSearch.revop, revopInfoQuery );
650
+ // Get all tempIds from revopInfo response
651
+ const tempIds = revopInfo?.body?.hits?.hits?.map( ( hit ) => hit?._source?.tempId ).filter( Boolean ) || [];
652
+ // Prepare management eyeZone query based on storeId and dateString
653
+ const managerEyeZoneQuery = {
654
+ size: 1,
655
+ query: {
656
+ bool: {
657
+ must: [
658
+ {
659
+ term: {
660
+ 'storeId.keyword': inputData.storeId,
661
+ },
662
+ },
663
+ {
664
+ term: {
665
+ 'storeDate': inputData.dateString,
666
+ },
667
+ },
668
+ ],
669
+ },
670
+ },
671
+ _source: [ 'originalToTrackerCustomerMapping' ],
672
+ };
673
+
674
+ // Query the managerEyeZone index for the matching document
675
+ const managerEyeZoneResp = await getOpenSearchData( openSearch.managerEyeZone, managerEyeZoneQuery );
676
+ const managerEyeZoneHit = managerEyeZoneResp?.body?.hits?.hits?.[0]?._source;
677
+ // Extract originalToTrackerCustomerMapping if it exists
678
+ const mapping =
679
+ managerEyeZoneHit && managerEyeZoneHit.originalToTrackerCustomerMapping ?
680
+ managerEyeZoneHit.originalToTrackerCustomerMapping :
681
+ {};
682
+
683
+ // Find tempIds that exist in both revopInfo results and manager mapping
684
+ const temp = [];
685
+ tempIds.filter( ( tid ) => mapping[tid] !== null ? temp.push( { tempId: mapping[tid] } ) :'' );
686
+ const isSendMessge = await sendSqsMessage( inputData, temp, getStoreType, inputData.storeId );
687
+ if ( isSendMessge == true ) {
688
+ logger.info( '....1' );
689
+ }
690
+
691
+ return res.sendSuccess( 'Ticket closed successfully' );
692
+ } else {
693
+ return res.sendError( 'Internal Server Error', 500 );
694
+ }
695
+ } catch ( error ) {
696
+ const err = error.message || 'Internal Server Error';
697
+ logger.error( { error: error, funtion: 'tangoReviewAccuracyClosedTicket' } );
698
+ return res.sendError( err, 500 );
699
+ }
700
+ }
701
+
63
702
  async function bulkUpdateStatusToPending( indexName, inputData ) {
64
703
  const bulkBody = [];
65
704
 
@@ -195,7 +834,6 @@ export async function ticketSummary1( req, res ) {
195
834
 
196
835
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
197
836
  const aggs = getData?.body?.aggregations;
198
- logger.info( { aggs: aggs, body: getData?.body } );
199
837
 
200
838
  const result = {
201
839
  totalTickets: aggs.totalTicketCount.value,
@@ -218,426 +856,2426 @@ export async function ticketSummary1( req, res ) {
218
856
 
219
857
  export async function ticketSummary( req, res ) {
220
858
  try {
859
+ const inputData = req.query;
860
+
221
861
  let result = '';
862
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
222
863
  const userInfo = req.user;
223
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
224
-
225
- if ( req.user.userType =='tango' ) {
226
- result ={
227
- totalTickets: 0,
228
- averageAccuracyOverAll: 0,
229
- openTickets: 0,
230
- openInfraIssues: 0,
231
- inprogress: 0,
232
- closedTickets: 0,
233
- ticketAccuracyAbove: '0%',
234
- ticketAccuracyBelow: '0%',
235
- };
236
- } else {
237
- result = req.user.role === 'superadmin'?
238
- {
239
- totalTickets: 0,
240
- openTickets: 0,
241
- inprogress: 0,
242
- closedTickets: 0,
243
- dueToday: 0,
244
- Expired: 0,
245
- underTangoReview: 0,
246
- avgTicket: '0%',
247
- avgAccuracy: '0%',
248
- } :
249
- req.user.role === 'user'? 'NA':
250
- ticketsFeature?
251
- {
252
- totalTickets: 0,
253
- openTickets: 0,
254
- inprogress: 0,
255
- closedTickets: 0,
256
- dueToday: 0,
257
- Expired: 0,
258
- avgTicket: '0%',
259
- avgAccuracy: '0%',
260
- }: 'NA';
864
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
865
+ const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
866
+ const getConfig = await findOneClient( { clientId: inputData?.clientId }, { footfallDirectoryConfigs: 1 } );
867
+ if ( !getConfig || getConfig ==null ) {
868
+ return res.sendError( 'this client not configured against footfall directory', 400 );
261
869
  }
870
+ inputData.clientId = inputData?.clientId?.split( ',' );
871
+ // const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
872
+ if ( req?.user?.userType === 'tango' ) {
873
+ switch ( inputData?.tangoType ) {
874
+ case 'store':
875
+ const storeQuery = {
876
+ size: 0,
877
+ query: {
878
+ bool: {
879
+ must: [
880
+ {
881
+ 'range': {
882
+ 'dateString': {
883
+ 'gte': inputData?.fromDate,
884
+ 'lte': inputData?.toDate,
885
+ 'format': 'yyyy-MM-dd',
886
+ },
887
+ },
888
+ },
262
889
 
263
- return res.sendSuccess( { result: result } );
264
- } catch ( error ) {
265
- const err = error.message || 'Internal Server Error';
266
- logger.error( { error: error, messgage: req.query } );
267
- return res.sendSuccess( err, 500 );
268
- }
269
- }
890
+ {
891
+ terms: {
892
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
893
+ inputData.clientId :
894
+ [ inputData.clientId ],
895
+ },
270
896
 
271
- export async function ticketList1( req, res ) {
272
- try {
273
- const openSearch = JSON.parse( process.env.OPENSEARCH );
274
- const inputData = req.query;
275
- const limit = inputData.limit || 10;
276
- const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
277
- const order = inputData?.sortOrder || -1;
897
+ },
278
898
 
279
- inputData.clientId = inputData.clientId.split( ',' ); // convert strig to array
899
+ {
900
+ range: {
901
+ reviced: {
902
+ lt: parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview ),
903
+ }
904
+ ,
905
+ },
906
+ },
907
+ {
908
+ nested: {
909
+ path: 'mappingInfo',
910
+ query: {
911
+ bool: {
912
+ must: [
913
+ {
914
+ term: {
915
+ 'mappingInfo.type': 'tangoreview',
916
+ },
917
+ },
918
+ ],
919
+ },
920
+ },
921
+ },
922
+ },
280
923
 
924
+ ],
925
+ },
926
+ },
927
+ };
928
+
929
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
930
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
931
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
932
+ // Remove any previous mappingInfo.status term
933
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
934
+ if ( nested ) {
935
+ // filter out all mappingInfo.status
936
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
937
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
938
+ } );
939
+ // add desired status
940
+ nested.nested.query.bool.must.push( {
941
+ term: {
942
+ 'mappingInfo.status': statusValue,
943
+ },
944
+ } );
945
+ }
946
+ return q;
947
+ }
281
948
 
282
- let filter = [
283
- {
284
- 'range': {
285
- 'dateString': {
286
- 'gte': inputData.fromDate,
287
- 'lte': inputData.toDate,
949
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
950
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
951
+
952
+ // locate nested section
953
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
954
+
955
+ if ( nested ) {
956
+ // remove old status filters
957
+ nested.nested.query.bool.must =
958
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
959
+
960
+ // add new filters
961
+ nested.nested.query.bool.must.push( ...filters );
962
+ }
963
+
964
+ return {
965
+ ...q,
966
+ size: 0,
967
+ aggs: {
968
+ avg_value: {
969
+ avg: 'reviced',
970
+ },
971
+ },
972
+ };
973
+ };
974
+
975
+
976
+ // Get OpenSearch connection
977
+
978
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
979
+
980
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
981
+ let totalTickets = 0;
982
+
983
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
984
+
985
+ allQuery.size = 0;
986
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
987
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
988
+
989
+ // openTickets: mappingInfo.status: 'Open'
990
+ let openTickets = 0;
991
+
992
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
993
+ otQ.size = 0;
994
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
995
+ openTickets = openResp?.body?.hits?.total?.value || 0;
996
+ // logger.info( { msd: '..............2', openResp } );
997
+
998
+
999
+ // openInfraIssues: mappingInfo.status: 'Open Accuracy Issue'
1000
+ let openInfraIssues = 0;
1001
+
1002
+ let oiQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open - Accuracy Issue' );
1003
+ oiQ.size = 0;
1004
+ const infraResp = await getOpenSearchData( openSearch.footfallDirectory, oiQ );
1005
+ openInfraIssues = infraResp?.body?.hits?.total?.value || 0;
1006
+ // inprogress: mappingInfo.status: 'in-Progress'
1007
+ let inprogress = 0;
1008
+
1009
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
1010
+ ipQ.size = 0;
1011
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
1012
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
1013
+
1014
+ // closedTickets: mappingInfo.status: 'closed'
1015
+ let closedTickets = 0;
1016
+
1017
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
1018
+ clQ.size = 0;
1019
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
1020
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
1021
+ // Average revisedPerc (for all tangoreview)
1022
+ let averageAccuracyOverAll = 0;
1023
+
1024
+ let avgQ = buildAggStoreQuery( baseStoreQuery );
1025
+ const avgResp = await getOpenSearchData( openSearch.footfallDirectory, avgQ );
1026
+ averageAccuracyOverAll = avgResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1027
+
1028
+ // ticketAccuracyAbove: avg of revicedPerc > 85%
1029
+ let ticketAccuracyAbove = 0;
1030
+
1031
+
1032
+ // For this, add a filter on revicedPerc >= 85
1033
+ let aboveQ = buildAggStoreQuery( baseStoreQuery, [
1034
+ {
1035
+ range: {
1036
+ reviced: {
1037
+ gte: parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview ),
1038
+ },
1039
+ },
1040
+ },
1041
+
1042
+
1043
+ ] );
1044
+ const aboveResp = await getOpenSearchData( openSearch.footfallDirectory, aboveQ );
1045
+ ticketAccuracyAbove = aboveResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1046
+
1047
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1048
+ let ticketAccuracyBelow = 0;
1049
+
1050
+ let belowQ = buildAggStoreQuery( baseStoreQuery, [
1051
+ {
1052
+ range: {
1053
+ reviced: {
1054
+ lt: parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview ),
1055
+ },
1056
+ },
1057
+ },
1058
+
1059
+ ] );
1060
+ const belowResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
1061
+ ticketAccuracyBelow = belowResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1062
+
1063
+ // Final result object
1064
+ result = {
1065
+ totalTickets,
1066
+ averageAccuracyOverAll: averageAccuracyOverAll+'%',
1067
+ openTickets,
1068
+ openInfraIssues,
1069
+ inprogress,
1070
+ closedTickets,
1071
+ ticketAccuracyAbove: ticketAccuracyAbove+'%',
1072
+ ticketAccuracyBelow: ticketAccuracyBelow+'%',
1073
+ };
1074
+ break;
1075
+ case 'internal':
1076
+ const internalQuery = {
1077
+ size: 0,
1078
+ query: {
1079
+ bool: {
1080
+ must: [
1081
+ {
1082
+ 'range': {
1083
+ 'dateString': {
1084
+ 'gte': inputData?.fromDate,
1085
+ 'lte': inputData?.toDate,
1086
+ 'format': 'yyyy-MM-dd',
1087
+ },
1088
+ },
1089
+ },
1090
+ {
1091
+ terms: {
1092
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1093
+ inputData.clientId :
1094
+ [ inputData.clientId ],
1095
+ },
1096
+
1097
+ },
1098
+
1099
+ ],
1100
+ },
1101
+ },
1102
+ };
1103
+
1104
+ const internalOpen = {
1105
+ size: 0,
1106
+ query: {
1107
+ bool: {
1108
+ must: [
1109
+ {
1110
+ 'range': {
1111
+ 'dateString': {
1112
+ 'gte': inputData?.fromDate,
1113
+ 'lte': inputData?.toDate,
1114
+ 'format': 'yyyy-MM-dd',
1115
+ },
1116
+ },
1117
+ },
1118
+ {
1119
+ terms: {
1120
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1121
+ inputData.clientId :
1122
+ [ inputData.clientId ],
1123
+ },
1124
+
1125
+ },
1126
+ {
1127
+ terms: {
1128
+ 'status.keyword': [ 'Open', 'Raised' ],
1129
+ },
1130
+
1131
+ },
1132
+
1133
+ ],
1134
+ },
1135
+ },
1136
+ };
1137
+
1138
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
1139
+ function buildInternalQueryWithStatus( baseQuery, statusValue ) {
1140
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
1141
+ // Check if nested query exists, if not create it
1142
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
1143
+ if ( !nested ) {
1144
+ // Create nested query structure
1145
+ nested = {
1146
+ nested: {
1147
+ path: 'mappingInfo',
1148
+ query: {
1149
+ bool: {
1150
+ must: [],
1151
+ },
1152
+ },
1153
+ },
1154
+ };
1155
+ q.query.bool.must.push( nested );
1156
+ }
1157
+ // filter out all mappingInfo.status
1158
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
1159
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
1160
+ } );
1161
+ // add desired status
1162
+ nested.nested.query.bool.must.push( {
1163
+ term: {
1164
+ 'mappingInfo.status': statusValue,
1165
+ },
1166
+ } );
1167
+ return q;
1168
+ }
1169
+
1170
+ const buildAggInternalQuery = ( baseQuery, filters = [] ) => {
1171
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1172
+
1173
+ // locate nested section
1174
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1175
+
1176
+ if ( nested ) {
1177
+ // remove old status filters
1178
+ nested.nested.query.bool.must =
1179
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1180
+
1181
+ // add new filters
1182
+ q.query.bool.must.push( ...filters );
1183
+ }
1184
+
1185
+ return {
1186
+ ...q,
1187
+ size: 0,
1188
+ aggs: {
1189
+ avg_value: {
1190
+ avg: {
1191
+ field: 'reviced',
1192
+ },
1193
+ },
1194
+ },
1195
+ };
1196
+ };
1197
+
1198
+
1199
+ // Get OpenSearch connection
1200
+
1201
+
1202
+ const baseInternalQuery = JSON.parse( JSON.stringify( internalQuery ) );
1203
+
1204
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1205
+ let totalInternalTickets = 0;
1206
+
1207
+ let allInternalQuery = JSON.parse( JSON.stringify( baseInternalQuery ) );
1208
+
1209
+ allInternalQuery.size = 0;
1210
+ const totalInternalResp = await getOpenSearchData( openSearch.footfallDirectory, allInternalQuery );
1211
+ totalInternalTickets = totalInternalResp?.body?.hits?.total?.value || 0;
1212
+ logger.info( { totalInternalResp } );
1213
+
1214
+ // openTickets: mappingInfo.status: 'Open'
1215
+ let openInternalTickets = 0;
1216
+
1217
+ // let otQInternal = buildInternalQueryWithStatus( baseInternalQuery, 'Open' );
1218
+ // otQInternal.size = 0;
1219
+ const openInternalResp = await getOpenSearchData( openSearch.footfallDirectory, internalOpen );
1220
+ openInternalTickets = openInternalResp?.body?.hits?.total?.value || 0;
1221
+ // logger.info( { msd: '..............2', openResp } );
1222
+
1223
+
1224
+ // openInfraIssues: mappingInfo.status: 'Open Accuracy Issue'
1225
+ let openInternalInfraIssues = 0;
1226
+
1227
+ let oiQinternal = buildInternalQueryWithStatus( baseInternalQuery, 'Open - Accuracy Issue' );
1228
+ oiQinternal.size = 0;
1229
+ const infraInternalResp = await getOpenSearchData( openSearch.footfallDirectory, oiQinternal );
1230
+ openInternalInfraIssues = infraInternalResp?.body?.hits?.total?.value || 0;
1231
+
1232
+
1233
+ // inprogress: mappingInfo.status: 'in-Progress'
1234
+ let inprogressIntrenal = 0;
1235
+
1236
+ let ipQInternal = buildInternalQueryWithStatus( baseInternalQuery, 'In-Progress' );
1237
+ ipQInternal.size = 0;
1238
+ const ipInternalResp = await getOpenSearchData( openSearch.footfallDirectory, ipQInternal );
1239
+ inprogressIntrenal = ipInternalResp?.body?.hits?.total?.value || 0;
1240
+
1241
+ // closedTickets: mappingInfo.status: 'closed'
1242
+ let closedInternalTickets = 0;
1243
+
1244
+ let clQInternal = buildInternalQueryWithStatus( baseInternalQuery, 'Closed' );
1245
+ clQInternal.size = 0;
1246
+ const clInternalResp = await getOpenSearchData( openSearch.footfallDirectory, clQInternal );
1247
+ closedInternalTickets = clInternalResp?.body?.hits?.total?.value || 0;
1248
+ // Average revisedPerc (for all tangoreview)
1249
+ let internalAverageAccuracyOverAll = 0;
1250
+
1251
+ let avgQInternal = buildAggInternalQuery( baseInternalQuery );
1252
+ const avgInternalResp = await getOpenSearchData( openSearch.footfallDirectory, avgQInternal );
1253
+ internalAverageAccuracyOverAll = avgInternalResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1254
+
1255
+ // ticketAccuracyAbove: avg of revicedPerc > 85%
1256
+ let internalTicketAccuracyAbove = 0;
1257
+
1258
+
1259
+ // For this, add a filter on revicedPerc >= 85
1260
+ let aboveQinternal = buildAggInternalQuery( baseInternalQuery, [
1261
+ {
1262
+ range: {
1263
+ reviced: {
1264
+ gte: parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview ),
1265
+ },
1266
+ },
1267
+ },
1268
+
1269
+
1270
+ ] );
1271
+ const aboveRespInternal = await getOpenSearchData( openSearch.footfallDirectory, aboveQinternal );
1272
+ internalTicketAccuracyAbove = aboveRespInternal?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1273
+
1274
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1275
+ let internalTicketAccuracyBelow = 0;
1276
+
1277
+ let belowQIneranl = buildAggInternalQuery( baseInternalQuery, [
1278
+ {
1279
+ range: {
1280
+ reviced: {
1281
+ lt: parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview ),
1282
+ },
1283
+ },
1284
+ },
1285
+
1286
+ ] );
1287
+ const belowRespInternal = await getOpenSearchData( openSearch.footfallDirectory, belowQIneranl );
1288
+ internalTicketAccuracyBelow = belowRespInternal?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1289
+
1290
+ // Final result object
1291
+ result = {
1292
+ totalTickets: totalInternalTickets,
1293
+ averageAccuracyOverAll: internalAverageAccuracyOverAll+'%',
1294
+ openTickets: openInternalTickets,
1295
+ openInfraIssues: openInternalInfraIssues,
1296
+ inprogress: inprogressIntrenal,
1297
+ closedTickets: closedInternalTickets,
1298
+ ticketAccuracyAbove: internalTicketAccuracyAbove+'%',
1299
+ ticketAccuracyBelow: internalTicketAccuracyBelow+'%',
1300
+ };
1301
+ break;
1302
+ default: '';
1303
+ }
1304
+ } else if ( req?.user?.userType !== 'tango' ) {
1305
+ if ( ticketsFeature && !ticketsApproveFeature ) {
1306
+ const storeQuery = {
1307
+ size: 0,
1308
+ query: {
1309
+ bool: {
1310
+ must: [
1311
+ {
1312
+ 'range': {
1313
+ 'dateString': {
1314
+ 'gte': inputData?.fromDate,
1315
+ 'lte': inputData?.toDate,
1316
+ 'format': 'yyyy-MM-dd',
1317
+ },
1318
+ },
1319
+ },
1320
+ {
1321
+ terms: {
1322
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1323
+ inputData.clientId :
1324
+ [ inputData.clientId ],
1325
+ },
1326
+
1327
+ },
1328
+ {
1329
+ nested: {
1330
+ path: 'mappingInfo',
1331
+ query: {
1332
+ bool: {
1333
+ must: [
1334
+ {
1335
+ term: {
1336
+ 'mappingInfo.type': 'review',
1337
+ },
1338
+ },
1339
+ ],
1340
+ },
1341
+ },
1342
+ },
1343
+ },
1344
+
1345
+ ],
1346
+ },
1347
+ },
1348
+ };
1349
+
1350
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
1351
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
1352
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
1353
+ // Remove any previous mappingInfo.status term
1354
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
1355
+ if ( nested ) {
1356
+ // filter out all mappingInfo.status
1357
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
1358
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
1359
+ } );
1360
+ // add desired status
1361
+ nested.nested.query.bool.must.push( {
1362
+ term: {
1363
+ 'mappingInfo.status': statusValue,
1364
+ },
1365
+ } );
1366
+ }
1367
+ return q;
1368
+ }
1369
+
1370
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
1371
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1372
+
1373
+ // locate nested section
1374
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1375
+
1376
+ if ( nested ) {
1377
+ // remove old status filters
1378
+ nested.nested.query.bool.must =
1379
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1380
+
1381
+ // add new filters
1382
+ nested.nested.query.bool.must.push( ...filters );
1383
+ }
1384
+
1385
+ return {
1386
+ ...q,
1387
+ size: 0,
1388
+ aggs: {
1389
+ avg_value: {
1390
+ avg: 'mappingInfo.reviced',
1391
+ },
1392
+ },
1393
+ };
1394
+ };
1395
+
1396
+
1397
+ // Get OpenSearch connection
1398
+
1399
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
1400
+
1401
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1402
+ let totalTickets = 0;
1403
+
1404
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1405
+
1406
+ allQuery.size = 0;
1407
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
1408
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
1409
+
1410
+ // openTickets: mappingInfo.status: 'Open'
1411
+ let openTickets = 0;
1412
+
1413
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
1414
+ otQ.size = 0;
1415
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
1416
+ openTickets = openResp?.body?.hits?.total?.value || 0;
1417
+ // logger.info( { msd: '..............2', openResp } );
1418
+
1419
+
1420
+ // inprogress: mappingInfo.status: 'in-Progress'
1421
+ let inprogress = 0;
1422
+
1423
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
1424
+ ipQ.size = 0;
1425
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
1426
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
1427
+
1428
+ // closedTickets: mappingInfo.status: 'closed'
1429
+ let closedTickets = 0;
1430
+
1431
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
1432
+ clQ.size = 0;
1433
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
1434
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
1435
+
1436
+ let dueToday = 0;
1437
+ let todayDateString = new Date().toISOString().slice( 0, 10 );
1438
+
1439
+ // Build a query for tickets with mappingInfo.dueDate == todayDateString
1440
+ let dueTodayQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1441
+ // Locate nested mappingInfo query
1442
+ let nestedDue = dueTodayQuery.query.bool.must.find( ( m ) => m.nested );
1443
+ if ( nestedDue ) {
1444
+ // Remove any previous mappingInfo.dueDate term
1445
+ nestedDue.nested.query.bool.must = nestedDue.nested.query.bool.must.filter(
1446
+ ( mustItem ) => !( mustItem.term && mustItem.term['mappingInfo.dueDate'] ),
1447
+ );
1448
+ // Add new dueDate filter
1449
+ nestedDue.nested.query.bool.must.push( {
1450
+ term: { 'mappingInfo.dueDate': todayDateString },
1451
+ } );
1452
+ }
1453
+ dueTodayQuery.size = 0;
1454
+ const dueTodayResp = await getOpenSearchData( openSearch.footfallDirectory, dueTodayQuery );
1455
+ dueToday = dueTodayResp?.body?.hits?.total?.value || 0;
1456
+
1457
+ // filter expired Tickets
1458
+ let expiredTickets = 0;
1459
+
1460
+ let eQ = buildStoreQueryWithStatus( baseStoreQuery, 'Under Tango Review' );
1461
+ eQ.size = 0;
1462
+ const eResp = await getOpenSearchData( openSearch.footfallDirectory, eQ );
1463
+ expiredTickets = eResp?.body?.hits?.total?.value || 0;
1464
+
1465
+ // Calculate average ticket percentage: avg((reviced/footfallCount)*100) filtered by baseStoreQuery
1466
+
1467
+ // Build aggregation query for ticket percentage
1468
+ let ticketPercentageAvg = 0;
1469
+
1470
+ let avgTicketPercentageQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1471
+ avgTicketPercentageQuery.size = 0;
1472
+ avgTicketPercentageQuery.aggs = {
1473
+ avg_ticket_percentage: {
1474
+ avg: {
1475
+ script: {
1476
+ source: `
1477
+ if (doc.containsKey('reviced') && doc['reviced'].size()!=0 &&
1478
+ doc.containsKey('footfallCount') && doc['footfallCount'].size()!=0 && doc['footfallCount'].value != 0) {
1479
+ return (doc['reviced'].value / doc['footfallCount'].value) * 100;
1480
+ } else {
1481
+ return null;
1482
+ }
1483
+ `,
1484
+ lang: 'painless',
1485
+ },
1486
+ },
1487
+ },
1488
+ };
1489
+
1490
+ const avgTicketPercentageResp = await getOpenSearchData( openSearch.footfallDirectory, avgTicketPercentageQuery );
1491
+
1492
+ ticketPercentageAvg = avgTicketPercentageResp?.body?.aggregations?.avg_ticket_percentage?.value?.toFixed( 2 ) || '0';
1493
+
1494
+ logger.info( { avgTicketPercentageResp } );
1495
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1496
+ let ticketAccuracy = 0;
1497
+
1498
+ let belowQ = buildAggStoreQuery( baseStoreQuery );
1499
+ logger.info( { belowQ } );
1500
+ const accuracyResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
1501
+ ticketAccuracy = accuracyResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1502
+ logger.info( { accuracyResp } );
1503
+ // Final result object
1504
+ result = {
1505
+ totalTickets,
1506
+ openTickets,
1507
+ inprogress,
1508
+ closedTickets,
1509
+ dueToday: dueToday,
1510
+ Expired: expiredTickets,
1511
+ avgTicket: ticketPercentageAvg+'%',
1512
+ avgAccuracy: ticketAccuracy+'%',
1513
+ };
1514
+ } else if ( ticketsFeature && ticketsApproveFeature ) {
1515
+ if ( inputData?.permissionType === 'review' ) {
1516
+ const storeQuery = {
1517
+ size: 0,
1518
+ query: {
1519
+ bool: {
1520
+ must: [
1521
+ {
1522
+ 'range': {
1523
+ 'dateString': {
1524
+ 'gte': inputData?.fromDate,
1525
+ 'lte': inputData?.toDate,
1526
+ 'format': 'yyyy-MM-dd',
1527
+ },
1528
+ },
1529
+ },
1530
+ {
1531
+ terms: {
1532
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1533
+ inputData.clientId :
1534
+ [ inputData.clientId ],
1535
+ },
1536
+
1537
+ },
1538
+ {
1539
+ nested: {
1540
+ path: 'mappingInfo',
1541
+ query: {
1542
+ bool: {
1543
+ must: [
1544
+ {
1545
+ term: {
1546
+ 'mappingInfo.type': 'review',
1547
+ },
1548
+ },
1549
+ ],
1550
+ },
1551
+ },
1552
+ },
1553
+ },
1554
+
1555
+ ],
1556
+ },
1557
+ },
1558
+ };
1559
+
1560
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
1561
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
1562
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
1563
+ // Remove any previous mappingInfo.status term
1564
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
1565
+ if ( nested ) {
1566
+ // filter out all mappingInfo.status
1567
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
1568
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
1569
+ } );
1570
+ // add desired status
1571
+ nested.nested.query.bool.must.push( {
1572
+ term: {
1573
+ 'mappingInfo.status': statusValue,
1574
+ },
1575
+ } );
1576
+ }
1577
+ return q;
1578
+ }
1579
+
1580
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
1581
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1582
+
1583
+ // locate nested section
1584
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1585
+
1586
+ if ( nested ) {
1587
+ // remove old status filters
1588
+ nested.nested.query.bool.must =
1589
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1590
+
1591
+ // add new filters
1592
+ nested.nested.query.bool.must.push( ...filters );
1593
+ }
1594
+
1595
+ return {
1596
+ ...q,
1597
+ size: 0,
1598
+ aggs: {
1599
+ avg_value: {
1600
+ avg: 'mappingInfo.reviced',
1601
+ },
1602
+ },
1603
+ };
1604
+ };
1605
+
1606
+
1607
+ // Get OpenSearch connection
1608
+
1609
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
1610
+
1611
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1612
+ let totalTickets = 0;
1613
+
1614
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1615
+
1616
+ allQuery.size = 0;
1617
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
1618
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
1619
+
1620
+ // openTickets: mappingInfo.status: 'Open'
1621
+ let openTickets = 0;
1622
+
1623
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
1624
+ otQ.size = 0;
1625
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
1626
+ openTickets = openResp?.body?.hits?.total?.value || 0;
1627
+ // logger.info( { msd: '..............2', openResp } );
1628
+
1629
+
1630
+ // inprogress: mappingInfo.status: 'in-Progress'
1631
+ let inprogress = 0;
1632
+
1633
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
1634
+ ipQ.size = 0;
1635
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
1636
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
1637
+
1638
+ // closedTickets: mappingInfo.status: 'closed'
1639
+ let closedTickets = 0;
1640
+
1641
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
1642
+ clQ.size = 0;
1643
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
1644
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
1645
+
1646
+ let dueToday = 0;
1647
+ let todayDateString = new Date().toISOString().slice( 0, 10 );
1648
+
1649
+ // Build a query for tickets with mappingInfo.dueDate == todayDateString
1650
+ let dueTodayQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1651
+ // Locate nested mappingInfo query
1652
+ let nestedDue = dueTodayQuery.query.bool.must.find( ( m ) => m.nested );
1653
+ if ( nestedDue ) {
1654
+ // Remove any previous mappingInfo.dueDate term
1655
+ nestedDue.nested.query.bool.must = nestedDue.nested.query.bool.must.filter(
1656
+ ( mustItem ) => !( mustItem.term && mustItem.term['mappingInfo.dueDate'] ),
1657
+ );
1658
+ // Add new dueDate filter
1659
+ nestedDue.nested.query.bool.must.push( {
1660
+ term: { 'mappingInfo.dueDate': todayDateString },
1661
+ } );
1662
+ }
1663
+ dueTodayQuery.size = 0;
1664
+ const dueTodayResp = await getOpenSearchData( openSearch.footfallDirectory, dueTodayQuery );
1665
+ dueToday = dueTodayResp?.body?.hits?.total?.value || 0;
1666
+
1667
+ // filter expired Tickets
1668
+ let expiredTickets = 0;
1669
+
1670
+ let eQ = buildStoreQueryWithStatus( baseStoreQuery, 'Under Tango Review' );
1671
+ eQ.size = 0;
1672
+ const eResp = await getOpenSearchData( openSearch.footfallDirectory, eQ );
1673
+ expiredTickets = eResp?.body?.hits?.total?.value || 0;
1674
+
1675
+ // Calculate average ticket percentage: avg((reviced/footfallCount)*100) filtered by baseStoreQuery
1676
+
1677
+ // Build aggregation query for ticket percentage
1678
+ let ticketPercentageAvg = 0;
1679
+
1680
+ let avgTicketPercentageQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1681
+ avgTicketPercentageQuery.size = 0;
1682
+ avgTicketPercentageQuery.aggs = {
1683
+ avg_ticket_percentage: {
1684
+ avg: {
1685
+ script: {
1686
+ source: `
1687
+ if (doc.containsKey('reviced') && doc['reviced'].size()!=0 &&
1688
+ doc.containsKey('footfallCount') && doc['footfallCount'].size()!=0 && doc['footfallCount'].value != 0) {
1689
+ return (doc['reviced'].value / doc['footfallCount'].value) * 100;
1690
+ } else {
1691
+ return null;
1692
+ }
1693
+ `,
1694
+ lang: 'painless',
1695
+ },
1696
+ },
1697
+ },
1698
+ };
1699
+
1700
+ const avgTicketPercentageResp = await getOpenSearchData( openSearch.footfallDirectory, avgTicketPercentageQuery );
1701
+
1702
+ ticketPercentageAvg = avgTicketPercentageResp?.body?.aggregations?.avg_ticket_percentage?.value?.toFixed( 2 ) || '0';
1703
+
1704
+ logger.info( { avgTicketPercentageResp } );
1705
+ // ticketAccuracyBelow: avg of revicedPerc < 85%
1706
+ let ticketAccuracy = 0;
1707
+
1708
+ let belowQ = buildAggStoreQuery( baseStoreQuery );
1709
+ logger.info( { belowQ } );
1710
+ const accuracyResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
1711
+ ticketAccuracy = accuracyResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1712
+ logger.info( { accuracyResp } );
1713
+ // Final result object
1714
+ result = {
1715
+ totalTickets,
1716
+ openTickets,
1717
+ inprogress,
1718
+ closedTickets,
1719
+ dueToday: dueToday,
1720
+ Expired: expiredTickets,
1721
+ avgTicket: ticketPercentageAvg+'%',
1722
+ avgAccuracy: ticketAccuracy+'%',
1723
+ };
1724
+ } else {
1725
+ const storeQuery = {
1726
+ size: 0,
1727
+ query: {
1728
+ bool: {
1729
+ must: [
1730
+ {
1731
+ 'range': {
1732
+ 'dateString': {
1733
+ 'gte': inputData?.fromDate,
1734
+ 'lte': inputData?.toDate,
1735
+ 'format': 'yyyy-MM-dd',
1736
+ },
1737
+ },
1738
+ },
1739
+ {
1740
+ terms: {
1741
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1742
+ inputData.clientId :
1743
+ [ inputData.clientId ],
1744
+ },
1745
+
1746
+ },
1747
+ {
1748
+ nested: {
1749
+ path: 'mappingInfo',
1750
+ query: {
1751
+ bool: {
1752
+ must: [
1753
+ {
1754
+ term: {
1755
+ 'mappingInfo.type': inputData.permissionType === 'review'? 'review':'approve',
1756
+ },
1757
+ },
1758
+ ],
1759
+ },
1760
+ },
1761
+ },
1762
+ },
1763
+
1764
+ ],
1765
+ },
1766
+ },
1767
+ };
1768
+
1769
+ // Helper function to clone deep and replace mappingInfo.status for openTickets/closed/etc
1770
+ function buildStoreQueryWithStatus( baseQuery, statusValue ) {
1771
+ let q = JSON.parse( JSON.stringify( baseQuery ) );
1772
+ // Remove any previous mappingInfo.status term
1773
+ let nested = q.query.bool.must.find( ( m ) => m.nested );
1774
+ if ( nested ) {
1775
+ // filter out all mappingInfo.status
1776
+ nested.nested.query.bool.must = nested?.nested?.query?.bool?.must.filter( ( mustItem ) => {
1777
+ return !( mustItem.term && mustItem.term['mappingInfo.status'] );
1778
+ } );
1779
+ // add desired status
1780
+ nested.nested.query.bool.must.push( {
1781
+ term: {
1782
+ 'mappingInfo.status': statusValue,
1783
+ },
1784
+ } );
1785
+ }
1786
+ return q;
1787
+ }
1788
+
1789
+ const buildAggStoreQuery = ( baseQuery, filters = [] ) => {
1790
+ const q = JSON.parse( JSON.stringify( baseQuery ) );
1791
+
1792
+ // locate nested section
1793
+ const nested = q.query.bool.must.find( ( m ) => m.nested );
1794
+
1795
+ if ( nested ) {
1796
+ // remove old status filters
1797
+ nested.nested.query.bool.must =
1798
+ nested.nested.query.bool.must.filter( ( m ) => !( m.term && m.term['mappingInfo.status'] ) );
1799
+
1800
+ // add new filters
1801
+ nested.nested.query.bool.must.push( ...filters );
1802
+ }
1803
+
1804
+ return {
1805
+ ...q,
1806
+ size: 0,
1807
+ aggs: {
1808
+ avg_value: {
1809
+ avg: 'mappingInfp.reviced',
1810
+ },
1811
+ },
1812
+ };
1813
+ };
1814
+
1815
+
1816
+ // Get OpenSearch connection
1817
+
1818
+ const baseStoreQuery = JSON.parse( JSON.stringify( storeQuery ) );
1819
+
1820
+ // Total Tickets (all tickets with mappingInfo.type == tangoreview)
1821
+ let totalTickets = 0;
1822
+
1823
+ let allQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1824
+
1825
+ allQuery.size = 0;
1826
+ const totalResp = await getOpenSearchData( openSearch.footfallDirectory, allQuery );
1827
+ totalTickets = totalResp?.body?.hits?.total?.value || 0;
1828
+
1829
+ // openTickets: mappingInfo.status: 'Open'
1830
+ let openTickets = 0;
1831
+
1832
+ let otQ = buildStoreQueryWithStatus( baseStoreQuery, 'Open' );
1833
+ otQ.size = 0;
1834
+ const openResp = await getOpenSearchData( openSearch.footfallDirectory, otQ );
1835
+ openTickets = openResp?.body?.hits?.total?.value || 0;
1836
+ // logger.info( { msd: '..............2', openResp } );
1837
+
1838
+
1839
+ // inprogress: mappingInfo.status: 'in-Progress'
1840
+ let inprogress = 0;
1841
+
1842
+ let ipQ = buildStoreQueryWithStatus( baseStoreQuery, 'In-Progress' );
1843
+ ipQ.size = 0;
1844
+ const ipResp = await getOpenSearchData( openSearch.footfallDirectory, ipQ );
1845
+ inprogress = ipResp?.body?.hits?.total?.value || 0;
1846
+
1847
+ // closedTickets: mappingInfo.status: 'closed'
1848
+ let closedTickets = 0;
1849
+
1850
+ let clQ = buildStoreQueryWithStatus( baseStoreQuery, 'Closed' );
1851
+ clQ.size = 0;
1852
+ const clResp = await getOpenSearchData( openSearch.footfallDirectory, clQ );
1853
+ closedTickets = clResp?.body?.hits?.total?.value || 0;
1854
+ // dueToday: Tickets whose dueDate is today (format 'yyyy-MM-dd')
1855
+ let dueToday = 0;
1856
+ let todayDateString = new Date().toISOString().slice( 0, 10 );
1857
+
1858
+ // Build a query for tickets with mappingInfo.dueDate == todayDateString
1859
+ let dueTodayQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1860
+ // Locate nested mappingInfo query
1861
+ let nestedDue = dueTodayQuery.query.bool.must.find( ( m ) => m.nested );
1862
+ if ( nestedDue ) {
1863
+ // Remove any previous mappingInfo.dueDate term
1864
+ nestedDue.nested.query.bool.must = nestedDue.nested.query.bool.must.filter(
1865
+ ( mustItem ) => !( mustItem.term && mustItem.term['mappingInfo.dueDate'] ),
1866
+ );
1867
+ // Add new dueDate filter
1868
+ nestedDue.nested.query.bool.must.push( {
1869
+ term: { 'mappingInfo.dueDate': todayDateString },
1870
+ } );
1871
+ }
1872
+ dueTodayQuery.size = 0;
1873
+ const dueTodayResp = await getOpenSearchData( openSearch.footfallDirectory, dueTodayQuery );
1874
+ dueToday = dueTodayResp?.body?.hits?.total?.value || 0;
1875
+
1876
+
1877
+ // filter expired Tickets
1878
+ let expiredTickets = 0;
1879
+
1880
+ let eQ = buildStoreQueryWithStatus( baseStoreQuery, 'Under Tango Review' );
1881
+ eQ.size = 0;
1882
+ const eResp = await getOpenSearchData( openSearch.footfallDirectory, eQ );
1883
+ expiredTickets = eResp?.body?.hits?.total?.value || 0;
1884
+
1885
+ // filter under tango review
1886
+
1887
+ let undertangoTickets = 0;
1888
+
1889
+ let utrQ = buildStoreQueryWithStatus( baseStoreQuery, 'Under Tango Review' );
1890
+ utrQ.size = 0;
1891
+ const utrResp = await getOpenSearchData( openSearch.footfallDirectory, utrQ );
1892
+ undertangoTickets = utrResp?.body?.hits?.total?.value || 0;
1893
+
1894
+ let ticketPercentageAvg =0;
1895
+ let avgTicketPercentageQuery = JSON.parse( JSON.stringify( baseStoreQuery ) );
1896
+ avgTicketPercentageQuery.size = 0;
1897
+ avgTicketPercentageQuery.aggs = {
1898
+ avg_ticket_percentage: {
1899
+ avg: {
1900
+ script: {
1901
+ source: `
1902
+ if (doc.containsKey('reviced') && doc['reviced'].size()!=0 &&
1903
+ doc.containsKey('footfallCount') && doc['footfallCount'].size()!=0 && doc['footfallCount'].value != 0) {
1904
+ return (doc['reviced'].value / doc['footfallCount'].value) * 100;
1905
+ } else {
1906
+ return null;
1907
+ }
1908
+ `,
1909
+ lang: 'painless',
1910
+ },
1911
+ },
1912
+ },
1913
+ };
1914
+
1915
+ const avgTicketPercentageResp = await getOpenSearchData( openSearch.footfallDirectory, avgTicketPercentageQuery );
1916
+
1917
+ ticketPercentageAvg = avgTicketPercentageResp?.body?.aggregations?.avg_ticket_percentage?.value?.toFixed( 2 ) || '0';
1918
+ logger.info( { avgTicketPercentageResp } );
1919
+ let ticketAccuracy = 0;
1920
+
1921
+ let belowQ = buildAggStoreQuery( baseStoreQuery );
1922
+ logger.info( { belowQ } );
1923
+ const accuracyResp = await getOpenSearchData( openSearch.footfallDirectory, belowQ );
1924
+ ticketAccuracy = accuracyResp?.body?.aggregations?.avg_value?.value?.toFixed( 2 ) || '0';
1925
+ logger.info( { accuracyResp } );
1926
+ // Final result object
1927
+ result = {
1928
+ totalTickets,
1929
+ openTickets,
1930
+ inprogress,
1931
+ closedTickets,
1932
+ dueToday: dueToday,
1933
+ Expired: expiredTickets,
1934
+ underTangoReview: undertangoTickets,
1935
+ avgTicket: ticketPercentageAvg+'%',
1936
+ avgAccuracy: ticketAccuracy+'%',
1937
+ };
1938
+ }
1939
+ }
1940
+ }
1941
+
1942
+ return res.sendSuccess( { result: result } );
1943
+ } catch ( error ) {
1944
+ const err = error.message || 'Internal Server Error';
1945
+ logger.error( { error: error, messgage: req.query } );
1946
+ return res.sendSuccess( err, 500 );
1947
+ }
1948
+ }
1949
+
1950
+ export async function ticketList1( req, res ) {
1951
+ try {
1952
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1953
+ const inputData = req.query;
1954
+ const limit =inputData?.isExport ? 10000: inputData.limit || 10;
1955
+ const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
1956
+ const order = inputData?.sortOrder || -1;
1957
+
1958
+ inputData.clientId = inputData.clientId.split( ',' ); // convert strig to array
1959
+
1960
+
1961
+ let filter = [
1962
+ {
1963
+ 'range': {
1964
+ 'dateString': {
1965
+ 'gte': inputData.fromDate,
1966
+ 'lte': inputData.toDate,
288
1967
  'format': 'yyyy-MM-dd',
289
1968
  },
290
- },
291
- },
292
- {
293
- terms: {
294
- 'clientId.keyword': Array.isArray( inputData.clientId ) ?
295
- inputData.clientId :
296
- [ inputData.clientId ],
297
- },
298
- },
299
- ];
1969
+ },
1970
+ },
1971
+ {
1972
+ terms: {
1973
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1974
+ inputData.clientId :
1975
+ [ inputData.clientId ],
1976
+ },
1977
+ },
1978
+ ];
1979
+
1980
+ if ( inputData?.storeId ) {
1981
+ filter.push(
1982
+ {
1983
+ terms: {
1984
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
1985
+ inputData.storeId :
1986
+ [ inputData.storeId ],
1987
+ },
1988
+ },
1989
+ );
1990
+ }
1991
+
1992
+ let search = {
1993
+ 'must': filter,
1994
+ };
1995
+
1996
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
1997
+ search = {
1998
+ 'must': filter,
1999
+ 'should': [
2000
+ {
2001
+ 'wildcard': {
2002
+ 'storeName.keyword': {
2003
+ 'value': `*${inputData.searchValue}*`,
2004
+ },
2005
+ },
2006
+ },
2007
+ {
2008
+ 'wildcard': {
2009
+ 'storeId.keyword': {
2010
+ 'value': `*${inputData.searchValue}*`,
2011
+ },
2012
+ },
2013
+ },
2014
+ {
2015
+ 'wildcard': {
2016
+ 'ticketId.keyword': {
2017
+ 'value': `*${inputData.searchValue}*`,
2018
+ },
2019
+ },
2020
+ },
2021
+ {
2022
+ 'wildcard': {
2023
+ 'status.keyword': {
2024
+ 'value': `*${inputData.searchValue}*`,
2025
+ },
2026
+ },
2027
+ },
2028
+
2029
+ ],
2030
+ 'minimum_should_match': 1,
2031
+ };
2032
+ }
2033
+
2034
+ let searchQuery = {
2035
+ '_source': [
2036
+ 'storeName',
2037
+ 'storeId',
2038
+ 'ticketId',
2039
+ 'createdAt',
2040
+ 'updatedAt',
2041
+ 'footfallCount',
2042
+ 'duplicateCount',
2043
+ 'employeeCount',
2044
+ 'houseKeepingCount',
2045
+ 'junkCount',
2046
+ 'employeeACCount',
2047
+ 'duplicateACCount',
2048
+ 'houseKeepingACCount',
2049
+ 'junkACCount',
2050
+ 'status',
2051
+ 'dateString',
2052
+ ],
2053
+ 'from': offset,
2054
+ 'size': limit,
2055
+ 'query': {
2056
+ 'bool': search,
2057
+ },
2058
+ 'sort': [
2059
+ { dateString: { order: 'desc' } },
2060
+ ],
2061
+ };
2062
+
2063
+ if ( inputData.sortBy && inputData.sortBy !== '' ) {
2064
+ let sortByValue = '';
2065
+
2066
+ if ( [ 'storeName', 'storeId', 'ticketId', 'status' ].includes( inputData?.sortBy ) ) {
2067
+ sortByValue = `${inputData.sortBy}.keyword`;
2068
+ } else {
2069
+ sortByValue = inputData.sortBy;
2070
+ }
2071
+
2072
+
2073
+ searchQuery = {
2074
+ '_source': [
2075
+ 'storeName',
2076
+ 'storeId',
2077
+ 'ticketId',
2078
+ 'createdAt',
2079
+ 'updatedAt',
2080
+ 'footfallCount',
2081
+ 'duplicateCount',
2082
+ 'employeeCount',
2083
+ 'houseKeepingCount',
2084
+ 'junkCount',
2085
+ 'status',
2086
+ 'employeeACCount',
2087
+ 'duplicateACCount',
2088
+ 'houseKeepingACCount',
2089
+ 'junkACCount',
2090
+ 'dateString',
2091
+ ],
2092
+ 'from': offset,
2093
+ 'size': limit,
2094
+ 'query': {
2095
+ 'bool': search,
2096
+ },
2097
+ 'sort': [
2098
+ { [sortByValue]: { order: order === -1 ? 'desc' : 'asc' } },
2099
+ ],
2100
+ };
2101
+ }
2102
+
2103
+ if ( inputData.isExport == true ) {
2104
+ searchQuery = {
2105
+ '_source': [
2106
+ 'storeName',
2107
+ 'storeId',
2108
+ 'ticketId',
2109
+ 'createdAt',
2110
+ 'updatedAt',
2111
+ 'footfallCount',
2112
+ 'duplicateCount',
2113
+ 'employeeACCount',
2114
+ 'duplicateACCount',
2115
+ 'employeeCount',
2116
+ 'houseKeepingACCount',
2117
+ 'houseKeepingCount',
2118
+ 'junkCount',
2119
+ 'junkACCount',
2120
+ 'status',
2121
+ 'dateString',
2122
+ ],
2123
+ 'from': 0,
2124
+ 'size': 10000,
2125
+ 'query': {
2126
+ 'bool': search,
2127
+ },
2128
+ 'sort': [
2129
+ { 'storeName.keyword': { order: 'desc' } },
2130
+ ],
2131
+ };
2132
+ }
2133
+ const getData = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
2134
+ const count = getData?.body?.hits?.total?.value;
2135
+ if ( !count || count == 0 ) {
2136
+ return res.sendError( 'No data found', 204 );
2137
+ }
2138
+ const searchValue = getData?.body?.hits?.hits;
2139
+ if ( !searchValue || searchValue?.length == 0 ) {
2140
+ return res.sendError( 'No data found', 204 );
2141
+ }
2142
+
2143
+ if ( inputData.isExport == true ) {
2144
+ const exportData = [];
2145
+ for ( const item of searchValue ) {
2146
+ exportData.push( {
2147
+ 'Store Name': item._source.storeName || '--',
2148
+ 'Store ID': item._source.storeId,
2149
+ 'Ticket raised on': dayjs( item._source.createdAt ).format( 'DD MMM, YYYY' ),
2150
+ 'Issue Date': dayjs( item._source.dateString ).format( 'DD MMM, YYYY' ),
2151
+ 'Total Footfalls': item._source.footfallCount,
2152
+ 'Duplicates': item?._source?.status === 'closed' ? item._source.duplicateACCount : item._source.duplicateCount,
2153
+ 'Employee/Staff': item?._source?.status === 'closed' ? item._source.employeeACCount : item._source.employeeCount,
2154
+ 'HouseKeeping': item?._source?.status === 'closed' ? item._source.houseKeepingACCount : item._source.houseKeepingCount,
2155
+ 'Junk': item?._source?.status === 'closed' ? ( item._source.junkACCount || 0 ) : ( item._source.junkCount || 0 ),
2156
+ 'Revised Footfall': item?._source?.status === 'closed' ? item._source.footfallCount - ( item._source.duplicateACCount + item._source.employeeACCount + item._source.houseKeepingACCount + ( item._source.junkACCount || 0 ) ) : item._source.footfallCount - ( item._source.duplicateCount + item._source.employeeCount + item._source.houseKeepingCount + ( item._source.junkCount || 0 ) ),
2157
+ 'Ticket%': item?._source?.status === 'closed' ? `${Math.round( ( ( item._source.duplicateACCount + item._source.employeeACCount + item._source.houseKeepingACCount + ( item?._source?.junkACCount || 0 ) ) / item._source.footfallCount ) * 100 ).toFixed( 0 )} %` : `${Math.round( ( ( item?._source?.duplicateCount + item?._source?.employeeCount + item?._source?.houseKeepingCount + ( item?._source?.junkCount || 0 ) ) / item?._source?.footfallCount ) * 100 ).toFixed( 0 )} %`,
2158
+ 'Status': item._source.status,
2159
+ } );
2160
+ }
2161
+ return await download( exportData, res );
2162
+ }
2163
+ return res.sendSuccess( { result: searchValue, count: count } );
2164
+ } catch ( error ) {
2165
+ const err = error.message || 'Internal Server Error';
2166
+ logger.error( { error: error, messgage: req.query } );
2167
+ return res.sendSuccess( err, 500 );
2168
+ }
2169
+ }
2170
+
2171
+ export async function ticketList( req, res ) {
2172
+ try {
2173
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
2174
+ const inputData = req.query;
2175
+ const userInfo = req.user;
2176
+ const limit =inputData?.isExport? 10000: inputData?.limit || 10;
2177
+ const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
2178
+ inputData.clientId = inputData?.clientId?.split( ',' ); // convert strig to array
2179
+
2180
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
2181
+
2182
+ const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
2183
+
2184
+ const searchQuery = {
2185
+
2186
+ size: limit, // or use parseInt(req.query.limit) for dynamic
2187
+ from: offset, // or use parseInt(req.query.offset) for dynamic
2188
+ sort: [ { 'createdAt': { order: 'desc' } } ],
2189
+
2190
+ query: {
2191
+ bool: {
2192
+ must: [
2193
+ {
2194
+ 'range': {
2195
+ 'dateString': {
2196
+ 'gte': inputData.fromDate,
2197
+ 'lte': inputData.toDate,
2198
+ 'format': 'yyyy-MM-dd',
2199
+ },
2200
+ },
2201
+ },
2202
+ {
2203
+ terms: {
2204
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
2205
+ inputData.clientId :
2206
+ [ inputData.clientId ],
2207
+ },
2208
+
2209
+ },
2210
+ ],
2211
+ },
2212
+ },
2213
+ };
2214
+
2215
+ if ( inputData.sortBy ) {
2216
+ let sortOrder = inputData.sortOrder === 1 ? 'asc' : 'desc';
2217
+
2218
+
2219
+ const stringKeywordFields = [ 'storeName', 'storeId', 'ticketId', 'status', 'type', 'clientId' ];
2220
+ let sortField = inputData.sortBy == 'footfall' ? 'footfallCount' : inputData.sortBy == 'issueDate' ? 'dateString' : inputData.sortBy;
2221
+ if ( stringKeywordFields.includes( sortField ) ) {
2222
+ sortField = `${sortField}.keyword`;
2223
+ }
2224
+
2225
+ // Remove default sort so we don't duplicate/conflict
2226
+ searchQuery.sort = [
2227
+ { [sortField]: { order: sortOrder } },
2228
+ ];
2229
+ }
2230
+ // Example: Filtering by storeId if present in the query
2231
+ if ( inputData.storeId ) {
2232
+ inputData.storeId = inputData?.storeId?.split( ',' );
2233
+ searchQuery.query.bool.must.push( {
2234
+ terms: {
2235
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
2236
+ inputData.storeId :
2237
+ [ inputData.storeId ],
2238
+ },
2239
+ } );
2240
+ }
2241
+ if ( inputData.status && inputData.status!=='' ) {
2242
+ inputData.status = inputData?.status?.split( ',' );
2243
+ if ( req.user.userType === 'tango' ) {
2244
+ searchQuery.query.bool.must.push( {
2245
+ terms: {
2246
+ 'status.keyword': Array.isArray( inputData?.status ) ?
2247
+ inputData?.status :
2248
+ [ inputData?.status ],
2249
+ },
2250
+ } );
2251
+ } else if ( inputData?.permissionType === 'approve' ) {
2252
+ searchQuery.query.bool.must.push( {
2253
+ nested: {
2254
+ path: 'mappingInfo',
2255
+ query: {
2256
+ bool: {
2257
+ must: [
2258
+ {
2259
+ term: {
2260
+ 'mappingInfo.type': 'approve',
2261
+ },
2262
+ },
2263
+ {
2264
+ term: {
2265
+ 'mappingInfo.status': inputData.status[0],
2266
+ },
2267
+ },
2268
+ ],
2269
+ },
2270
+ },
2271
+ },
2272
+ } );
2273
+ } else if ( inputData?.permissionType === 'review' ) {
2274
+ searchQuery.query.bool.must.push( {
2275
+ nested: {
2276
+ path: 'mappingInfo',
2277
+ query: {
2278
+ bool: {
2279
+ must: [
2280
+ {
2281
+ term: {
2282
+ 'mappingInfo.type': 'review',
2283
+ },
2284
+ },
2285
+ {
2286
+ term: {
2287
+ 'mappingInfo.status': inputData.status[0],
2288
+ },
2289
+ },
2290
+ ],
2291
+ },
2292
+ },
2293
+ },
2294
+ } );
2295
+ } else if ( ticketsFeature ) {
2296
+ searchQuery.query.bool.must.push( {
2297
+ nested: {
2298
+ path: 'mappingInfo',
2299
+ query: {
2300
+ bool: {
2301
+ must: [
2302
+ {
2303
+ term: {
2304
+ 'mappingInfo.type': 'review',
2305
+ },
2306
+ },
2307
+ {
2308
+ term: {
2309
+ 'mappingInfo.status': inputData.status[0],
2310
+ },
2311
+ },
2312
+ ],
2313
+ },
2314
+ },
2315
+ },
2316
+ } );
2317
+ } else if ( ticketsApproveFeature ) {
2318
+ searchQuery.query.bool.must.push( {
2319
+ nested: {
2320
+ path: 'mappingInfo',
2321
+ query: {
2322
+ bool: {
2323
+ must: [
2324
+ {
2325
+ term: {
2326
+ 'mappingInfo.type': 'approve',
2327
+ },
2328
+ },
2329
+ {
2330
+ term: {
2331
+ 'mappingInfo.status': inputData.status[0],
2332
+ },
2333
+ },
2334
+ ],
2335
+ },
2336
+ },
2337
+ },
2338
+ } );
2339
+ }
2340
+ }
2341
+
2342
+ if ( inputData.filterByStatus && inputData.filterByStatus!=='' ) {
2343
+ inputData.filterByStatus = inputData?.filterByStatus?.split( ',' );
2344
+
2345
+ if ( req?.user?.userType === 'tango' ) {
2346
+ {
2347
+ switch ( inputData?.tangoType ) {
2348
+ case 'store':
2349
+ searchQuery.query.bool.must.push( {
2350
+ nested: {
2351
+ path: 'mappingInfo',
2352
+ query: {
2353
+ bool: {
2354
+ must: [
2355
+ {
2356
+ term: {
2357
+ 'mappingInfo.type': 'tangoreview',
2358
+ },
2359
+ },
2360
+ {
2361
+ terms: {
2362
+ 'mappingInfo.status': inputData?.filterByStatus,
2363
+ },
2364
+ },
2365
+ ],
2366
+ },
2367
+ },
2368
+ },
2369
+ } );
2370
+ break;
2371
+ case 'internal':
2372
+ searchQuery.query.bool.must.push( {
2373
+ 'terms': {
2374
+ 'status.keyword': Array.isArray( inputData?.filterByStatus ) ?
2375
+ inputData?.filterByStatus :
2376
+ [ inputData?.filterByStatus ],
2377
+ },
2378
+ } );
2379
+ break;
2380
+ defaut:
2381
+ searchQuery.query.bool.must.push( {
2382
+ nested: {
2383
+ path: 'mappingInfo',
2384
+ query: {
2385
+ bool: {
2386
+ must: [
2387
+ {
2388
+ term: {
2389
+ 'mappingInfo.type': 'tangoreview',
2390
+ },
2391
+ },
2392
+ {
2393
+ terms: {
2394
+ 'mappingInfo.status': inputData?.filterByStatus,
2395
+ },
2396
+ },
2397
+ ],
2398
+ },
2399
+ },
2400
+ },
2401
+ } );
2402
+ }
2403
+ }
2404
+ } else if ( ticketsFeature && !ticketsApproveFeature ) {
2405
+ searchQuery.query.bool.must.push( {
2406
+ nested: {
2407
+ path: 'mappingInfo',
2408
+ query: {
2409
+ bool: {
2410
+ must: [
2411
+ {
2412
+ term: {
2413
+ 'mappingInfo.type': 'review',
2414
+ },
2415
+ },
2416
+ {
2417
+ terms: {
2418
+ 'mappingInfo.status': inputData?.filterByStatus,
2419
+ },
2420
+ },
2421
+ ],
2422
+ },
2423
+ },
2424
+ },
2425
+ } );
2426
+ } else if ( ticketsFeature && ticketsApproveFeature ) {
2427
+ switch ( inputData.permisisionType ) {
2428
+ case 'review':
2429
+ searchQuery.query.bool.must.push( {
2430
+ nested: {
2431
+ path: 'mappingInfo',
2432
+ query: {
2433
+ bool: {
2434
+ must: [
2435
+ {
2436
+ term: {
2437
+ 'mappingInfo.type': 'review',
2438
+ },
2439
+ },
2440
+ {
2441
+ terms: {
2442
+ 'mappingInfo.status': inputData?.filterByStatus,
2443
+ },
2444
+ },
2445
+ ],
2446
+ },
2447
+ },
2448
+ },
2449
+ } );
2450
+ break;
2451
+ case 'approve':
2452
+ searchQuery.query.bool.must.push( {
2453
+ nested: {
2454
+ path: 'mappingInfo',
2455
+ query: {
2456
+ bool: {
2457
+ must: [
2458
+ {
2459
+ term: {
2460
+ 'mappingInfo.type': 'approve',
2461
+ },
2462
+ },
2463
+ {
2464
+ terms: {
2465
+ 'mappingInfo.status': inputData?.filterByStatus,
2466
+ },
2467
+ },
2468
+ ],
2469
+ },
2470
+ },
2471
+ },
2472
+ } );
2473
+ default: searchQuery.query.bool.must.push( {
2474
+ nested: {
2475
+ path: 'mappingInfo',
2476
+ query: {
2477
+ bool: {
2478
+ must: [
2479
+ {
2480
+ term: {
2481
+ 'mappingInfo.type': 'approve',
2482
+ },
2483
+ },
2484
+ {
2485
+ terms: {
2486
+ 'mappingInfo.status': inputData?.filterByStatus,
2487
+ },
2488
+ },
2489
+ ],
2490
+ },
2491
+ },
2492
+ },
2493
+ } );
2494
+ }
2495
+ }
2496
+ }
2497
+
2498
+ if ( inputData?.filterByStore && inputData?.filterByStore !== '' ) {
2499
+ let percQuery = null;
2500
+ const value = inputData.filterByStore;
2501
+
2502
+ // Helper function: remove trailing '%' and convert to number
2503
+ const percValue = ( val ) => {
2504
+ if ( typeof val === 'string' ) {
2505
+ return parseFloat( val.replace( '%', '' ).trim() );
2506
+ }
2507
+ return parseFloat( val );
2508
+ };
2509
+
2510
+ // Example filter values: "<90", "<=90", ">=90", "50 to 90"
2511
+ if ( /^<=?\d+$/.test( value ) ) {
2512
+ // "<90" or "<=90"
2513
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2514
+ const op = value.includes( '=' ) ? 'lte' : 'lt';
2515
+ percQuery = {
2516
+ script: {
2517
+ script: {
2518
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'lt' ? '<' : '<='} params.num`,
2519
+ params: { num },
2520
+ },
2521
+ },
2522
+ };
2523
+ } else if ( /^>=?\d+$/.test( value ) ) {
2524
+ // ">=90"
2525
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2526
+ const op = value.includes( '=' ) ? 'gte' : 'gt';
2527
+ percQuery = {
2528
+ script: {
2529
+ script: {
2530
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'gt' ? '>' : '>='} params.num`,
2531
+ params: { num },
2532
+ },
2533
+ },
2534
+ };
2535
+ } else if ( /^(\d+)\s*to\s*(\d+)$/.test( value ) ) {
2536
+ // "50 to 90"
2537
+ const match = value.match( /^(\d+)\s*to\s*(\d+)$/ );
2538
+ const from = percValue( match[1] );
2539
+ const to = percValue( match[2] );
2540
+ percQuery = {
2541
+ script: {
2542
+ script: {
2543
+ source:
2544
+ `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) >= params.from && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) <= params.to`,
2545
+ params: { from, to },
2546
+ },
2547
+ },
2548
+ };
2549
+ }
2550
+ // fallback: treat as exact match (e.g., "90")
2551
+ if ( !percQuery && /^\d+$/.test( value ) ) {
2552
+ percQuery = {
2553
+ script: {
2554
+ script: {
2555
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) == params.num`,
2556
+ params: { num: percValue( value ) },
2557
+ },
2558
+ },
2559
+ };
2560
+ }
2561
+
2562
+ if ( percQuery ) {
2563
+ searchQuery.query.bool.must.push( {
2564
+ nested: {
2565
+ path: 'mappingInfo',
2566
+ query: {
2567
+ bool: {
2568
+ must: [
2569
+ { term: { 'mappingInfo.type': 'tagging' } },
2570
+ percQuery,
2571
+ ],
2572
+ },
2573
+ },
2574
+ },
2575
+ } );
2576
+ }
2577
+ }
2578
+
2579
+ if ( inputData?.filterByReviewer && inputData?.filterByReviewer !== '' ) {
2580
+ let percQuery = null;
2581
+ const value = inputData.filterByReviewer;
2582
+
2583
+ // Helper function: remove trailing '%' and convert to number
2584
+ const percValue = ( val ) => {
2585
+ if ( typeof val === 'string' ) {
2586
+ return parseFloat( val.replace( '%', '' ).trim() );
2587
+ }
2588
+ return parseFloat( val );
2589
+ };
2590
+
2591
+ // Example filter values: "<90", "<=90", ">=90", "50 to 90"
2592
+ if ( /^<=?\d+$/.test( value ) ) {
2593
+ // "<90" or "<=90"
2594
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2595
+ const op = value.includes( '=' ) ? 'lte' : 'lt';
2596
+ percQuery = {
2597
+ script: {
2598
+ script: {
2599
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'lt' ? '<' : '<='} params.num`,
2600
+ params: { num },
2601
+ },
2602
+ },
2603
+ };
2604
+ } else if ( /^>=?\d+$/.test( value ) ) {
2605
+ // ">=90"
2606
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2607
+ const op = value.includes( '=' ) ? 'gte' : 'gt';
2608
+ percQuery = {
2609
+ script: {
2610
+ script: {
2611
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'gt' ? '>' : '>='} params.num`,
2612
+ params: { num },
2613
+ },
2614
+ },
2615
+ };
2616
+ } else if ( /^(\d+)\s*to\s*(\d+)$/.test( value ) ) {
2617
+ // "50 to 90"
2618
+ const match = value.match( /^(\d+)\s*to\s*(\d+)$/ );
2619
+ const from = percValue( match[1] );
2620
+ const to = percValue( match[2] );
2621
+ percQuery = {
2622
+ script: {
2623
+ script: {
2624
+ source:
2625
+ `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) >= params.from && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) <= params.to`,
2626
+ params: { from, to },
2627
+ },
2628
+ },
2629
+ };
2630
+ }
2631
+ // fallback: treat as exact match (e.g., "90")
2632
+ if ( !percQuery && /^\d+$/.test( value ) ) {
2633
+ percQuery = {
2634
+ script: {
2635
+ script: {
2636
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) == params.num`,
2637
+ params: { num: percValue( value ) },
2638
+ },
2639
+ },
2640
+ };
2641
+ }
300
2642
 
301
- if ( inputData?.storeId ) {
302
- filter.push(
303
- {
304
- terms: {
305
- 'storeId.keyword': Array.isArray( inputData.storeId ) ?
306
- inputData.storeId :
307
- [ inputData.storeId ],
2643
+ if ( percQuery ) {
2644
+ searchQuery.query.bool.must.push( {
2645
+ nested: {
2646
+ path: 'mappingInfo',
2647
+ query: {
2648
+ bool: {
2649
+ must: [
2650
+ { term: { 'mappingInfo.type': 'review' } },
2651
+ percQuery,
2652
+ ],
2653
+ },
308
2654
  },
309
2655
  },
310
- );
2656
+ } );
2657
+ }
311
2658
  }
312
2659
 
313
- let search = {
314
- 'must': filter,
315
- };
2660
+ if ( inputData?.filterByApprover && inputData?.filterByApprover !== '' ) {
2661
+ let percQuery = null;
2662
+ const value = inputData.filterByApprover;
316
2663
 
317
- if ( inputData.searchValue && inputData.searchValue !== '' ) {
318
- search = {
319
- 'must': filter,
320
- 'should': [
321
- {
322
- 'wildcard': {
323
- 'storeName.keyword': {
324
- 'value': `*${inputData.searchValue}*`,
325
- },
2664
+ // Helper function: remove trailing '%' and convert to number
2665
+ const percValue = ( val ) => {
2666
+ if ( typeof val === 'string' ) {
2667
+ return parseFloat( val.replace( '%', '' ).trim() );
2668
+ }
2669
+ return parseFloat( val );
2670
+ };
2671
+
2672
+ // Example filter values: "<90", "<=90", ">=90", "50 to 90"
2673
+ if ( /^<=?\d+$/.test( value ) ) {
2674
+ // "<90" or "<=90"
2675
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2676
+ const op = value.includes( '=' ) ? 'lte' : 'lt';
2677
+ percQuery = {
2678
+ script: {
2679
+ script: {
2680
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'lt' ? '<' : '<='} params.num`,
2681
+ params: { num },
326
2682
  },
327
2683
  },
328
- {
329
- 'wildcard': {
330
- 'storeId.keyword': {
331
- 'value': `*${inputData.searchValue}*`,
332
- },
2684
+ };
2685
+ } else if ( /^>=?\d+$/.test( value ) ) {
2686
+ // ">=90"
2687
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2688
+ const op = value.includes( '=' ) ? 'gte' : 'gt';
2689
+ percQuery = {
2690
+ script: {
2691
+ script: {
2692
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'gt' ? '>' : '>='} params.num`,
2693
+ params: { num },
333
2694
  },
334
2695
  },
335
- {
336
- 'wildcard': {
337
- 'ticketId.keyword': {
338
- 'value': `*${inputData.searchValue}*`,
339
- },
2696
+ };
2697
+ } else if ( /^(\d+)\s*to\s*(\d+)$/.test( value ) ) {
2698
+ // "50 to 90"
2699
+ const match = value.match( /^(\d+)\s*to\s*(\d+)$/ );
2700
+ const from = percValue( match[1] );
2701
+ const to = percValue( match[2] );
2702
+ percQuery = {
2703
+ script: {
2704
+ script: {
2705
+ source:
2706
+ `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) >= params.from && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) <= params.to`,
2707
+ params: { from, to },
340
2708
  },
341
2709
  },
342
- {
343
- 'wildcard': {
344
- 'status.keyword': {
345
- 'value': `*${inputData.searchValue}*`,
346
- },
2710
+ };
2711
+ }
2712
+ // fallback: treat as exact match (e.g., "90")
2713
+ if ( !percQuery && /^\d+$/.test( value ) ) {
2714
+ percQuery = {
2715
+ script: {
2716
+ script: {
2717
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) == params.num`,
2718
+ params: { num: percValue( value ) },
347
2719
  },
348
2720
  },
2721
+ };
2722
+ }
349
2723
 
350
- ],
351
- 'minimum_should_match': 1,
352
- };
2724
+ if ( percQuery ) {
2725
+ searchQuery.query.bool.must.push( {
2726
+ nested: {
2727
+ path: 'mappingInfo',
2728
+ query: {
2729
+ bool: {
2730
+ must: [
2731
+ { term: { 'mappingInfo.type': 'approve' } },
2732
+ percQuery,
2733
+ ],
2734
+ },
2735
+ },
2736
+ },
2737
+ } );
2738
+ }
353
2739
  }
354
2740
 
355
- let searchQuery = {
356
- '_source': [
357
- 'storeName',
358
- 'storeId',
359
- 'ticketId',
360
- 'createdAt',
361
- 'updatedAt',
362
- 'footfallCount',
363
- 'duplicateCount',
364
- 'employeeCount',
365
- 'houseKeepingCount',
366
- 'junkCount',
367
- 'employeeACCount',
368
- 'duplicateACCount',
369
- 'houseKeepingACCount',
370
- 'junkACCount',
371
- 'status',
372
- 'dateString',
373
- ],
374
- 'from': offset,
375
- 'size': limit,
376
- 'query': {
377
- 'bool': search,
378
- },
379
- 'sort': [
380
- { dateString: { order: 'desc' } },
381
- ],
382
- };
2741
+ if ( inputData?.filterByTango && inputData?.filterByTango !== '' ) {
2742
+ let percQuery = null;
2743
+ const value = inputData.filterByTango;
383
2744
 
384
- if ( inputData.sortBy && inputData.sortBy !== '' ) {
385
- let sortByValue = '';
2745
+ // Helper function: remove trailing '%' and convert to number
2746
+ const percValue = ( val ) => {
2747
+ if ( typeof val === 'string' ) {
2748
+ return parseFloat( val.replace( '%', '' ).trim() );
2749
+ }
2750
+ return parseFloat( val );
2751
+ };
386
2752
 
387
- if ( [ 'storeName', 'storeId', 'ticketId', 'status' ].includes( inputData?.sortBy ) ) {
388
- sortByValue = `${inputData.sortBy}.keyword`;
389
- } else {
390
- sortByValue = inputData.sortBy;
2753
+ // Example filter values: "<90", "<=90", ">=90", "50 to 90"
2754
+ if ( /^<=?\d+$/.test( value ) ) {
2755
+ // "<90" or "<=90"
2756
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2757
+ const op = value.includes( '=' ) ? 'lte' : 'lt';
2758
+ percQuery = {
2759
+ script: {
2760
+ script: {
2761
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'lt' ? '<' : '<='} params.num`,
2762
+ params: { num },
2763
+ },
2764
+ },
2765
+ };
2766
+ } else if ( /^>=?\d+$/.test( value ) ) {
2767
+ // ">=90"
2768
+ const num = percValue( value.replace( /[^\d]/g, '' ) );
2769
+ const op = value.includes( '=' ) ? 'gte' : 'gt';
2770
+ percQuery = {
2771
+ script: {
2772
+ script: {
2773
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) ${op === 'gt' ? '>' : '>='} params.num`,
2774
+ params: { num },
2775
+ },
2776
+ },
2777
+ };
2778
+ } else if ( /^(\d+)\s*to\s*(\d+)$/.test( value ) ) {
2779
+ // "50 to 90"
2780
+ const match = value.match( /^(\d+)\s*to\s*(\d+)$/ );
2781
+ const from = percValue( match[1] );
2782
+ const to = percValue( match[2] );
2783
+ percQuery = {
2784
+ script: {
2785
+ script: {
2786
+ source:
2787
+ `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) >= params.from && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) <= params.to`,
2788
+ params: { from, to },
2789
+ },
2790
+ },
2791
+ };
2792
+ }
2793
+ // fallback: treat as exact match (e.g., "90")
2794
+ if ( !percQuery && /^\d+$/.test( value ) ) {
2795
+ percQuery = {
2796
+ script: {
2797
+ script: {
2798
+ source: `doc['mappingInfo.revicedPerc.keyword'].size()!=0 && Integer.parseInt(doc['mappingInfo.revicedPerc.keyword'].value.replace('%','')) == params.num`,
2799
+ params: { num: percValue( value ) },
2800
+ },
2801
+ },
2802
+ };
391
2803
  }
392
2804
 
2805
+ if ( percQuery ) {
2806
+ searchQuery.query.bool.must.push( {
2807
+ nested: {
2808
+ path: 'mappingInfo',
2809
+ query: {
2810
+ bool: {
2811
+ must: [
2812
+ { term: { 'mappingInfo.type': 'tangoreview' } },
2813
+ percQuery,
2814
+ ],
2815
+ },
2816
+ },
2817
+ },
2818
+ } );
2819
+ }
2820
+ }
393
2821
 
394
- searchQuery = {
395
- '_source': [
396
- 'storeName',
397
- 'storeId',
398
- 'ticketId',
399
- 'createdAt',
400
- 'updatedAt',
401
- 'footfallCount',
402
- 'duplicateCount',
403
- 'employeeCount',
404
- 'houseKeepingCount',
405
- 'junkCount',
406
- 'status',
407
- 'employeeACCount',
408
- 'duplicateACCount',
409
- 'houseKeepingACCount',
410
- 'junkACCount',
411
- 'dateString',
412
- ],
413
- 'from': offset,
414
- 'size': limit,
415
- 'query': {
416
- 'bool': search,
2822
+ if ( inputData?.filterByReviewedBy && inputData?.filterByReviewedBy !== '' ) {
2823
+ inputData.filterByReviewedBy = inputData?.filterByReviewedBy?.split( ',' );
2824
+ searchQuery.query.bool.must.push( {
2825
+ nested: {
2826
+ path: 'mappingInfo',
2827
+ query: {
2828
+ bool: {
2829
+ must: [
2830
+ { term: { 'mappingInfo.type': 'review' } },
2831
+ { terms: { 'mappingInfo.createdByEmail': inputData?.filterByReviewedBy } },
2832
+
2833
+ ],
2834
+ },
2835
+ },
417
2836
  },
418
- 'sort': [
419
- { [sortByValue]: { order: order === -1 ? 'desc' : 'asc' } },
420
- ],
421
- };
2837
+ } );
422
2838
  }
423
2839
 
424
- if ( inputData.isExport == true ) {
425
- searchQuery = {
426
- '_source': [
427
- 'storeName',
428
- 'storeId',
429
- 'ticketId',
430
- 'createdAt',
431
- 'updatedAt',
432
- 'footfallCount',
433
- 'duplicateCount',
434
- 'employeeACCount',
435
- 'duplicateACCount',
436
- 'employeeCount',
437
- 'houseKeepingACCount',
438
- 'houseKeepingCount',
439
- 'junkCount',
440
- 'junkACCount',
441
- 'status',
442
- 'dateString',
443
- ],
444
- 'from': 0,
445
- 'size': 10000,
446
- 'query': {
447
- 'bool': search,
2840
+ if ( inputData?.fileterByApprovedBy && inputData?.fileterByApprovedBy !== '' ) {
2841
+ inputData.fileterByApprovedBy = inputData?.fileterByApprovedBy?.split( ',' );
2842
+ searchQuery.query.bool.must.push( {
2843
+ nested: {
2844
+ path: 'mappingInfo',
2845
+ query: {
2846
+ bool: {
2847
+ must: [
2848
+ { term: { 'mappingInfo.type': 'approve' } },
2849
+ { terms: { 'mappingInfo.createdByEmail': inputData?.fileterByApprovedBy } },
2850
+
2851
+ ],
2852
+ },
2853
+ },
448
2854
  },
449
- 'sort': [
450
- { 'storeName.keyword': { order: 'desc' } },
451
- ],
452
- };
453
- }
454
- const getData = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
455
- const count = getData?.body?.hits?.total?.value;
456
- if ( !count || count == 0 ) {
457
- return res.sendError( 'No data found', 204 );
458
- }
459
- const searchValue = getData?.body?.hits?.hits;
460
- if ( !searchValue || searchValue?.length == 0 ) {
461
- return res.sendError( 'No data found', 204 );
2855
+ } );
462
2856
  }
463
2857
 
464
- if ( inputData.isExport == true ) {
465
- const exportData = [];
466
- for ( const item of searchValue ) {
467
- exportData.push( {
468
- 'Store Name': item._source.storeName || '--',
469
- 'Store ID': item._source.storeId,
470
- 'Ticket raised on': dayjs( item._source.createdAt ).format( 'DD MMM, YYYY' ),
471
- 'Issue Date': dayjs( item._source.dateString ).format( 'DD MMM, YYYY' ),
472
- 'Total Footfalls': item._source.footfallCount,
473
- 'Duplicates': item?._source?.status === 'closed'? item._source.duplicateACCount : item._source.duplicateCount,
474
- 'Employee/Staff': item?._source?.status === 'closed'? item._source.employeeACCount : item._source.employeeCount,
475
- 'HouseKeeping': item?._source?.status === 'closed'? item._source.houseKeepingACCount : item._source.houseKeepingCount,
476
- 'Junk': item?._source?.status === 'closed'? ( item._source.junkACCount || 0 ) : ( item._source.junkCount|| 0 ),
477
- 'Revised Footfall': item?._source?.status === 'closed'? item._source.footfallCount - ( item._source.duplicateACCount + item._source.employeeACCount + item._source.houseKeepingACCount +( item._source.junkACCount || 0 ) ) : item._source.footfallCount - ( item._source.duplicateCount + item._source.employeeCount + item._source.houseKeepingCount + ( item._source.junkCount || 0 ) ),
478
- 'Ticket%': item?._source?.status === 'closed'?`${Math.round( ( ( item._source.duplicateACCount + item._source.employeeACCount + item._source.houseKeepingACCount + ( item?._source?.junkACCount || 0 ) )/ item._source.footfallCount )*100 ).toFixed( 0 )} %` : `${Math.round( ( ( item?._source?.duplicateCount + item?._source?.employeeCount + item?._source?.houseKeepingCount + ( item?._source?.junkCount || 0 ) )/ item?._source?.footfallCount )*100 ).toFixed( 0 )} %`,
479
- 'Status': item._source.status,
480
- } );
481
- }
482
- return await download( exportData, res );
483
- }
484
- return res.sendSuccess( { result: searchValue, count: count } );
485
- } catch ( error ) {
486
- const err = error.message || 'Internal Server Error';
487
- logger.error( { error: error, messgage: req.query } );
488
- return res.sendSuccess( err, 500 );
489
- }
490
- }
2858
+ if ( req?.user?.userType === 'tango' && inputData.tangoType !== 'internal' ) {
2859
+ searchQuery.query.bool.must.push(
2860
+ {
2861
+ term: {
2862
+ 'type.keyword': 'store',
2863
+ },
2864
+ },
2865
+ {
2866
+ range: {
2867
+ reviced: {
2868
+ lt: 85,
2869
+ },
2870
+ },
2871
+ },
2872
+ {
2873
+ nested: {
2874
+ path: 'mappingInfo',
2875
+ query: {
2876
+ bool: {
2877
+ must: [
2878
+ {
2879
+ term: {
2880
+ 'mappingInfo.type': 'tangoreview',
2881
+ },
2882
+ },
2883
+
2884
+ ],
2885
+ },
2886
+ },
2887
+ },
491
2888
 
492
- export async function ticketList( req, res ) {
493
- try {
494
- const openSearch = JSON.parse( process.env.OPENSEARCH );
495
- const inputData= req.query;
496
- const userInfo = req.user;
497
- const limit =inputData?.limit || 10;
498
- const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
2889
+ },
2890
+
2891
+
2892
+ );
2893
+ } else if ( req?.user?.userType === 'client' ) {
2894
+ searchQuery.query.bool.must.push(
2895
+ {
2896
+ term: {
2897
+ 'type.keyword': 'store',
2898
+ },
2899
+ },
2900
+ {
2901
+ nested: {
2902
+ path: 'mappingInfo',
2903
+ query: {
2904
+ bool: {
2905
+ must: [
2906
+ {
2907
+ term: {
2908
+ 'mappingInfo.type': ticketsFeature ? 'review' : ticketsApproveFeature ?
2909
+ 'approve' : 'tagging',
2910
+ },
2911
+ },
2912
+
2913
+ ],
2914
+ },
2915
+ },
2916
+ },
2917
+ },
499
2918
 
500
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
2919
+ );
2920
+ }
501
2921
 
502
- // Get ticket list from the footfallDirectory index in OpenSearch
503
- // Assumes you have openSearch and getOpenSearchData available in this scope.
504
- // `openSearch.footfallDirectory` is the index for the tickets.
505
- // Uses req.query for any filter (add additional filtering logic per your requirements).
2922
+ if ( inputData?.permissionType ) {
2923
+ searchQuery.query.bool.must.push(
2924
+ {
2925
+ nested: {
2926
+ path: 'mappingInfo',
2927
+ query: {
2928
+ bool: {
2929
+ must: [
2930
+ {
2931
+ term: {
2932
+ 'mappingInfo.type': inputData?.permissionType == 'review' ? 'review' :
2933
+ 'approve',
2934
+ },
2935
+ },
2936
+
2937
+ ],
2938
+ },
2939
+ },
2940
+ },
2941
+ },
2942
+ );
2943
+ }
506
2944
 
507
- const searchQuery = {
2945
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
2946
+ searchQuery.query.bool['should'] = [];
2947
+ searchQuery.query.bool.should = [
508
2948
 
509
- size: limit, // or use parseInt(req.query.limit) for dynamic
510
- from: offset, // or use parseInt(req.query.offset) for dynamic
511
- sort: [ { 'createdAt': { order: 'desc' } } ],
512
- query: {
513
- bool: {
514
- must: [],
2949
+ {
2950
+ 'wildcard': {
2951
+ 'storeName.keyword': {
2952
+ 'value': `*${inputData.searchValue}*`,
2953
+ },
2954
+ },
2955
+ },
2956
+ {
2957
+ 'wildcard': {
2958
+ 'storeId.keyword': {
2959
+ 'value': `*${inputData.searchValue}*`,
2960
+ },
2961
+ },
2962
+ },
2963
+ {
2964
+ 'wildcard': {
2965
+ 'ticketId.keyword': {
2966
+ 'value': `*${inputData.searchValue}*`,
2967
+ },
2968
+ },
515
2969
  },
516
- },
517
- };
518
2970
 
519
- // Example: Filtering by storeId if present in the query
520
- if ( inputData.storeId ) {
521
- searchQuery.query.bool.must.push( {
522
- term: { 'storeId.keyword': inputData.storeId },
523
- } );
524
- }
525
- // Example: Filtering by status if provided
526
- if ( inputData.status ) {
527
- searchQuery.query.bool.must.push( {
528
- term: { 'status.keyword': inputData.status },
529
- } );
530
- }
531
2971
 
2972
+ ];
2973
+ searchQuery.query.bool['minimum_should_match'] = 1;
2974
+ }
532
2975
  // You can add more filters as needed
533
- logger.info( { searchQuery, index: openSearch.footfallDirectory } );
534
2976
  const searchResult = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
535
-
536
-
537
2977
  const count = searchResult?.body?.hits?.total?.value || 0;
2978
+ logger.info( { searchResult } );
538
2979
  if ( count === 0 ) {
539
2980
  return res.sendError( 'no data found', 204 );
540
2981
  }
541
2982
  const ticketListData = searchResult?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
542
2983
 
543
- let temp =[];
544
- if ( req.user.userType =='tango' ) {
545
- if ( inputData.tangotype == 'store' ) {
546
- for ( let item of ticketListData ) {
547
- temp.push( {
548
-
549
- ticketId: item?.ticketId,
550
- storeId: item?.storeId,
551
- storeName: item?.storeName,
552
-
553
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
554
- issueDate: item?.dateString,
555
- dueDate: '',
556
- footfall: item?.footfall,
557
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
558
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
559
- approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
560
- tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
561
- status: item?.status,
562
-
563
- } );
2984
+ let temp = [];
2985
+ if ( req.user.userType === 'tango' ) {
2986
+ if ( inputData.tangoType === 'store' ) {
2987
+ if ( inputData?.isExport ) {
2988
+ const exportData = [];
2989
+ for ( let item of ticketListData ) {
2990
+ exportData.push( {
2991
+
2992
+ 'Ticket ID': item?.ticketId,
2993
+ 'store Name': item?.storeName,
2994
+ 'store ID': item?.storeId,
2995
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.createdAt,
2996
+ 'Issue Date': item?.dateString,
2997
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.dueDate,
2998
+ 'Actual FF': item?.footfallCount,
2999
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3000
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3001
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3002
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3003
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
3004
+ 'Comments': item?.mappingInfo?.find( ( f ) => f.type === 'finalRevision' )?.comments || '--',
3005
+ 'Sub Comments': item?.mappingInfo?.find( ( f ) => f.type === 'finalRevision' )?.subComments || '--',
3006
+
3007
+ } );
3008
+ }
3009
+ return await download( exportData, res );
3010
+ } else {
3011
+ for ( let item of ticketListData ) {
3012
+ temp.push( {
3013
+
3014
+ ticketId: item?.ticketId,
3015
+ storeId: item?.storeId,
3016
+ storeName: item?.storeName,
3017
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.createdAt,
3018
+ issueDate: item?.dateString,
3019
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.dueDate,
3020
+ footfall: item?.footfallCount,
3021
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3022
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3023
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3024
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3025
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
3026
+ comments: item?.mappingInfo?.find( ( f ) => f.type === 'finalRevision' )?.comments || '--',
3027
+ subComments: item?.mappingInfo?.find( ( f ) => f.type === 'finalRevision' )?.subComments || '--',
3028
+
3029
+ } );
3030
+ }
564
3031
  }
565
3032
  } else {
566
- for ( let item of ticketListData ) {
567
- temp.push( {
568
-
569
- ticketId: item?.ticketId,
570
- storeId: item?.storeId,
571
- storeName: item?.storeName,
572
-
573
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
574
- issueDate: item?.dateString,
575
- footfall: item?.footfall,
576
- dueDate: '',
577
- type: item.type || 'store',
578
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
579
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
580
- approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
581
- tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
582
- status: item?.status,
583
- tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
584
-
585
- } );
3033
+ if ( inputData?.isExport ) {
3034
+ const exportData = [];
3035
+ for ( let item of ticketListData ) {
3036
+ console.log( item );
3037
+ exportData.push( {
3038
+
3039
+ 'Ticket ID': item?.ticketId,
3040
+ 'Store Name': item?.storeName,
3041
+ 'Store ID': item?.storeId,
3042
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
3043
+ 'Issue Date': item?.dateString,
3044
+ 'Ticket Type': item?.type,
3045
+ 'Actual FF': item?.footfallCount,
3046
+ // Use the dueDate from the last mappingInfo item whose type is NOT 'finalRevisoon'
3047
+ 'Due Date': ( () => {
3048
+ if ( Array.isArray( item?.mappingInfo ) ) {
3049
+ const filtered = item.mappingInfo.filter( ( f ) => f.dueDate && f.type !== 'finalRevisoon' );
3050
+ return filtered.length > 0 ? filtered[filtered.length - 1].dueDate : '';
3051
+ }
3052
+ return '';
3053
+ } )(),
3054
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3055
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3056
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3057
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3058
+ 'Ticket Status': item?.type === 'store'? item?.status : '',
3059
+ 'Tango Status': item?.type === 'store'? item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--' : item.status,
3060
+
3061
+ } );
3062
+ }
3063
+ return await download( exportData, res );
3064
+ } else {
3065
+ for ( let item of ticketListData ) {
3066
+ console.log( '🚀 ~ ticketList ~ item:', item );
3067
+ temp.push( {
3068
+
3069
+ ticketId: item?.ticketId,
3070
+ storeId: item?.storeId,
3071
+ storeName: item?.storeName,
3072
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f?.type === 'tagging' )?.createdAt,
3073
+ issueDate: item?.dateString,
3074
+ footfall: item?.footfallCount,
3075
+ dueDate: ( () => {
3076
+ if ( Array.isArray( item?.mappingInfo ) ) {
3077
+ const filtered = item.mappingInfo.filter( ( f ) => f?.dueDate && f?.type !== 'finalRevisoon' );
3078
+ return filtered.length > 0 ? filtered[filtered.length - 1].dueDate : '';
3079
+ }
3080
+ return '';
3081
+ } )(),
3082
+ type: item?.type || 'store',
3083
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3084
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3085
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3086
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3087
+ status: item?.status,
3088
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
3089
+
3090
+ } );
3091
+ }
586
3092
  }
587
3093
  }
588
3094
  } else {
589
- if ( req.user.role === 'superadmin' ) {
590
- for ( let item of ticketListData ) {
591
- temp.push( {
592
-
593
- ticketId: item?.ticketId,
594
- storeId: item?.storeId,
595
- storeName: item?.storeName,
596
-
597
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
598
- issueDate: item?.dateString,
599
- dueDate: '',
600
- footfall: item?.footfall,
601
-
602
- type: item.type || 'store',
603
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
604
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
605
- approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc||'--',
606
- tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
607
- status: item?.status,
608
- tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
609
- approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail||'--',
610
-
611
- } );
3095
+ if ( inputData?.permissionType === 'approve' ) {
3096
+ if ( inputData.exportData ) {
3097
+ const exportData = [];
3098
+ for ( let item of ticketListData ) {
3099
+ exportData.push( {
3100
+
3101
+ 'Ticket ID': item?.ticketId,
3102
+ 'Store Name': item?.storeName,
3103
+ 'Store ID': item?.storeId,
3104
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3105
+ 'Issue Date': item?.dateString,
3106
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3107
+ 'Actual FF': item?.footfallCount,
3108
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3109
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3110
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3111
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3112
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3113
+ 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3114
+ 'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3115
+
3116
+ } );
3117
+ }
3118
+ return await download( exportData, res );
3119
+ } else {
3120
+ for ( let item of ticketListData ) {
3121
+ temp.push( {
3122
+
3123
+ ticketId: item?.ticketId,
3124
+ storeId: item?.storeId,
3125
+ storeName: item?.storeName,
3126
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3127
+ issueDate: item?.dateString,
3128
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3129
+ footfall: item?.footfallCount,
3130
+ type: item.type || 'store',
3131
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3132
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3133
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3134
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3135
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3136
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3137
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3138
+
3139
+ } );
3140
+ }
3141
+ }
3142
+ } else if ( inputData?.permissionType === 'review' ) {
3143
+ if ( inputData?.isExport ) {
3144
+ const exportData = [];
3145
+ for ( let item of ticketListData ) {
3146
+ exportData.push( {
3147
+
3148
+ 'Ticket ID': item?.ticketId,
3149
+ 'Store Name': item?.storeName,
3150
+ 'Store ID': item?.storeId,
3151
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3152
+ 'Issue Date': item?.dateString,
3153
+ 'Actual FF': item?.footfallCount,
3154
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3155
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3156
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3157
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3158
+ 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3159
+
3160
+ } );
3161
+ }
3162
+ return await download( exportData, res );
3163
+ } else {
3164
+ for ( let item of ticketListData ) {
3165
+ temp.push( {
3166
+
3167
+ ticketId: item?.ticketId,
3168
+ storeId: item?.storeId,
3169
+ storeName: item?.storeName,
3170
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3171
+ issueDate: item?.dateString,
3172
+ footfall: item?.footfallCount,
3173
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3174
+ type: item.type || 'store',
3175
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3176
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3177
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3178
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3179
+
3180
+ } );
3181
+ }
612
3182
  }
613
3183
  } else if ( req.user.role === 'user' ) {
614
3184
  temp = [];
615
3185
  } else if ( ticketsFeature ) {
616
- for ( let item of ticketListData ) {
617
- temp.push( {
618
-
619
- ticketId: item?.ticketId,
620
- storeId: item?.storeId,
621
- storeName: item?.storeName,
622
- ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
623
- issueDate: item?.dateString,
624
- footfall: item?.footfall,
625
- dueDate: '',
626
- type: item.type || 'store',
627
- storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
628
- reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc|| '--',
629
-
630
- status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status|| '--',
631
- ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail||'--',
632
-
633
- } );
3186
+ if ( inputData?.isExport ) {
3187
+ const exportData = [];
3188
+ for ( let item of ticketListData ) {
3189
+ exportData.push( {
3190
+
3191
+ 'Ticket ID': item?.ticketId,
3192
+ 'Store ID': item?.storeId,
3193
+ 'Store Name': item?.storeName,
3194
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3195
+ 'Issue Date': item?.dateString,
3196
+ 'Actual FF': item?.footfallCount,
3197
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3198
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3199
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3200
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3201
+ 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3202
+
3203
+ } );
3204
+ }
3205
+ return await download( exportData, res );
3206
+ } else {
3207
+ for ( let item of ticketListData ) {
3208
+ temp.push( {
3209
+
3210
+ ticketId: item?.ticketId,
3211
+ storeId: item?.storeId,
3212
+ storeName: item?.storeName,
3213
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3214
+ issueDate: item?.dateString,
3215
+ footfall: item?.footfallCount,
3216
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3217
+ type: item.type || 'store',
3218
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3219
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3220
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3221
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3222
+
3223
+ } );
3224
+ }
3225
+ }
3226
+ } else if ( ticketsApproveFeature ) {
3227
+ if ( inputData.isExport ) {
3228
+ const exportData = [];
3229
+ for ( let item of ticketListData ) {
3230
+ exportData.push( {
3231
+
3232
+ 'Ticket ID': item?.ticketId,
3233
+ 'Store Name': item?.storeName,
3234
+ 'Store ID': item?.storeId,
3235
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3236
+ 'Issue Date': item?.dateString,
3237
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3238
+ 'Actual FF': item?.footfallCount,
3239
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3240
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3241
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3242
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3243
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3244
+ 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3245
+ 'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3246
+
3247
+ } );
3248
+ }
3249
+ return await download( exportData, res );
3250
+ } else {
3251
+ for ( let item of ticketListData ) {
3252
+ temp.push( {
3253
+
3254
+ ticketId: item?.ticketId,
3255
+ storeId: item?.storeId,
3256
+ storeName: item?.storeName,
3257
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3258
+ issueDate: item?.dateString,
3259
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3260
+ footfall: item?.footfallCount,
3261
+ type: item.type || 'store',
3262
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3263
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3264
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3265
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3266
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3267
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3268
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3269
+
3270
+ } );
3271
+ }
634
3272
  }
635
3273
  } else {
636
- temp =[];
3274
+ temp = [];
637
3275
  }
638
3276
  }
639
3277
 
640
- return res.sendSuccess( { result: temp } );
3278
+ return res.sendSuccess( { result: temp, count: count } );
641
3279
  } catch ( error ) {
642
3280
  const err = error.message || 'Internal Server Error';
643
3281
  logger.error( { error: error, messgage: req.query } );
@@ -649,149 +3287,19 @@ export async function getTickets( req, res ) {
649
3287
  try {
650
3288
  const openSearch = JSON.parse( process.env.OPENSEARCH );
651
3289
  const inputData = req.query;
652
- const limit = inputData.limit;
653
- const skip = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
654
- inputData.storeId = inputData.storeId.split( ',' ); // convert strig to array
655
- logger.info( { inputData: inputData, limit: limit, skip: skip } );
656
- let source = [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'junkCount', 'junkACCount', 'comments', 'employee', 'houseKeeping', 'junk', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'approverRole', 'approverUserName', 'approverEmail' ];
3290
+
3291
+
3292
+ let source = [ 'storeId', 'createdByEmail', 'type', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'junkCount', 'junkACCount', 'comments', 'employee', 'houseKeeping', 'junk', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'approverRole', 'approverUserName', 'approverEmail', 'type' ];
657
3293
  let filter = [
658
3294
 
659
3295
  {
660
- range: {
661
- dateString: {
662
- gte: inputData.fromDate,
663
- lte: inputData.toDate,
664
- format: 'yyyy-MM-dd',
665
- },
3296
+ term: {
3297
+ 'ticketId.keyword': inputData.ticketId,
666
3298
  },
667
3299
  },
668
3300
  ];
669
- if ( inputData?.storeId ) {
670
- filter.push(
671
- {
672
- terms: {
673
- 'storeId.keyword': Array.isArray( inputData.storeId ) ?
674
- inputData.storeId :
675
- inputData.storeId,
676
- },
677
- },
678
- );
679
- }
680
- if ( inputData?.dateString ) {
681
- filter.push(
682
- {
683
- term: {
684
- 'dateString': inputData.dateString,
685
- },
686
- },
687
- );
688
- }
689
- if ( inputData.status ) {
690
- filter.push(
691
- {
692
- term: {
693
- 'status.keyword': inputData.status,
694
- },
695
- },
696
- );
697
- }
698
- if (
699
- inputData.revopsType
700
- ) {
701
- inputData.revopsType === 'employee' ?
702
- filter.push( {
703
- range: {
704
- employeeCount: {
705
- gt: 0,
706
- },
707
- },
708
- } ) :
709
- inputData.revopsType === 'houseKeeping' ?
710
- filter.push( {
711
- range: {
712
- houseKeepingCount: {
713
- gt: 0,
714
- },
715
- },
716
- } ) :
717
- inputData.revopsType === 'junk' ?
718
- filter.push( {
719
- range: {
720
- junkCount: {
721
- gt: 0,
722
- },
723
- },
724
- } ) :
725
- filter.push( {
726
- range: {
727
- duplicateCount: {
728
- gt: 0,
729
- },
730
- },
731
- } );
732
- source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'email', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
733
- inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
734
- inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkStatus', 'junkACCount', 'approverRole', 'approverUserName', 'approverEmail' ] :
735
- inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'revicedFootfall', 'revicedPerc', 'mappingInfo', 'footfallCount', 'duplicateCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'email', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'houseKeepingACCount', 'houseKeepingCount', 'employeeCount', 'employeeACCount', 'duplicateCount', 'duplicateACCount', 'junkCount', 'junkACCount', 'junkStatus', 'approverRole', 'approverUserName', 'approverEmail' ] : [];
736
- }
737
-
738
- if ( inputData.action ) {
739
- filter.push( {
740
- bool: {
741
- should: [
742
- {
743
- constant_score: {
744
- filter: { term: { 'houseKeepingStatus.keyword': inputData.action } },
745
- boost: 1,
746
- _name: 'matched_housekeeping',
747
- },
748
- },
749
- {
750
- constant_score: {
751
- filter: { term: { 'employeeStatus.keyword': inputData.action } },
752
- boost: 1,
753
- _name: 'matched_employee',
754
- },
755
- },
756
- {
757
- constant_score: {
758
- filter: { term: { 'duplicateStatus.keyword': inputData.action } },
759
- boost: 1,
760
- _name: 'matched_duplicate',
761
- },
762
- },
763
- {
764
- constant_score: {
765
- filter: { term: { 'junkStatus.keyword': inputData.action } },
766
- boost: 1,
767
- _name: 'matched_junk',
768
- },
769
- },
770
- ],
771
- minimum_should_match: 1,
772
- },
773
- } );
774
- }
775
3301
 
776
3302
 
777
- let getRevCount = {};
778
- if ( inputData.revopsType ) {
779
- getRevCount = {
780
- size: 0,
781
- query: {
782
- bool: {
783
- filter: filter,
784
- },
785
- },
786
- aggs: {
787
- totalCount: {
788
- sum: {
789
- field: inputData.revopsType == 'employee' ? 'employeeCount' : inputData.revopsType == 'houseKeeping' ? 'houseKeepingCount' :inputData.revopsType == 'junk' ? 'junkCount': 'duplicateCount',
790
- },
791
- },
792
- },
793
- };
794
- }
795
3303
  const getCount = {
796
3304
  query: {
797
3305
  bool: {
@@ -802,20 +3310,14 @@ export async function getTickets( req, res ) {
802
3310
 
803
3311
 
804
3312
  const geteDataCount = await getOpenSearchCount( openSearch.footfallDirectory, getCount );
805
- const geteRevDataCount = inputData?.revopsType ? await getOpenSearchData( openSearch.footfallDirectory, getRevCount ) : null;
806
- const revCount = inputData?.revopsType ? geteRevDataCount?.body?.aggregations?.totalCount?.value : 0;
807
- const count = geteDataCount?.body?.count;
808
- if ( !geteDataCount || count == 0 ) {
809
- if ( inputData.storeId?.length > 0 ) {
810
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
811
- return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
812
- }
813
- return res.sendError( 'No data found', 204 );
3313
+
3314
+
3315
+ if ( !geteDataCount ) {
3316
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
814
3317
  }
815
3318
 
816
3319
  const getQuery = {
817
- size: limit,
818
- from: skip,
3320
+ size: 1,
819
3321
  query: {
820
3322
  bool: {
821
3323
  filter: filter,
@@ -825,20 +3327,16 @@ export async function getTickets( req, res ) {
825
3327
  };
826
3328
 
827
3329
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
828
-
829
3330
  const response = getData?.body?.hits?.hits;
830
3331
  if ( !response || response.length == 0 ) {
831
- if ( inputData.storeId?.length > 0 ) {
832
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
833
- return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
834
- }
835
- return res.sendError( 'No data', 204 );
3332
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
836
3333
  }
837
3334
  let temp = [];
838
3335
  if ( inputData?.action ) {
839
3336
  response.map( ( hit ) => {
840
3337
  const defaultData = {
841
3338
  storeId: hit._source.storeId,
3339
+ createdByEmail: hit?._source?.createdByEmail,
842
3340
  dateString: hit?._source?.dateString,
843
3341
  ticketName: hit?._source?.ticketName,
844
3342
  status: hit?._source?.status?.revicedFootfall,
@@ -870,29 +3368,31 @@ export async function getTickets( req, res ) {
870
3368
  approverUserName: hit?._source?.approverUserName,
871
3369
  approverEmail: hit?._source?.approverEmail,
872
3370
  approverRole: hit?._source?.approverRole,
3371
+ type: hit?._source?.type,
873
3372
  };
874
3373
  let result;
875
3374
 
3375
+
876
3376
  const matched = hit.matched_queries;
877
3377
  // Add only matched data array
878
- if ( matched.includes( 'matched_employee' )&& ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
3378
+ if ( matched.includes( 'matched_employee' ) && ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
879
3379
  result = defaultData;
880
3380
  result.employee = hit?._source?.employee;
881
3381
  // result.type = 'employee';
882
3382
  result.matched = matched;
883
3383
  }
884
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
3384
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
885
3385
  result = defaultData;
886
3386
  result.houseKeeping = hit?._source?.houseKeeping;
887
3387
  result.matched = matched;
888
3388
  }
889
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
3389
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
890
3390
  result = defaultData;
891
3391
  result.duplicateImages = hit?._source?.duplicateImages;
892
3392
  result.matched = matched;
893
3393
  }
894
3394
 
895
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
3395
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
896
3396
  result = defaultData;
897
3397
  result.junk = hit?._source?.junk;
898
3398
  result.matched = matched;
@@ -911,15 +3411,264 @@ export async function getTickets( req, res ) {
911
3411
  }
912
3412
  } );
913
3413
  }
3414
+
914
3415
  const finalResponse = inputData.action ? temp : response;
3416
+ const getRevopQuery = {
3417
+ size: 10000,
3418
+ query: {
3419
+ bool: {
3420
+ must: [
3421
+ { term: { 'storeId.keyword': response?.[0]?._source?.storeId } }, // assuming inputData.storeId is an array
3422
+ { term: { 'dateString': response?.[0]?._source?.dateString } },
3423
+ ],
3424
+ },
3425
+ },
3426
+ };
3427
+
3428
+
3429
+ const revopResp = await getOpenSearchData( openSearch.revop, getRevopQuery );
3430
+
3431
+
3432
+ // Map revopResp.body.hits.hits to revopSources
3433
+ const revopSources = Array.isArray( revopResp?.body?.hits?.hits ) ?
3434
+ revopResp?.body?.hits?.hits?.map( ( hit ) => hit._source ) :
3435
+ [];
3436
+
3437
+ // Create a map of revopSources by id for quick lookup
3438
+ const revopSourcesMap = new Map();
3439
+ revopSources.forEach( ( item ) => {
3440
+ if ( item?.id ) {
3441
+ revopSourcesMap.set( String( item.id ), item );
3442
+ }
3443
+ } );
3444
+
3445
+ // Process revopSources to replace duplicateImage entries with full objects from the array
3446
+ const processedRevopSources = revopSources.map( ( item ) => {
3447
+ // Check if this is a duplicate parent item
3448
+ if ( item?.revopsType === 'duplicate' && item?.isParent === true && Array.isArray( item?.duplicateImage ) ) {
3449
+ // Map each duplicateImage entry to the full object from revopSources
3450
+ const updatedDuplicateImage = item.duplicateImage.map( ( duplicateImg ) => {
3451
+ const duplicateId = String( duplicateImg?.id );
3452
+ // Find the full object in revopSources that matches this id
3453
+ const fullObject = revopSourcesMap.get( duplicateId );
3454
+ // Return the full object if found, otherwise return the original duplicateImg
3455
+ return fullObject || duplicateImg;
3456
+ } );
3457
+
3458
+ return {
3459
+ ...item,
3460
+ duplicateImage: updatedDuplicateImage,
3461
+ };
3462
+ }
3463
+ // Return item as-is if it doesn't meet the criteria
3464
+ return item;
3465
+ } );
3466
+
915
3467
  if ( finalResponse?.length == 0 || !finalResponse ) {
916
3468
  if ( inputData.storeId?.length > 0 ) {
917
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
3469
+ const getStoreName = await findOneStore( { storeId: response?.[0]?._source?.storeId }, { storeName: 1 } );
918
3470
  return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
919
3471
  }
920
3472
  return res.sendError( 'No Data found', 204 );
921
3473
  }
922
- return res.sendSuccess( { result: finalResponse, count: count, revopCount: revCount } );
3474
+
3475
+
3476
+ // replace the mappingInfo.revisedDetail with processedRevopSources
3477
+ if ( Array.isArray( finalResponse ) ) {
3478
+ for ( let item of finalResponse ) {
3479
+ if (
3480
+ item &&
3481
+ item._source &&
3482
+ item._source.mappingInfo
3483
+
3484
+ ) {
3485
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
3486
+ const vmsCommentsLogIndex = openSearch.vmsCommentsLog || 'vms-comments-log-dev';
3487
+ let commentsResponse = [];
3488
+ const commentsFilter = [
3489
+ { term: { 'storeId.keyword': item?._source?.storeId } },
3490
+ { term: { dateString: item?._source?.dateString } },
3491
+
3492
+ ];
3493
+
3494
+ const commentsQuery = {
3495
+ size: 10000,
3496
+ sort: [
3497
+ { createdAt: { order: 'desc' } }, // Sort descending by createdAt
3498
+ ],
3499
+ query: {
3500
+ bool: {
3501
+ filter: commentsFilter,
3502
+ },
3503
+ },
3504
+ };
3505
+
3506
+ const commentsRes = await getOpenSearchData( vmsCommentsLogIndex, commentsQuery );
3507
+ // If mappingInfo is an array, update revisedDetail for each mappingInfo object
3508
+ if ( Array.isArray( item._source.mappingInfo ) ) {
3509
+ item._source.mappingInfo.forEach( ( mappingObj ) => {
3510
+ commentsResponse = commentsRes?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
3511
+
3512
+ // Check if duplicate condition exists in commentsResponse
3513
+
3514
+ // Structure comments output
3515
+ let commentsDetails = [];
3516
+
3517
+
3518
+ const types = [ 'tagging', 'review', 'approve' ];
3519
+
3520
+ // Process each type
3521
+ types.forEach( ( typeValue ) => {
3522
+ if ( typeValue === 'tagging' ) {
3523
+ // For tagging, group by category and create separate objects for each category
3524
+ const taggingComments = commentsResponse.filter( ( c ) => c.type === typeValue );
3525
+
3526
+ // Group by category
3527
+ const categoryGroups = {};
3528
+ taggingComments.forEach( ( c ) => {
3529
+ const category = c.category || 'other';
3530
+ if ( !categoryGroups[category] ) {
3531
+ categoryGroups[category] = [];
3532
+ }
3533
+ categoryGroups[category].push( c );
3534
+ } );
3535
+
3536
+ // Create separate objects for each category
3537
+ Object.keys( categoryGroups ).forEach( ( category ) => {
3538
+ const categoryComments = categoryGroups[category];
3539
+ let parent = null;
3540
+
3541
+ const comms = categoryComments.map( ( c ) => {
3542
+ if ( category === 'duplicate' ) {
3543
+ if ( !parent && c.parent ) {
3544
+ parent = c.parent;
3545
+ }
3546
+ return {
3547
+ createdByEmail: c.createdByEmail,
3548
+ createdByUserName: c.createdByUserName,
3549
+ createdByRole: c.createdByRole,
3550
+ message: c.message,
3551
+ };
3552
+ } else {
3553
+ return {
3554
+ id: c.id,
3555
+ tempId: c.tempId,
3556
+ timeRange: c.timeRange,
3557
+ entryTime: c.entryTime,
3558
+ exitTime: c.exitTime,
3559
+ filePath: c.filePath,
3560
+ isChecked: c.isChecked,
3561
+ createdAt: c.createdAt,
3562
+ message: c.message,
3563
+ createdByEmail: c.createdByEmail,
3564
+ createdByUserName: c.createdByUserName,
3565
+ createdByRole: c.createdByRole,
3566
+ };
3567
+ }
3568
+ } );
3569
+
3570
+ const taggingObj = {
3571
+ category: category,
3572
+ type: typeValue,
3573
+ comments: comms,
3574
+ };
3575
+
3576
+ // Add parent only for duplicate category
3577
+ if ( category === 'duplicate' && parent !== null ) {
3578
+ taggingObj.parent = parent;
3579
+ }
3580
+
3581
+ commentsDetails.push( taggingObj );
3582
+ } );
3583
+ } else if ( typeValue === 'review' || typeValue === 'approve' ) {
3584
+ // For review and approve, keep existing structure
3585
+ const comms = commentsResponse
3586
+ .filter( ( c ) => c.type === typeValue )
3587
+ .map( ( c ) => {
3588
+ if ( c.category === 'duplicate' ) {
3589
+ return {
3590
+ parent: c?.taggedImages[0]?._source?.parent,
3591
+ category: c.category,
3592
+ taggedImages: Array.isArray( c.taggedImages ) ?
3593
+ c.taggedImages.map( ( img ) => ( {
3594
+ id: img?._source?.id,
3595
+ tempId: img?._source?.tempId,
3596
+ timeRange: img?._source?.timeRange,
3597
+ entryTime: img?._source?.entryTime,
3598
+ exitTime: img?._source?.exitTime,
3599
+ filePath: img?._source?.filePath,
3600
+ isChecked: img?._source?.isChecked,
3601
+ } ) ) :
3602
+ [],
3603
+ createdByEmail: c.createdByEmail,
3604
+ createdByUserName: c.createdByUserName,
3605
+ createdByRole: c.createdByRole,
3606
+ status: c.status,
3607
+ message: c.message,
3608
+ };
3609
+ } else {
3610
+ return {
3611
+ category: c.category,
3612
+ taggedImages: Array.isArray( c.taggedImages ) ?
3613
+ c.taggedImages.map( ( img ) => ( {
3614
+ id: img?._source?.id,
3615
+ tempId: img?._source?.tempId,
3616
+ timeRange: img?._source?.timeRange,
3617
+ entryTime: img?._source?.entryTime,
3618
+ exitTime: img?._source?.exitTime,
3619
+ filePath: img?._source?.filePath,
3620
+ isChecked: img?._source?.isChecked,
3621
+ } ) ) :
3622
+ [],
3623
+ createdByEmail: c.createdByEmail,
3624
+ createdByUserName: c.createdByUserName,
3625
+ createdByRole: c.createdByRole,
3626
+ status: c.status,
3627
+ message: c.message,
3628
+ };
3629
+ }
3630
+ } );
3631
+
3632
+ // Only add if there are comments
3633
+ if ( comms.length > 0 ) {
3634
+ commentsDetails.push( {
3635
+ type: typeValue,
3636
+ comments: comms,
3637
+ } );
3638
+ } else {
3639
+ // Add empty comments array if no comments
3640
+ commentsDetails.push( {
3641
+ type: typeValue,
3642
+ comments: [],
3643
+ } );
3644
+ }
3645
+ }
3646
+ } );
3647
+
3648
+
3649
+ item._source.commentsDetails = commentsDetails;
3650
+
3651
+ if (
3652
+ Object.prototype.hasOwnProperty.call( mappingObj, 'revisedDetail' ) &&
3653
+ mappingObj.type !== 'tangoreview'
3654
+ ) {
3655
+ mappingObj.revisedDetail = processedRevopSources;
3656
+ }
3657
+ } );
3658
+ } else {
3659
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
3660
+ }
3661
+ }
3662
+ }
3663
+ } else if (
3664
+ finalResponse &&
3665
+ finalResponse._source &&
3666
+ finalResponse._source.mappingInfo
3667
+ ) {
3668
+ finalResponse._source.mappingInfo.revisedDetail = processedRevopSources;
3669
+ }
3670
+
3671
+ return res.sendSuccess( { result: finalResponse } );
923
3672
  } catch ( error ) {
924
3673
  const err = error.message || 'Internal Server Error';
925
3674
  logger.error( { error: error, messgage: req.query } );
@@ -991,7 +3740,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
991
3740
  item.isChecked == true ? tempId.push( { tempId: item.tempId, timeRange: item.timeRange } ) : null;
992
3741
  bulkBody.push(
993
3742
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${item.timeRange}_${item.tempId}` } },
994
- { doc: { isChecked: item.isChecked, status: item?.isChecked == true? 'approved':'rejected' } },
3743
+ { doc: { isChecked: item.isChecked, status: item?.isChecked == true ? 'approved' : 'rejected' } },
995
3744
  );
996
3745
  } );
997
3746
  }
@@ -1006,11 +3755,11 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1006
3755
  updateData.employee = updatedEmployee;
1007
3756
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: updateData } );
1008
3757
  for ( let employee of updateData?.employee ) {
1009
- ( employee.isChecked == true ) ? tempId.push( { tempId: employee.tempId, timeRange: employee.timeRange } ) : null;
1010
- bulkBody.push(
1011
- { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
1012
- { doc: { isChecked: employee.isChecked, status: employee?.isChecked == true? 'approved':'rejected' } },
1013
- );
3758
+ ( employee.isChecked == true ) ? tempId.push( { tempId: employee.tempId, timeRange: employee.timeRange } ) : null;
3759
+ bulkBody.push(
3760
+ { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
3761
+ { doc: { isChecked: employee.isChecked, status: employee?.isChecked == true ? 'approved' : 'rejected' } },
3762
+ );
1014
3763
  }
1015
3764
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { employeeACCount: tempId?.length || 0 } } );
1016
3765
  }
@@ -1029,7 +3778,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1029
3778
  houseKeeping.isChecked == true ? tempId.push( { tempId: houseKeeping.tempId, timeRange: houseKeeping.timeRange } ) : null;
1030
3779
  bulkBody.push(
1031
3780
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${houseKeeping.timeRange}_${houseKeeping.tempId}` } },
1032
- { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true? 'approved':'rejected' } },
3781
+ { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true ? 'approved' : 'rejected' } },
1033
3782
  );
1034
3783
  }
1035
3784
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { houseKeepingACCount: tempId?.length || 0 } } );
@@ -1050,7 +3799,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1050
3799
  junk.isChecked == true ? tempId.push( { tempId: junk.tempId, timeRange: junk.timeRange } ) : null;
1051
3800
  bulkBody.push(
1052
3801
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${junk.timeRange}_${junk.tempId}` } },
1053
- { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true? 'approved':'rejected' } },
3802
+ { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true ? 'approved' : 'rejected' } },
1054
3803
  );
1055
3804
  }
1056
3805
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { junkACCount: tempId?.length || 0 } } );
@@ -1075,7 +3824,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1075
3824
  let getUpdateExistingOne = await getOpenSearchById( openSearch.footfallDirectory, _id );
1076
3825
  const tempIdList = await extractCheckedTempIds( getUpdateExistingOne?.body );
1077
3826
  const isSendMessge = await sendSqsMessage( data, tempIdList, getStoreType, storeId );
1078
- if ( isSendMessge ==true ) {
3827
+ if ( isSendMessge == true ) {
1079
3828
  return true; // res.sendSuccess( 'Ticket has been updated successfully' );
1080
3829
  } else {
1081
3830
  return false; // res.sendError( 'No SQS message sent', 500 );
@@ -1138,7 +3887,7 @@ function updateEmployeeCheckFlags( existingEmployees, inputEmployees, status ) {
1138
3887
  // Step 2: Loop through all existing and update isChecked accordingly
1139
3888
  const updatedEmployees = existingEmployees.map( ( emp ) => ( {
1140
3889
  ...emp,
1141
- isChecked: status === 'rejected'? !checkedTempIds.has( emp.tempId ):checkedTempIds.has( emp.tempId ),
3890
+ isChecked: status === 'rejected' ? !checkedTempIds.has( emp.tempId ) : checkedTempIds.has( emp.tempId ),
1142
3891
  } ) );
1143
3892
 
1144
3893
  return updatedEmployees;
@@ -1188,7 +3937,7 @@ function mergeDuplicateImagesWithUncheck( existingData, inputData, status ) {
1188
3937
  export async function sendSqsMessage( inputData, tempId, getStoreType, storeId ) {
1189
3938
  const sqs = JSON.parse( process.env.SQS );
1190
3939
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1191
- const sqsName = getStoreType > 0? sqs.revopTrackTicket: sqs.revopTicket;
3940
+ const sqsName = getStoreType > 0 ? sqs.revopTrackTicket : sqs.revopTicket;
1192
3941
  const sqsProduceQueue = getStoreType > 0 ? {
1193
3942
  QueueUrl: `${sqs.url}${sqsName}`,
1194
3943
  MessageBody: JSON.stringify( {
@@ -1216,12 +3965,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1216
3965
 
1217
3966
  } ),
1218
3967
  };
1219
- const sqsQueue = getStoreType > 0? await sendMessageToFIFOQueue( sqsProduceQueue ):
1220
- await sendMessageToQueue(
1221
- sqsProduceQueue.QueueUrl,
1222
- sqsProduceQueue.MessageBody,
1223
-
1224
- );
3968
+ const sqsQueue = getStoreType > 0 ? await sendMessageToFIFOQueue( sqsProduceQueue ) :
3969
+ await sendMessageToQueue(
3970
+ sqsProduceQueue.QueueUrl,
3971
+ sqsProduceQueue.MessageBody,
3972
+ );
1225
3973
  if ( sqsQueue.statusCode ) {
1226
3974
  logger.error( {
1227
3975
  error: `${sqsQueue}`,
@@ -1229,7 +3977,7 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1229
3977
  } );
1230
3978
  return false;
1231
3979
  } else {
1232
- const id =`${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0? 'live':'reduction'}_${Date.now()}`;
3980
+ const id = `${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0 ? 'live' : 'reduction'}_${Date.now()}`;
1233
3981
  const logs = {
1234
3982
  QueueUrl: sqsProduceQueue.QueueUrl,
1235
3983
  MessageBody: sqsProduceQueue.MessageBody,
@@ -1279,13 +4027,7 @@ export async function getTaggedStores( req, res ) {
1279
4027
  },
1280
4028
  },
1281
4029
  ];
1282
- // if ( req?.user?.userType == 'client' && req?.user?.role !== 'superadmin' && req?.stores?.length > 0 ) {
1283
- // filter.push(
1284
- // {
1285
- // terms: { 'storeId.keyword': req.stores },
1286
- // },
1287
- // );
1288
- // }
4030
+
1289
4031
  filter.push(
1290
4032
  {
1291
4033
  terms: { 'storeId.keyword': req?.stores || [] },
@@ -1336,7 +4078,7 @@ export async function getTaggedStores( req, res ) {
1336
4078
  + (doc.containsKey('employeeCount') && !doc['employeeCount'].empty ? doc['employeeCount'].value : 0)
1337
4079
  + (doc.containsKey('junkCount') && !doc['junkCount'].empty ? doc['junkCount'].value : 0);
1338
4080
  `,
1339
- lang: 'painless',
4081
+ lang: 'scripting',
1340
4082
  },
1341
4083
  },
1342
4084
  },
@@ -1400,8 +4142,8 @@ export async function downloadTickets( req, res ) {
1400
4142
  {
1401
4143
  terms: {
1402
4144
  'storeId.keyword': Array.isArray( inputData.storeId ) ?
1403
- inputData.storeId :
1404
- inputData.storeId,
4145
+ inputData.storeId :
4146
+ inputData.storeId,
1405
4147
  },
1406
4148
  },
1407
4149
  );
@@ -1447,28 +4189,28 @@ export async function downloadTickets( req, res ) {
1447
4189
  gt: 0,
1448
4190
  },
1449
4191
  },
1450
-
1451
- } ) :
1452
- inputData.revopsType === 'junk' ?
1453
- filter.push( {
1454
- range: {
1455
- junkCount: {
1456
- gt: 0,
1457
- },
1458
- },
1459
- } ):
1460
-
1461
- filter.push( {
1462
- range: {
1463
- duplicateCount: {
1464
- gt: 0,
1465
- },
1466
- },
1467
- } );
4192
+
4193
+ } ) :
4194
+ inputData.revopsType === 'junk' ?
4195
+ filter.push( {
4196
+ range: {
4197
+ junkCount: {
4198
+ gt: 0,
4199
+ },
4200
+ },
4201
+ } ) :
4202
+
4203
+ filter.push( {
4204
+ range: {
4205
+ duplicateCount: {
4206
+ gt: 0,
4207
+ },
4208
+ },
4209
+ } );
1468
4210
  source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1469
4211
  inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1470
4212
  inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1471
- inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'junkCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] : [];
4213
+ inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'junkCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] : [];
1472
4214
  }
1473
4215
 
1474
4216
  if ( inputData.action ) {
@@ -1545,7 +4287,7 @@ export async function downloadTickets( req, res ) {
1545
4287
  let temp = [];
1546
4288
  if ( inputData?.action ) {
1547
4289
  response.map( ( hit ) => {
1548
- const defaultData ={
4290
+ const defaultData = {
1549
4291
  storeId: hit._source.storeId,
1550
4292
  dateString: hit?._source?.dateString,
1551
4293
  ticketName: hit?._source?.ticketName,
@@ -1575,19 +4317,19 @@ export async function downloadTickets( req, res ) {
1575
4317
  // result.type = 'employee';
1576
4318
  result.matched = matched;
1577
4319
  }
1578
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
4320
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
1579
4321
  logger.info( { revop: inputData.revopsType } );
1580
4322
  result = defaultData;
1581
4323
  result.houseKeeping = hit?._source?.houseKeeping;
1582
4324
  result.matched = matched;
1583
4325
  }
1584
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
4326
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
1585
4327
  logger.info( { revop: inputData.revopsType } );
1586
4328
  result = defaultData;
1587
4329
  result.duplicateImages = hit?._source?.duplicateImages;
1588
4330
  result.matched = matched;
1589
4331
  }
1590
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
4332
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
1591
4333
  result = defaultData;
1592
4334
  result.junk = hit?._source?.junk;
1593
4335
  result.matched = matched;
@@ -1631,7 +4373,7 @@ export async function downloadTickets( req, res ) {
1631
4373
  revopsType: inputData?.revopsType,
1632
4374
  type: 'get-tickets',
1633
4375
  };
1634
- const record={
4376
+ const record = {
1635
4377
  stores: inputData?.storeId,
1636
4378
  fromDate: inputData?.fromDate,
1637
4379
  toDate: inputData?.toDate,
@@ -1649,7 +4391,7 @@ export async function downloadTickets( req, res ) {
1649
4391
  const sqs = JSON.parse( process.env.SQS );
1650
4392
  const sqsName = sqs.revopDownload;
1651
4393
 
1652
- const sqsProduceQueue ={
4394
+ const sqsProduceQueue = {
1653
4395
  QueueUrl: `${sqs.url}${sqsName}`,
1654
4396
  MessageBody: JSON.stringify( {
1655
4397
  _id: getId?._id,
@@ -1678,7 +4420,6 @@ export async function downloadTickets( req, res ) {
1678
4420
  }
1679
4421
 
1680
4422
  async function extractTempIds( document ) {
1681
- logger.info( { document: document } );
1682
4423
  const source = document?._source || {};
1683
4424
  const result = [];
1684
4425
 
@@ -1719,6 +4460,14 @@ async function extractTempIds( document ) {
1719
4460
  export async function reviewerList( req, res ) {
1720
4461
  try {
1721
4462
  const inputData = req.query;
4463
+
4464
+ console.log( '🚀 ~ reviewerList ~ inputData.tangotype:', inputData.tangotype );
4465
+ if ( inputData.tangotype!=''&&inputData.tangotype==='internal' ) {
4466
+ const getUserlist = await findUser( { userType: 'tango' }, { userName: 1, email: 1, role: 1 } );
4467
+ return res.sendSuccess( getUserlist || [] );
4468
+ }
4469
+
4470
+
1722
4471
  // Build the query for users who have rolespermission with featureName "FootfallDirectory",
1723
4472
  // and a module "Reviewer" where isAdd or isEdit is true.
1724
4473
  const reviewerRoleQuery = {
@@ -1728,7 +4477,7 @@ export async function reviewerList( req, res ) {
1728
4477
  featureName: 'FootfallDirectory',
1729
4478
  modules: {
1730
4479
  $elemMatch: {
1731
- name: 'Reviewer',
4480
+ name: inputData?.type === 'review' ? 'reviewer' : 'approver',
1732
4481
  $or: [ { isAdd: true }, { isEdit: true } ],
1733
4482
  },
1734
4483
  },
@@ -1737,7 +4486,7 @@ export async function reviewerList( req, res ) {
1737
4486
  };
1738
4487
 
1739
4488
  const getUserlist = await findUser( reviewerRoleQuery, { userName: 1, email: 1, role: 1 } );
1740
- return res.sendSuccess( getUserlist|| [] );
4489
+ return res.sendSuccess( getUserlist || [] );
1741
4490
  } catch ( error ) {
1742
4491
  const err = error.message || 'Internal Server Error';
1743
4492
  return res.sendError( err, 500 );
@@ -1747,6 +4496,7 @@ export async function reviewerList( req, res ) {
1747
4496
  export async function openTicketList( req, res ) {
1748
4497
  try {
1749
4498
  const inputData = req.body;
4499
+ logger.info( { inputData } );
1750
4500
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1751
4501
 
1752
4502
  // INSERT_YOUR_CODE
@@ -1759,6 +4509,29 @@ export async function openTicketList( req, res ) {
1759
4509
  clientId: Array.isArray( clientId ) ? clientId : [ clientId ],
1760
4510
  },
1761
4511
  },
4512
+ {
4513
+ nested: {
4514
+ path: 'mappingInfo',
4515
+ query: {
4516
+ bool: {
4517
+ must: [
4518
+ {
4519
+ term: {
4520
+ 'mappingInfo.type': inputData.type,
4521
+ },
4522
+ },
4523
+ {
4524
+ term: {
4525
+ 'mappingInfo.status': 'Open',
4526
+ },
4527
+ },
4528
+
4529
+ ],
4530
+ },
4531
+ },
4532
+ },
4533
+ },
4534
+
1762
4535
  {
1763
4536
  range: {
1764
4537
  dateString: {
@@ -1770,6 +4543,7 @@ export async function openTicketList( req, res ) {
1770
4543
  },
1771
4544
  ];
1772
4545
 
4546
+
1773
4547
  const openSearchQuery = {
1774
4548
  size: 10000,
1775
4549
  query: {
@@ -1777,14 +4551,44 @@ export async function openTicketList( req, res ) {
1777
4551
  filter: filter,
1778
4552
  },
1779
4553
  },
1780
- _source: [ 'ticketId', 'storeName', 'revicedFootfall', 'footfallCount', 'revicedPerc' ],
4554
+ _source: [ 'ticketId', 'storeName', 'storeId', 'dateString', 'revicedFootfall', 'footfallCount', 'revicedPerc', 'type' ],
1781
4555
  };
1782
4556
 
4557
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
4558
+ openSearchQuery.query.bool['should'] = [];
4559
+ openSearchQuery.query.bool.should = [
4560
+
4561
+ {
4562
+ 'wildcard': {
4563
+ 'storeName.keyword': {
4564
+ 'value': `*${inputData.searchValue}*`,
4565
+ },
4566
+ },
4567
+ },
4568
+ {
4569
+ 'wildcard': {
4570
+ 'ticketId.keyword': {
4571
+ 'value': `*${inputData.searchValue}*`,
4572
+ },
4573
+ },
4574
+ },
4575
+
4576
+
4577
+ ];
4578
+ openSearchQuery.query.bool['minimum_should_match'] = 1;
4579
+ }
4580
+
4581
+ // INSERT_YOUR_CODE
4582
+ // Add sorting by revicedPerc descending (highest revised accuracy first)
4583
+ openSearchQuery.sort = [
4584
+ { 'revicedPerc.keyword': { order: inputData?.sortOrder === 1 ? 'asc' : 'desc' } },
4585
+ ];
4586
+
1783
4587
 
1784
4588
  // Assuming getOpenSearchData and openSearch.footfallDirectoryTagging are available
1785
4589
  const result = await getOpenSearchData( openSearch.footfallDirectory, openSearchQuery );
1786
4590
  const getUserlist = result?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1787
- return res.sendSuccess( getUserlist|| [] );
4591
+ return res.sendSuccess( getUserlist || [] );
1788
4592
  } catch ( error ) {
1789
4593
  const err = error.message || 'Internal Server Error';
1790
4594
  logger.error( { error: error, function: 'openTicketList' } );
@@ -1797,73 +4601,32 @@ export async function assignTicket( req, res ) {
1797
4601
  const inputData = req.body;
1798
4602
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1799
4603
 
1800
- // INSERT_YOUR_CODE
1801
- // Build the query to match storeId(s) and dateString range [fromDate, toDate], format: 'yyyy-mm-dd'
1802
- const { email, userName, role, actionType } = inputData;
1803
-
1804
- // INSERT_YOUR_CODE
1805
-
1806
- // Find and update mappingInfo fields for the provided ticketId and actionType
1807
- // Requires ticketId in inputData
1808
- const { ticketId } = inputData;
1809
- if ( !ticketId ) {
1810
- return res.sendError( 'ticketId is required', 400 );
1811
- }
1812
-
1813
- // Build the OpenSearch update-by-query body
1814
- const updateBody = {
1815
- script: {
1816
- source: `
1817
- if (ctx._source.mappingInfo != null) {
1818
- for (int i = 0; i < ctx._source.mappingInfo.length; i++) {
1819
- if (ctx._source.mappingInfo[i].type == params.actionType) {
1820
- ctx._source.mappingInfo[i].createdByEmail = params.email;
1821
- ctx._source.mappingInfo[i].createdByUserName = params.userName;
1822
- ctx._source.mappingInfo[i].createdByRole = params.role;
1823
- }
1824
- }
1825
- }
1826
- `,
1827
- lang: 'painless',
1828
- params: {
1829
- email,
1830
- userName,
1831
- role,
1832
- actionType,
1833
- },
1834
- },
1835
- query: {
1836
- bool: {
1837
- must: [
1838
- { term: { 'ticketId.keyword': ticketId } },
1839
- {
1840
- nested: {
1841
- path: 'mappingInfo',
1842
- query: {
1843
- bool: {
1844
- must: [
1845
- { match: { 'mappingInfo.type': actionType } },
1846
- ],
1847
- },
1848
- },
1849
- },
1850
- },
1851
- ],
1852
- },
1853
- },
1854
- };
1855
-
1856
- // Call OpenSearch _update_by_query to update doc(s) where ticketId and mappingInfo[i].type == actionType
1857
- const response = await upsertOpenSearchData(
1858
- openSearch.footfallDirectory,
1859
- '11-1716_2025-11-20_footfall-directory-tagging',
1860
- updateBody, // custom arg to indicate passthrough for update-by-query, depends on helper implementation
1861
- );
1862
-
4604
+ const { email, userName, role, storeId, dateString } = inputData;
4605
+ const _id = `${storeId}_${dateString}_footfall-directory-tagging`;
1863
4606
 
1864
- logger.info( { response } );
4607
+ const getTicket = await getOpenSearchById( openSearch.footfallDirectory, _id );
4608
+ if ( !getTicket ) {
4609
+ return res.sendError( 'Ticket is not found', 400 );
4610
+ }
4611
+ const source = getTicket?.body?._source;
4612
+ const mappingInfo = Array.isArray( source.mappingInfo ) ? source.mappingInfo : [];
4613
+ if ( mappingInfo.length === 0 ) {
4614
+ return res.sendError( 'Ticket is not found', 400 );
4615
+ }
4616
+ const lastIndex = mappingInfo.length - 1;
4617
+ const lastEntry = mappingInfo[lastIndex];
4618
+ if ( String( lastEntry.status ) !== 'In-Progress' ) {
4619
+ return res.sendError( 'Ticket is not in progress', 400 );
4620
+ }
1865
4621
 
1866
- return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
4622
+ const currentTime = new Date();
4623
+ const updatedMappingInfo = [ ...mappingInfo ];
4624
+ updatedMappingInfo[lastIndex] = { ...lastEntry, createdByEmail: email, updatedAt: currentTime, createdByUserName: userName, createdByRole: role };
4625
+ const updateResult = await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { mappingInfo: updatedMappingInfo } } );
4626
+ if ( !updateResult ) {
4627
+ return res.sendError( 'Failed to update ticket', 400 );
4628
+ }
4629
+ return res.sendSuccess( { message: 'Ticket assigned successfully' } );
1867
4630
  } catch ( error ) {
1868
4631
  const err = error.message || 'Internal Server Error';
1869
4632
  logger.error( { error: error, function: 'assignTicket' } );
@@ -1874,11 +4637,13 @@ export async function assignTicket( req, res ) {
1874
4637
  export async function updateTempStatus( req, res ) {
1875
4638
  try {
1876
4639
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1877
- const { id, status } = req.body;
4640
+ const inputData = req.body;
4641
+ const { id, type, status, comments } = inputData;
1878
4642
 
1879
4643
  // Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
1880
4644
  // 1. Search for all documents matching the ticket IDs
1881
4645
  const searchBody = {
4646
+ size: 10000,
1882
4647
  query: {
1883
4648
  bool: {
1884
4649
  must: [
@@ -1897,16 +4662,14 @@ export async function updateTempStatus( req, res ) {
1897
4662
  openSearch.revop,
1898
4663
  searchBody,
1899
4664
  );
1900
- logger.info( { searchResp: searchResp } );
4665
+
1901
4666
  // Extract bulk IDs to update
1902
4667
  const hits = searchResp?.body?.hits?.hits ?? [];
1903
- logger.info( { hits: hits } );
4668
+
1904
4669
  if ( !hits.length ) {
1905
4670
  return res.sendError( 'no data', 204 );
1906
4671
  }
1907
4672
 
1908
- // 2. Build bulk update commands
1909
- // Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
1910
4673
 
1911
4674
  // 1. Get all IDs from hits
1912
4675
  const docIdToIndex = {};
@@ -1914,7 +4677,6 @@ export async function updateTempStatus( req, res ) {
1914
4677
  docIdToIndex[doc._id] = doc._index;
1915
4678
  } );
1916
4679
  const docIds = hits.map( ( doc ) => doc._id );
1917
- logger.info( { docIds } );
1918
4680
  // 2. Fetch all docs by ID to get 'actions' (in chunks if large)
1919
4681
  const getBody = [];
1920
4682
  for ( const doc of hits ) {
@@ -1922,71 +4684,76 @@ export async function updateTempStatus( req, res ) {
1922
4684
  }
1923
4685
 
1924
4686
  let mgetResp;
1925
- try {
1926
- mgetResp = await getOpenSearchData(
1927
- openSearch.revop,
1928
- {
1929
- query: {
1930
- ids: {
1931
- values: docIds,
1932
- },
4687
+
4688
+ mgetResp = await getOpenSearchData(
4689
+ openSearch.revop,
4690
+ {
4691
+ size: 10000,
4692
+ query: {
4693
+ ids: {
4694
+ values: docIds,
1933
4695
  },
1934
- _source: true,
1935
4696
  },
1936
- );
1937
- } catch ( err ) {
1938
- logger.error( { error: err } );
1939
- mgetResp = undefined;
1940
- }
1941
- logger.info( { mgetResp } );
4697
+ _source: true,
4698
+ },
4699
+ );
1942
4700
  // (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
1943
4701
  // For fallback, fetch all source fields via another search
1944
- let fullDocs = [];
1945
- if ( mgetResp && mgetResp.body && mgetResp.body.docs && Array.isArray( mgetResp.body.docs ) ) {
1946
- fullDocs = mgetResp.body.docs;
1947
- } else if ( searchResp.body && searchResp.body.hits && searchResp.body.hits.hits ) {
1948
- // fallback: use searchResp docs (request _source above)
1949
- fullDocs = searchResp.body.hits.hits;
1950
- }
4702
+ let fullDocs = mgetResp?.body?.hits?.hits || searchResp?.body?.hits?.hits || [];
1951
4703
 
1952
4704
  // 3. Prepare the new actions array for each doc, and set up bulk update payloads
1953
4705
  const reviewActions = [ 'approved', 'rejected' ];
1954
4706
  const docsToUpdate = [];
1955
- logger.info( { fullDocs: fullDocs } );
1956
4707
  for ( const doc of fullDocs ) {
1957
4708
  const source = doc._source || doc.fields || {}; // support mget and search hits
1958
4709
  let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
1959
4710
  if ( reviewActions.includes( status ) ) {
1960
4711
  // for review: update or push 'review'
1961
4712
  let found = false;
1962
- actions = actions.map( ( item ) => {
1963
- if ( item.actionType === 'review' ) {
1964
- found = true;
1965
- return { ...item, action: status };
1966
- }
1967
- return item;
1968
- } );
1969
- if ( !found ) {
1970
- actions.push( { actionType: 'review', action: status } );
1971
- }
1972
- } else {
1973
- // tagging: update or push 'tagging'
1974
- let found = false;
1975
- actions = actions.map( ( item ) => {
1976
- if ( item.actionType === 'tagging' ) {
1977
- found = true;
1978
- return { ...item, action: 'submitted' };
1979
- }
1980
- return item;
1981
- } );
1982
- if ( !found ) {
1983
- actions.push( { actionType: 'tagging', action: 'submitted' } );
4713
+ switch ( type ) {
4714
+ case 'review':
4715
+ actions = actions.map( ( item ) => {
4716
+ if ( item.actionType === 'review' ) {
4717
+ found = true;
4718
+ return { ...item, action: status };
4719
+ }
4720
+ return item;
4721
+ } );
4722
+ if ( !found ) {
4723
+ actions.push( { actionType: 'review', action: status } );
4724
+ }
4725
+ break;
4726
+ case 'approve':
4727
+ actions = actions.map( ( item ) => {
4728
+ if ( item.actionType === 'approve' ) {
4729
+ found = true;
4730
+ return { ...item, action: status };
4731
+ }
4732
+ return item;
4733
+ } );
4734
+ if ( !found ) {
4735
+ actions.push( { actionType: 'approve', action: status } );
4736
+ }
4737
+ break;
4738
+ default:
4739
+ return res.sendError( 'wrong vaue', 400 );
1984
4740
  }
1985
4741
  }
4742
+ let isChecked = true;
4743
+ switch ( type ) {
4744
+ case 'review':
4745
+ isChecked = status === 'approved' ? true : false;
4746
+ break;
4747
+ case 'approve':
4748
+ isChecked = status === 'approved' ? doc?._source?.isChecked : !doc?._source?.isChecked;
4749
+ break;
4750
+ }
1986
4751
  docsToUpdate.push( {
1987
4752
  _index: doc._index || docIdToIndex[doc._id],
1988
4753
  _id: doc._id,
1989
4754
  actions,
4755
+ isChecked: isChecked,
4756
+ comments: comments || '',
1990
4757
  } );
1991
4758
  }
1992
4759
  const bulkPayload = [];
@@ -1996,11 +4763,9 @@ export async function updateTempStatus( req, res ) {
1996
4763
  update: { _index: doc._index, _id: doc._id },
1997
4764
  } );
1998
4765
  bulkPayload.push( {
1999
- doc: { actions: doc.actions },
4766
+ doc: { actions: doc.actions, isChecked: doc?.isChecked, comments: doc?.comments || '' },
2000
4767
  } );
2001
4768
  }
2002
- logger.info( { bulkPayload: bulkPayload } );
2003
-
2004
4769
 
2005
4770
  // 3. Execute bulk update
2006
4771
  const bulkResp = await bulkUpdate( bulkPayload );
@@ -2008,8 +4773,50 @@ export async function updateTempStatus( req, res ) {
2008
4773
  // Count successes
2009
4774
  const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
2010
4775
 
2011
- logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
4776
+ if ( inputData?.comments && inputData?.comments !== '' ) {
4777
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
4778
+ const searchBody1 = {
4779
+ size: 10000,
4780
+ query: {
4781
+ bool: {
4782
+ must: [
4783
+ {
4784
+ terms: {
4785
+ '_id': docIds,
4786
+ },
4787
+ },
4788
+ {
4789
+ term: {
4790
+ isParent: false,
4791
+ },
4792
+ },
4793
+ ],
4794
+ },
4795
+ },
4796
+ };
4797
+
4798
+ const getSearchResp = await getOpenSearchData(
4799
+ openSearch.revop,
4800
+ searchBody1,
4801
+ );
4802
+
2012
4803
 
4804
+ const taggedImages = getSearchResp?.body?.hits?.hits?.length > 0 ? getSearchResp?.body?.hits?.hits : [];
4805
+ const logs = {
4806
+ type: inputData.type,
4807
+ storeId: taggedImages?.[0]?._source?.storeId,
4808
+ dateString: taggedImages?.[0]?._source?.dateString,
4809
+ category: taggedImages?.[0]?._source?.revopsType || '',
4810
+ taggedImages: taggedImages,
4811
+ status: inputData?.status,
4812
+ createdByEmail: req?.user?.email,
4813
+ createdByUserName: req?.user?.userName,
4814
+ createdByRole: req?.user?.role,
4815
+ message: inputData.comments || '',
4816
+ createdAt: new Date(),
4817
+ };
4818
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
4819
+ }
2013
4820
  return res.sendSuccess( { updated: updatedCount } );
2014
4821
  } catch ( error ) {
2015
4822
  const err = error.message;
@@ -2018,3 +4825,283 @@ export async function updateTempStatus( req, res ) {
2018
4825
  }
2019
4826
  }
2020
4827
 
4828
+ export async function updateUserTicketStatus( req, res ) {
4829
+ try {
4830
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
4831
+ const { storeId, dateString } = req.body || {};
4832
+
4833
+ if ( !storeId || !dateString ) {
4834
+ return res.sendError( 'storeId and dateString are required', 400 );
4835
+ }
4836
+
4837
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
4838
+
4839
+ // Fetch existing ticket so we can validate mappingInfo state
4840
+ const existingDoc = await getOpenSearchById( openSearch.footfallDirectory, docId );
4841
+ const ticketSource = existingDoc?.body?._source;
4842
+
4843
+ if ( !ticketSource ) {
4844
+ return res.sendError( 'Ticket not found', 404 );
4845
+ }
4846
+
4847
+ const mappingInfo = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [];
4848
+ if ( mappingInfo.length === 0 ) {
4849
+ return res.sendError( 'mappingInfo is missing for this ticket', 400 );
4850
+ }
4851
+
4852
+ const lastIndex = mappingInfo.length - 1;
4853
+ const lastEntry = mappingInfo[lastIndex];
4854
+
4855
+ if ( !lastEntry ) {
4856
+ return res.sendError( 'Unable to determine current ticket status', 400 );
4857
+ }
4858
+
4859
+ if ( String( lastEntry.status ).toLowerCase() !== 'open' ) {
4860
+ return res.sendError( 'Ticket is already picked by another user', 409 );
4861
+ }
4862
+
4863
+ const currentTime = new Date();
4864
+ const updatedMappingInfo = [ ...mappingInfo ];
4865
+ updatedMappingInfo[lastIndex] = {
4866
+ ...lastEntry,
4867
+ status: 'In-Progress',
4868
+ updatedAt: currentTime,
4869
+ createdAt: currentTime,
4870
+ createdByRole: req?.user?.role || '',
4871
+ createdByEmail: req?.user?.email || '',
4872
+ createdByUserName: req?.user?.userName || '',
4873
+ };
4874
+
4875
+ const updatePayload = {
4876
+ doc: {
4877
+ status: lastEntry?.type == 'review'? 'Reviewer In progress':lastEntry?.type == 'approve'? 'Approver In progress':ticketSource?.status,
4878
+ mappingInfo: updatedMappingInfo,
4879
+ updatedAt: currentTime,
4880
+ },
4881
+ };
4882
+
4883
+ const updateResult = await updateOpenSearchData(
4884
+ openSearch.footfallDirectory,
4885
+ docId,
4886
+ updatePayload,
4887
+ );
4888
+
4889
+
4890
+ if ( !updateResult || !( updateResult.statusCode === 200 || updateResult.statusCode === 201 ) ) {
4891
+ return res.sendError( 'Failed to update ticket status', 500 );
4892
+ }
4893
+ return res.sendSuccess( 'Ticket status updated successfully' );
4894
+ } catch ( error ) {
4895
+ const err = error.message;
4896
+ logger.info( { error: err, function: 'updateUserTicketStatus' } );
4897
+ return res.sendError( err, 500 );
4898
+ }
4899
+ }
4900
+
4901
+ export async function multiCloseTicket( req, res ) {
4902
+ try {
4903
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
4904
+ const inputData = req.body;
4905
+
4906
+ // inputData structure should include an array of items to close
4907
+ // Accept both array or single ticket update
4908
+ const tickets = Array.isArray( inputData.ticketList ) ? inputData.ticketList : [ inputData.ticketList ];
4909
+ // const mode = inputData.mode || '';
4910
+
4911
+ if ( !tickets.length ) {
4912
+ return res.sendError( 'No tickets provided', 400 );
4913
+ }
4914
+
4915
+ const results = [];
4916
+ for ( const ticket of tickets ) {
4917
+ const { storeId, dateString } = ticket || {};
4918
+ if ( !storeId || !dateString ) {
4919
+ results.push( { storeId, dateString, success: false, error: 'Missing storeId or dateString' } );
4920
+ continue;
4921
+ }
4922
+ // 1. Update the ticket document in footfallDirectory index
4923
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
4924
+
4925
+ // Fetch existing doc to update mappingInfo
4926
+
4927
+ const doc = await getOpenSearchById( openSearch.footfallDirectory, docId );
4928
+
4929
+
4930
+ const ticketSource = doc?.body?._source;
4931
+ if ( !ticketSource || !ticketSource.mappingInfo ) {
4932
+ results.push( { storeId, dateString, success: false, error: 'Ticket or mappingInfo missing' } );
4933
+ continue;
4934
+ }
4935
+
4936
+ let mappingInfoArray = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [ ticketSource.mappingInfo ];
4937
+ // Find all mappingInfo items matching type 'approve'
4938
+ let updated = false;
4939
+ let newMappingInfoArray = mappingInfoArray.map( ( mi, i ) => {
4940
+ if ( mi?.type === 'approve' && mi?.status === 'Open' ) {
4941
+ updated = true;
4942
+ return {
4943
+ ...mi,
4944
+ status: 'Approver-Closed',
4945
+ status: 'Closed', // following the user's instruction to set sttaus property (assumed typo, but explicitly used)
4946
+ updatedAt: new Date(),
4947
+ };
4948
+ }
4949
+ return mi;
4950
+ } );
4951
+
4952
+ if ( !updated ) {
4953
+ // None found to update
4954
+ results.push( { storeId, dateString, success: false, error: `coudn't approve this store` } );
4955
+ continue;
4956
+ }
4957
+
4958
+ // Write update to footfallDirectory
4959
+ const ticketUpdatePayload = {
4960
+ doc: {
4961
+ mappingInfo: newMappingInfoArray,
4962
+ status: 'Approver-Closed', // status updated at top level as well
4963
+ updatedAt: new Date(),
4964
+ },
4965
+ };
4966
+ let ticketUpdateResult;
4967
+ try {
4968
+ ticketUpdateResult = await updateOpenSearchData( openSearch.footfallDirectory, docId, ticketUpdatePayload );
4969
+ if ( !( ticketUpdateResult && ( ticketUpdateResult.statusCode === 200 || ticketUpdateResult.statusCode === 201 ) ) ) {
4970
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
4971
+ continue;
4972
+ }
4973
+ } catch ( err ) {
4974
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
4975
+ continue;
4976
+ }
4977
+
4978
+
4979
+ // For each ticket, update actions array for all matching image docs (storeId & dateString)
4980
+
4981
+ // Query to find all matching docs in revopTagging index with storeId and dateString
4982
+ const revopImageQuery = {
4983
+ size: 10000, // assume there won't be more than 1000 images per ticket
4984
+ query: {
4985
+ bool: {
4986
+ must: [
4987
+ { term: { 'storeId.keyword': storeId } },
4988
+ { term: { dateString: dateString } },
4989
+ ],
4990
+ },
4991
+ },
4992
+ };
4993
+
4994
+ // Fetch matching docs
4995
+ const searchRes = await getOpenSearchData( openSearch.revop, revopImageQuery );
4996
+ const revopHits = searchRes?.body?.hits?.hits || [];
4997
+
4998
+ // Optimized: batch and parallelize image-doc updates to avoid long sequential waits
4999
+ const BATCH_SIZE = 100;
5000
+ const now = new Date();
5001
+
5002
+ for ( let i = 0; i < revopHits.length; i += BATCH_SIZE ) {
5003
+ const batch = revopHits.slice( i, i + BATCH_SIZE );
5004
+
5005
+ const updatePromises = batch.map( async ( hit ) => {
5006
+ const imageDocId = hit._id;
5007
+ const imageSource = hit._source || {};
5008
+ const imageActionsArray = Array.isArray( imageSource.actions ) ? [ ...imageSource.actions ] : [];
5009
+
5010
+ imageActionsArray.push( {
5011
+ actionType: 'approve',
5012
+ action: 'approved',
5013
+ } );
5014
+
5015
+ const imageUpdatePayload = {
5016
+ doc: {
5017
+ actions: imageActionsArray,
5018
+ updatedAt: now,
5019
+ },
5020
+ };
5021
+
5022
+ return updateOpenSearchData( openSearch.revop, imageDocId, imageUpdatePayload );
5023
+ } );
5024
+
5025
+ // Wait for this batch to finish before starting the next one
5026
+ await Promise.all( updatePromises );
5027
+ }
5028
+ }
5029
+ if ( results && results?.length > 0 ) {
5030
+ return res.sendError( results, 500 );
5031
+ }
5032
+ // Return batch summary
5033
+ } catch ( error ) {
5034
+ const err = error.message;
5035
+ logger.info( { error: err, function: 'multiCloseTicket' } );
5036
+ return res.sendError( err, 500 );
5037
+ }
5038
+ }
5039
+
5040
+
5041
+ export async function checkTicketExists( req, res ) {
5042
+ try {
5043
+ let inputData = req.body;
5044
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
5045
+ let findQuery = {
5046
+ size: 10000,
5047
+ query: {
5048
+ bool: {
5049
+ must: [
5050
+ {
5051
+ term: {
5052
+ 'storeId.keyword': inputData.storeId,
5053
+ },
5054
+ },
5055
+ {
5056
+ term: {
5057
+ 'dateString': inputData.dateString,
5058
+ },
5059
+ },
5060
+ ],
5061
+ },
5062
+ },
5063
+ };
5064
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
5065
+ let Ticket = findTicket.body?.hits?.hits;
5066
+
5067
+
5068
+ res.sendSuccess( Ticket );
5069
+ } catch ( error ) {
5070
+ const err = error.message;
5071
+ logger.info( { error: err, function: 'checkTicketExists' } );
5072
+ return res.sendError( err, 500 );
5073
+ }
5074
+ }
5075
+
5076
+
5077
+ export async function getAccuracyIssues( req, res ) {
5078
+ try {
5079
+ const inputData = req.query;
5080
+
5081
+
5082
+ const getIsues = await findStoreAccuracIssues( { clientId: inputData.clientId, isActive: true }, { issueName: 1 } );
5083
+ // const mode = inputData.mode || '';
5084
+ res.sendSuccess( { result: getIsues|| [] } );
5085
+ // Return batch summary
5086
+ } catch ( error ) {
5087
+ const err = error.message;
5088
+ logger.info( { error: err, function: 'getAccuracyIssues' } );
5089
+ return res.sendError( err, 500 );
5090
+ }
5091
+ }
5092
+
5093
+ export async function updateAccuracyIssues( req, res ) {
5094
+ try {
5095
+ const inputData = req.query;
5096
+
5097
+
5098
+ const getIsues = await upsertStoreAccuracIssues( { clientId: inputData.clientId, issueName: inputData?.issueName }, { issueName: inputData?.issueName, isActive: true } );
5099
+ // const mode = inputData.mode || '';
5100
+ res.sendSuccess( { result: getIsues|| [] } );
5101
+ // Return batch summary
5102
+ } catch ( error ) {
5103
+ const err = error.message;
5104
+ logger.info( { error: err, function: 'putAccuracyIssues' } );
5105
+ return res.sendError( err, 500 );
5106
+ }
5107
+ }