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

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,2424 @@ 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
1969
  },
291
1970
  },
292
- {
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( {
293
2234
  terms: {
294
- 'clientId.keyword': Array.isArray( inputData.clientId ) ?
295
- inputData.clientId :
296
- [ inputData.clientId ],
2235
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
2236
+ inputData.storeId :
2237
+ [ inputData.storeId ],
297
2238
  },
298
- },
299
- ];
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
+ }
300
2561
 
301
- if ( inputData?.storeId ) {
302
- filter.push(
303
- {
304
- terms: {
305
- 'storeId.keyword': Array.isArray( inputData.storeId ) ?
306
- inputData.storeId :
307
- [ inputData.storeId ],
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
+ },
308
2573
  },
309
2574
  },
310
- );
2575
+ } );
2576
+ }
311
2577
  }
312
2578
 
313
- let search = {
314
- 'must': filter,
315
- };
2579
+ if ( inputData?.filterByReviewer && inputData?.filterByReviewer !== '' ) {
2580
+ let percQuery = null;
2581
+ const value = inputData.filterByReviewer;
316
2582
 
317
- if ( inputData.searchValue && inputData.searchValue !== '' ) {
318
- search = {
319
- 'must': filter,
320
- 'should': [
321
- {
322
- 'wildcard': {
323
- 'storeName.keyword': {
324
- 'value': `*${inputData.searchValue}*`,
325
- },
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 },
326
2601
  },
327
2602
  },
328
- {
329
- 'wildcard': {
330
- 'storeId.keyword': {
331
- 'value': `*${inputData.searchValue}*`,
332
- },
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 },
333
2613
  },
334
2614
  },
335
- {
336
- 'wildcard': {
337
- 'ticketId.keyword': {
338
- 'value': `*${inputData.searchValue}*`,
339
- },
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 },
340
2627
  },
341
2628
  },
342
- {
343
- 'wildcard': {
344
- 'status.keyword': {
345
- 'value': `*${inputData.searchValue}*`,
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
+ }
2642
+
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
+ ],
346
2653
  },
347
2654
  },
348
2655
  },
2656
+ } );
2657
+ }
2658
+ }
349
2659
 
350
- ],
351
- 'minimum_should_match': 1,
2660
+ if ( inputData?.filterByApprover && inputData?.filterByApprover !== '' ) {
2661
+ let percQuery = null;
2662
+ const value = inputData.filterByApprover;
2663
+
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 );
352
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 },
2682
+ },
2683
+ },
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 },
2694
+ },
2695
+ },
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 },
2708
+ },
2709
+ },
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 ) },
2719
+ },
2720
+ },
2721
+ };
2722
+ }
2723
+
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
+ exportData.push( {
3037
+
3038
+ 'Ticket ID': item?.ticketId,
3039
+ 'Store Name': item?.storeName,
3040
+ 'Store ID': item?.storeId,
3041
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
3042
+ 'Issue Date': item?.dateString,
3043
+ 'Ticket Type': item?.type,
3044
+ 'Actual FF': item?.footfallCount,
3045
+ // Use the dueDate from the last mappingInfo item whose type is NOT 'finalRevisoon'
3046
+ 'Due Date': ( () => {
3047
+ if ( Array.isArray( item?.mappingInfo ) ) {
3048
+ const filtered = item.mappingInfo.filter( ( f ) => f.dueDate && f.type !== 'finalRevisoon' );
3049
+ return filtered.length > 0 ? filtered[filtered.length - 1].dueDate : '';
3050
+ }
3051
+ return '';
3052
+ } )(),
3053
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3054
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3055
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3056
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3057
+ 'Ticket Status': item?.status,
3058
+ 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
3059
+
3060
+ } );
3061
+ }
3062
+ return await download( exportData, res );
3063
+ } else {
3064
+ for ( let item of ticketListData ) {
3065
+ temp.push( {
3066
+
3067
+ ticketId: item?.ticketId,
3068
+ storeId: item?.storeId,
3069
+ storeName: item?.storeName,
3070
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
3071
+ issueDate: item?.dateString,
3072
+ footfall: item?.footfallCount,
3073
+ dueDate: ( () => {
3074
+ if ( Array.isArray( item?.mappingInfo ) ) {
3075
+ const filtered = item.mappingInfo.filter( ( f ) => f.dueDate && f.type !== 'finalRevisoon' );
3076
+ return filtered.length > 0 ? filtered[filtered.length - 1].dueDate : '';
3077
+ }
3078
+ return '';
3079
+ } )(),
3080
+ type: item?.type || 'store',
3081
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3082
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3083
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3084
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3085
+ status: item?.status,
3086
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
3087
+
3088
+ } );
3089
+ }
586
3090
  }
587
3091
  }
588
3092
  } 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
- } );
3093
+ if ( inputData?.permissionType === 'approve' ) {
3094
+ if ( inputData.exportData ) {
3095
+ const exportData = [];
3096
+ for ( let item of ticketListData ) {
3097
+ exportData.push( {
3098
+
3099
+ 'Ticket ID': item?.ticketId,
3100
+ 'Store Name': item?.storeName,
3101
+ 'Store ID': item?.storeId,
3102
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3103
+ 'Issue Date': item?.dateString,
3104
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3105
+ 'Actual FF': item?.footfallCount,
3106
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3107
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3108
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3109
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3110
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3111
+ 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3112
+ 'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3113
+
3114
+ } );
3115
+ }
3116
+ return await download( exportData, res );
3117
+ } else {
3118
+ for ( let item of ticketListData ) {
3119
+ temp.push( {
3120
+
3121
+ ticketId: item?.ticketId,
3122
+ storeId: item?.storeId,
3123
+ storeName: item?.storeName,
3124
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3125
+ issueDate: item?.dateString,
3126
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3127
+ footfall: item?.footfallCount,
3128
+ type: item.type || 'store',
3129
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3130
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3131
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3132
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3133
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3134
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3135
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3136
+
3137
+ } );
3138
+ }
3139
+ }
3140
+ } else if ( inputData?.permissionType === 'review' ) {
3141
+ if ( inputData?.isExport ) {
3142
+ const exportData = [];
3143
+ for ( let item of ticketListData ) {
3144
+ exportData.push( {
3145
+
3146
+ 'Ticket ID': item?.ticketId,
3147
+ 'Store Name': item?.storeName,
3148
+ 'Store ID': item?.storeId,
3149
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3150
+ 'Issue Date': item?.dateString,
3151
+ 'Actual FF': item?.footfallCount,
3152
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3153
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3154
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3155
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3156
+ 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3157
+
3158
+ } );
3159
+ }
3160
+ return await download( exportData, res );
3161
+ } else {
3162
+ for ( let item of ticketListData ) {
3163
+ temp.push( {
3164
+
3165
+ ticketId: item?.ticketId,
3166
+ storeId: item?.storeId,
3167
+ storeName: item?.storeName,
3168
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3169
+ issueDate: item?.dateString,
3170
+ footfall: item?.footfallCount,
3171
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3172
+ type: item.type || 'store',
3173
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3174
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3175
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3176
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3177
+
3178
+ } );
3179
+ }
612
3180
  }
613
3181
  } else if ( req.user.role === 'user' ) {
614
3182
  temp = [];
615
3183
  } 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
- } );
3184
+ if ( inputData?.isExport ) {
3185
+ const exportData = [];
3186
+ for ( let item of ticketListData ) {
3187
+ exportData.push( {
3188
+
3189
+ 'Ticket ID': item?.ticketId,
3190
+ 'Store ID': item?.storeId,
3191
+ 'Store Name': item?.storeName,
3192
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3193
+ 'Issue Date': item?.dateString,
3194
+ 'Actual FF': item?.footfallCount,
3195
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3196
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3197
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3198
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3199
+ 'Reviewed by': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3200
+
3201
+ } );
3202
+ }
3203
+ return await download( exportData, res );
3204
+ } else {
3205
+ for ( let item of ticketListData ) {
3206
+ temp.push( {
3207
+
3208
+ ticketId: item?.ticketId,
3209
+ storeId: item?.storeId,
3210
+ storeName: item?.storeName,
3211
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdAt,
3212
+ issueDate: item?.dateString,
3213
+ footfall: item?.footfallCount,
3214
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.dueDate,
3215
+ type: item.type || 'store',
3216
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3217
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3218
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
3219
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
3220
+
3221
+ } );
3222
+ }
3223
+ }
3224
+ } else if ( ticketsApproveFeature ) {
3225
+ if ( inputData.isExport ) {
3226
+ const exportData = [];
3227
+ for ( let item of ticketListData ) {
3228
+ exportData.push( {
3229
+
3230
+ 'Ticket ID': item?.ticketId,
3231
+ 'Store Name': item?.storeName,
3232
+ 'Store ID': item?.storeId,
3233
+ 'Ticket Raised': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3234
+ 'Issue Date': item?.dateString,
3235
+ 'Due Date': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3236
+ 'Actual FF': item?.footfallCount,
3237
+ 'Store (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3238
+ 'Reviewer (%)': item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3239
+ 'Approver (%)': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3240
+ 'Tango (%)': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3241
+ 'Status': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3242
+ 'Tango Status': item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3243
+ 'Approved by': item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3244
+
3245
+ } );
3246
+ }
3247
+ return await download( exportData, res );
3248
+ } else {
3249
+ for ( let item of ticketListData ) {
3250
+ temp.push( {
3251
+
3252
+ ticketId: item?.ticketId,
3253
+ storeId: item?.storeId,
3254
+ storeName: item?.storeName,
3255
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdAt,
3256
+ issueDate: item?.dateString,
3257
+ dueDate: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.dueDate,
3258
+ footfall: item?.footfallCount,
3259
+ type: item.type || 'store',
3260
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
3261
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
3262
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
3263
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3264
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
3265
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
3266
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
3267
+
3268
+ } );
3269
+ }
634
3270
  }
635
3271
  } else {
636
- temp =[];
3272
+ temp = [];
637
3273
  }
638
3274
  }
639
3275
 
640
- return res.sendSuccess( { result: temp } );
3276
+ return res.sendSuccess( { result: temp, count: count } );
641
3277
  } catch ( error ) {
642
3278
  const err = error.message || 'Internal Server Error';
643
3279
  logger.error( { error: error, messgage: req.query } );
@@ -649,149 +3285,19 @@ export async function getTickets( req, res ) {
649
3285
  try {
650
3286
  const openSearch = JSON.parse( process.env.OPENSEARCH );
651
3287
  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' ];
3288
+
3289
+
3290
+ let source = [ 'storeId', '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
3291
  let filter = [
658
3292
 
659
3293
  {
660
- range: {
661
- dateString: {
662
- gte: inputData.fromDate,
663
- lte: inputData.toDate,
664
- format: 'yyyy-MM-dd',
665
- },
3294
+ term: {
3295
+ 'ticketId.keyword': inputData.ticketId,
666
3296
  },
667
3297
  },
668
3298
  ];
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
3299
 
776
3300
 
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
3301
  const getCount = {
796
3302
  query: {
797
3303
  bool: {
@@ -802,20 +3308,14 @@ export async function getTickets( req, res ) {
802
3308
 
803
3309
 
804
3310
  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 );
3311
+
3312
+
3313
+ if ( !geteDataCount ) {
3314
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
814
3315
  }
815
3316
 
816
3317
  const getQuery = {
817
- size: limit,
818
- from: skip,
3318
+ size: 1,
819
3319
  query: {
820
3320
  bool: {
821
3321
  filter: filter,
@@ -825,14 +3325,9 @@ export async function getTickets( req, res ) {
825
3325
  };
826
3326
 
827
3327
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
828
-
829
3328
  const response = getData?.body?.hits?.hits;
830
3329
  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 );
3330
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
836
3331
  }
837
3332
  let temp = [];
838
3333
  if ( inputData?.action ) {
@@ -870,29 +3365,31 @@ export async function getTickets( req, res ) {
870
3365
  approverUserName: hit?._source?.approverUserName,
871
3366
  approverEmail: hit?._source?.approverEmail,
872
3367
  approverRole: hit?._source?.approverRole,
3368
+ type: hit?._source?.type,
873
3369
  };
874
3370
  let result;
875
3371
 
3372
+
876
3373
  const matched = hit.matched_queries;
877
3374
  // Add only matched data array
878
- if ( matched.includes( 'matched_employee' )&& ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
3375
+ if ( matched.includes( 'matched_employee' ) && ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
879
3376
  result = defaultData;
880
3377
  result.employee = hit?._source?.employee;
881
3378
  // result.type = 'employee';
882
3379
  result.matched = matched;
883
3380
  }
884
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
3381
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
885
3382
  result = defaultData;
886
3383
  result.houseKeeping = hit?._source?.houseKeeping;
887
3384
  result.matched = matched;
888
3385
  }
889
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
3386
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
890
3387
  result = defaultData;
891
3388
  result.duplicateImages = hit?._source?.duplicateImages;
892
3389
  result.matched = matched;
893
3390
  }
894
3391
 
895
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
3392
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
896
3393
  result = defaultData;
897
3394
  result.junk = hit?._source?.junk;
898
3395
  result.matched = matched;
@@ -911,15 +3408,264 @@ export async function getTickets( req, res ) {
911
3408
  }
912
3409
  } );
913
3410
  }
3411
+
914
3412
  const finalResponse = inputData.action ? temp : response;
3413
+ const getRevopQuery = {
3414
+ size: 10000,
3415
+ query: {
3416
+ bool: {
3417
+ must: [
3418
+ { term: { 'storeId.keyword': response?.[0]?._source?.storeId } }, // assuming inputData.storeId is an array
3419
+ { term: { 'dateString': response?.[0]?._source?.dateString } },
3420
+ ],
3421
+ },
3422
+ },
3423
+ };
3424
+
3425
+
3426
+ const revopResp = await getOpenSearchData( openSearch.revop, getRevopQuery );
3427
+
3428
+
3429
+ // Map revopResp.body.hits.hits to revopSources
3430
+ const revopSources = Array.isArray( revopResp?.body?.hits?.hits ) ?
3431
+ revopResp?.body?.hits?.hits?.map( ( hit ) => hit._source ) :
3432
+ [];
3433
+
3434
+ // Create a map of revopSources by id for quick lookup
3435
+ const revopSourcesMap = new Map();
3436
+ revopSources.forEach( ( item ) => {
3437
+ if ( item?.id ) {
3438
+ revopSourcesMap.set( String( item.id ), item );
3439
+ }
3440
+ } );
3441
+
3442
+ // Process revopSources to replace duplicateImage entries with full objects from the array
3443
+ const processedRevopSources = revopSources.map( ( item ) => {
3444
+ // Check if this is a duplicate parent item
3445
+ if ( item?.revopsType === 'duplicate' && item?.isParent === true && Array.isArray( item?.duplicateImage ) ) {
3446
+ // Map each duplicateImage entry to the full object from revopSources
3447
+ const updatedDuplicateImage = item.duplicateImage.map( ( duplicateImg ) => {
3448
+ const duplicateId = String( duplicateImg?.id );
3449
+ // Find the full object in revopSources that matches this id
3450
+ const fullObject = revopSourcesMap.get( duplicateId );
3451
+ // Return the full object if found, otherwise return the original duplicateImg
3452
+ return fullObject || duplicateImg;
3453
+ } );
3454
+
3455
+ return {
3456
+ ...item,
3457
+ duplicateImage: updatedDuplicateImage,
3458
+ };
3459
+ }
3460
+ // Return item as-is if it doesn't meet the criteria
3461
+ return item;
3462
+ } );
3463
+
915
3464
  if ( finalResponse?.length == 0 || !finalResponse ) {
916
3465
  if ( inputData.storeId?.length > 0 ) {
917
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
3466
+ const getStoreName = await findOneStore( { storeId: response?.[0]?._source?.storeId }, { storeName: 1 } );
918
3467
  return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
919
3468
  }
920
3469
  return res.sendError( 'No Data found', 204 );
921
3470
  }
922
- return res.sendSuccess( { result: finalResponse, count: count, revopCount: revCount } );
3471
+
3472
+
3473
+ // replace the mappingInfo.revisedDetail with processedRevopSources
3474
+ if ( Array.isArray( finalResponse ) ) {
3475
+ for ( let item of finalResponse ) {
3476
+ if (
3477
+ item &&
3478
+ item._source &&
3479
+ item._source.mappingInfo
3480
+
3481
+ ) {
3482
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
3483
+ const vmsCommentsLogIndex = openSearch.vmsCommentsLog || 'vms-comments-log-dev';
3484
+ let commentsResponse = [];
3485
+ const commentsFilter = [
3486
+ { term: { 'storeId.keyword': item?._source?.storeId } },
3487
+ { term: { dateString: item?._source?.dateString } },
3488
+
3489
+ ];
3490
+
3491
+ const commentsQuery = {
3492
+ size: 10000,
3493
+ sort: [
3494
+ { createdAt: { order: 'desc' } }, // Sort descending by createdAt
3495
+ ],
3496
+ query: {
3497
+ bool: {
3498
+ filter: commentsFilter,
3499
+ },
3500
+ },
3501
+ };
3502
+
3503
+ const commentsRes = await getOpenSearchData( vmsCommentsLogIndex, commentsQuery );
3504
+ // If mappingInfo is an array, update revisedDetail for each mappingInfo object
3505
+ if ( Array.isArray( item._source.mappingInfo ) ) {
3506
+ item._source.mappingInfo.forEach( ( mappingObj ) => {
3507
+ commentsResponse = commentsRes?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
3508
+
3509
+ // Check if duplicate condition exists in commentsResponse
3510
+
3511
+ // Structure comments output
3512
+ let commentsDetails = [];
3513
+
3514
+
3515
+ const types = [ 'tagging', 'review', 'approve' ];
3516
+
3517
+ // Process each type
3518
+ types.forEach( ( typeValue ) => {
3519
+ if ( typeValue === 'tagging' ) {
3520
+ // For tagging, group by category and create separate objects for each category
3521
+ const taggingComments = commentsResponse.filter( ( c ) => c.type === typeValue );
3522
+
3523
+ // Group by category
3524
+ const categoryGroups = {};
3525
+ taggingComments.forEach( ( c ) => {
3526
+ const category = c.category || 'other';
3527
+ if ( !categoryGroups[category] ) {
3528
+ categoryGroups[category] = [];
3529
+ }
3530
+ categoryGroups[category].push( c );
3531
+ } );
3532
+
3533
+ // Create separate objects for each category
3534
+ Object.keys( categoryGroups ).forEach( ( category ) => {
3535
+ const categoryComments = categoryGroups[category];
3536
+ let parent = null;
3537
+
3538
+ const comms = categoryComments.map( ( c ) => {
3539
+ if ( category === 'duplicate' ) {
3540
+ if ( !parent && c.parent ) {
3541
+ parent = c.parent;
3542
+ }
3543
+ return {
3544
+ createdByEmail: c.createdByEmail,
3545
+ createdByUserName: c.createdByUserName,
3546
+ createdByRole: c.createdByRole,
3547
+ message: c.message,
3548
+ };
3549
+ } else {
3550
+ return {
3551
+ id: c.id,
3552
+ tempId: c.tempId,
3553
+ timeRange: c.timeRange,
3554
+ entryTime: c.entryTime,
3555
+ exitTime: c.exitTime,
3556
+ filePath: c.filePath,
3557
+ isChecked: c.isChecked,
3558
+ createdAt: c.createdAt,
3559
+ message: c.message,
3560
+ createdByEmail: c.createdByEmail,
3561
+ createdByUserName: c.createdByUserName,
3562
+ createdByRole: c.createdByRole,
3563
+ };
3564
+ }
3565
+ } );
3566
+
3567
+ const taggingObj = {
3568
+ category: category,
3569
+ type: typeValue,
3570
+ comments: comms,
3571
+ };
3572
+
3573
+ // Add parent only for duplicate category
3574
+ if ( category === 'duplicate' && parent !== null ) {
3575
+ taggingObj.parent = parent;
3576
+ }
3577
+
3578
+ commentsDetails.push( taggingObj );
3579
+ } );
3580
+ } else if ( typeValue === 'review' || typeValue === 'approve' ) {
3581
+ // For review and approve, keep existing structure
3582
+ const comms = commentsResponse
3583
+ .filter( ( c ) => c.type === typeValue )
3584
+ .map( ( c ) => {
3585
+ if ( c.category === 'duplicate' ) {
3586
+ return {
3587
+ parent: c?.taggedImages[0]?._source?.parent,
3588
+ category: c.category,
3589
+ taggedImages: Array.isArray( c.taggedImages ) ?
3590
+ c.taggedImages.map( ( img ) => ( {
3591
+ id: img?._source?.id,
3592
+ tempId: img?._source?.tempId,
3593
+ timeRange: img?._source?.timeRange,
3594
+ entryTime: img?._source?.entryTime,
3595
+ exitTime: img?._source?.exitTime,
3596
+ filePath: img?._source?.filePath,
3597
+ isChecked: img?._source?.isChecked,
3598
+ } ) ) :
3599
+ [],
3600
+ createdByEmail: c.createdByEmail,
3601
+ createdByUserName: c.createdByUserName,
3602
+ createdByRole: c.createdByRole,
3603
+ status: c.status,
3604
+ message: c.message,
3605
+ };
3606
+ } else {
3607
+ return {
3608
+ category: c.category,
3609
+ taggedImages: Array.isArray( c.taggedImages ) ?
3610
+ c.taggedImages.map( ( img ) => ( {
3611
+ id: img?._source?.id,
3612
+ tempId: img?._source?.tempId,
3613
+ timeRange: img?._source?.timeRange,
3614
+ entryTime: img?._source?.entryTime,
3615
+ exitTime: img?._source?.exitTime,
3616
+ filePath: img?._source?.filePath,
3617
+ isChecked: img?._source?.isChecked,
3618
+ } ) ) :
3619
+ [],
3620
+ createdByEmail: c.createdByEmail,
3621
+ createdByUserName: c.createdByUserName,
3622
+ createdByRole: c.createdByRole,
3623
+ status: c.status,
3624
+ message: c.message,
3625
+ };
3626
+ }
3627
+ } );
3628
+
3629
+ // Only add if there are comments
3630
+ if ( comms.length > 0 ) {
3631
+ commentsDetails.push( {
3632
+ type: typeValue,
3633
+ comments: comms,
3634
+ } );
3635
+ } else {
3636
+ // Add empty comments array if no comments
3637
+ commentsDetails.push( {
3638
+ type: typeValue,
3639
+ comments: [],
3640
+ } );
3641
+ }
3642
+ }
3643
+ } );
3644
+
3645
+
3646
+ item._source.commentsDetails = commentsDetails;
3647
+
3648
+ if (
3649
+ Object.prototype.hasOwnProperty.call( mappingObj, 'revisedDetail' ) &&
3650
+ mappingObj.type !== 'tangoreview'
3651
+ ) {
3652
+ mappingObj.revisedDetail = processedRevopSources;
3653
+ }
3654
+ } );
3655
+ } else {
3656
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
3657
+ }
3658
+ }
3659
+ }
3660
+ } else if (
3661
+ finalResponse &&
3662
+ finalResponse._source &&
3663
+ finalResponse._source.mappingInfo
3664
+ ) {
3665
+ finalResponse._source.mappingInfo.revisedDetail = processedRevopSources;
3666
+ }
3667
+
3668
+ return res.sendSuccess( { result: finalResponse } );
923
3669
  } catch ( error ) {
924
3670
  const err = error.message || 'Internal Server Error';
925
3671
  logger.error( { error: error, messgage: req.query } );
@@ -991,7 +3737,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
991
3737
  item.isChecked == true ? tempId.push( { tempId: item.tempId, timeRange: item.timeRange } ) : null;
992
3738
  bulkBody.push(
993
3739
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${item.timeRange}_${item.tempId}` } },
994
- { doc: { isChecked: item.isChecked, status: item?.isChecked == true? 'approved':'rejected' } },
3740
+ { doc: { isChecked: item.isChecked, status: item?.isChecked == true ? 'approved' : 'rejected' } },
995
3741
  );
996
3742
  } );
997
3743
  }
@@ -1006,11 +3752,11 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1006
3752
  updateData.employee = updatedEmployee;
1007
3753
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: updateData } );
1008
3754
  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
- );
3755
+ ( employee.isChecked == true ) ? tempId.push( { tempId: employee.tempId, timeRange: employee.timeRange } ) : null;
3756
+ bulkBody.push(
3757
+ { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
3758
+ { doc: { isChecked: employee.isChecked, status: employee?.isChecked == true ? 'approved' : 'rejected' } },
3759
+ );
1014
3760
  }
1015
3761
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { employeeACCount: tempId?.length || 0 } } );
1016
3762
  }
@@ -1029,7 +3775,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1029
3775
  houseKeeping.isChecked == true ? tempId.push( { tempId: houseKeeping.tempId, timeRange: houseKeeping.timeRange } ) : null;
1030
3776
  bulkBody.push(
1031
3777
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${houseKeeping.timeRange}_${houseKeeping.tempId}` } },
1032
- { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true? 'approved':'rejected' } },
3778
+ { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true ? 'approved' : 'rejected' } },
1033
3779
  );
1034
3780
  }
1035
3781
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { houseKeepingACCount: tempId?.length || 0 } } );
@@ -1050,7 +3796,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1050
3796
  junk.isChecked == true ? tempId.push( { tempId: junk.tempId, timeRange: junk.timeRange } ) : null;
1051
3797
  bulkBody.push(
1052
3798
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${junk.timeRange}_${junk.tempId}` } },
1053
- { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true? 'approved':'rejected' } },
3799
+ { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true ? 'approved' : 'rejected' } },
1054
3800
  );
1055
3801
  }
1056
3802
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { junkACCount: tempId?.length || 0 } } );
@@ -1075,7 +3821,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1075
3821
  let getUpdateExistingOne = await getOpenSearchById( openSearch.footfallDirectory, _id );
1076
3822
  const tempIdList = await extractCheckedTempIds( getUpdateExistingOne?.body );
1077
3823
  const isSendMessge = await sendSqsMessage( data, tempIdList, getStoreType, storeId );
1078
- if ( isSendMessge ==true ) {
3824
+ if ( isSendMessge == true ) {
1079
3825
  return true; // res.sendSuccess( 'Ticket has been updated successfully' );
1080
3826
  } else {
1081
3827
  return false; // res.sendError( 'No SQS message sent', 500 );
@@ -1138,7 +3884,7 @@ function updateEmployeeCheckFlags( existingEmployees, inputEmployees, status ) {
1138
3884
  // Step 2: Loop through all existing and update isChecked accordingly
1139
3885
  const updatedEmployees = existingEmployees.map( ( emp ) => ( {
1140
3886
  ...emp,
1141
- isChecked: status === 'rejected'? !checkedTempIds.has( emp.tempId ):checkedTempIds.has( emp.tempId ),
3887
+ isChecked: status === 'rejected' ? !checkedTempIds.has( emp.tempId ) : checkedTempIds.has( emp.tempId ),
1142
3888
  } ) );
1143
3889
 
1144
3890
  return updatedEmployees;
@@ -1188,7 +3934,7 @@ function mergeDuplicateImagesWithUncheck( existingData, inputData, status ) {
1188
3934
  export async function sendSqsMessage( inputData, tempId, getStoreType, storeId ) {
1189
3935
  const sqs = JSON.parse( process.env.SQS );
1190
3936
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1191
- const sqsName = getStoreType > 0? sqs.revopTrackTicket: sqs.revopTicket;
3937
+ const sqsName = getStoreType > 0 ? sqs.revopTrackTicket : sqs.revopTicket;
1192
3938
  const sqsProduceQueue = getStoreType > 0 ? {
1193
3939
  QueueUrl: `${sqs.url}${sqsName}`,
1194
3940
  MessageBody: JSON.stringify( {
@@ -1216,12 +3962,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1216
3962
 
1217
3963
  } ),
1218
3964
  };
1219
- const sqsQueue = getStoreType > 0? await sendMessageToFIFOQueue( sqsProduceQueue ):
1220
- await sendMessageToQueue(
1221
- sqsProduceQueue.QueueUrl,
1222
- sqsProduceQueue.MessageBody,
1223
-
1224
- );
3965
+ const sqsQueue = getStoreType > 0 ? await sendMessageToFIFOQueue( sqsProduceQueue ) :
3966
+ await sendMessageToQueue(
3967
+ sqsProduceQueue.QueueUrl,
3968
+ sqsProduceQueue.MessageBody,
3969
+ );
1225
3970
  if ( sqsQueue.statusCode ) {
1226
3971
  logger.error( {
1227
3972
  error: `${sqsQueue}`,
@@ -1229,7 +3974,7 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1229
3974
  } );
1230
3975
  return false;
1231
3976
  } else {
1232
- const id =`${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0? 'live':'reduction'}_${Date.now()}`;
3977
+ const id = `${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0 ? 'live' : 'reduction'}_${Date.now()}`;
1233
3978
  const logs = {
1234
3979
  QueueUrl: sqsProduceQueue.QueueUrl,
1235
3980
  MessageBody: sqsProduceQueue.MessageBody,
@@ -1279,13 +4024,7 @@ export async function getTaggedStores( req, res ) {
1279
4024
  },
1280
4025
  },
1281
4026
  ];
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
- // }
4027
+
1289
4028
  filter.push(
1290
4029
  {
1291
4030
  terms: { 'storeId.keyword': req?.stores || [] },
@@ -1336,7 +4075,7 @@ export async function getTaggedStores( req, res ) {
1336
4075
  + (doc.containsKey('employeeCount') && !doc['employeeCount'].empty ? doc['employeeCount'].value : 0)
1337
4076
  + (doc.containsKey('junkCount') && !doc['junkCount'].empty ? doc['junkCount'].value : 0);
1338
4077
  `,
1339
- lang: 'painless',
4078
+ lang: 'scripting',
1340
4079
  },
1341
4080
  },
1342
4081
  },
@@ -1400,8 +4139,8 @@ export async function downloadTickets( req, res ) {
1400
4139
  {
1401
4140
  terms: {
1402
4141
  'storeId.keyword': Array.isArray( inputData.storeId ) ?
1403
- inputData.storeId :
1404
- inputData.storeId,
4142
+ inputData.storeId :
4143
+ inputData.storeId,
1405
4144
  },
1406
4145
  },
1407
4146
  );
@@ -1449,26 +4188,26 @@ export async function downloadTickets( req, res ) {
1449
4188
  },
1450
4189
 
1451
4190
  } ) :
1452
- inputData.revopsType === 'junk' ?
1453
- filter.push( {
1454
- range: {
1455
- junkCount: {
1456
- gt: 0,
4191
+ inputData.revopsType === 'junk' ?
4192
+ filter.push( {
4193
+ range: {
4194
+ junkCount: {
4195
+ gt: 0,
4196
+ },
1457
4197
  },
1458
- },
1459
- } ):
4198
+ } ) :
1460
4199
 
1461
- filter.push( {
1462
- range: {
1463
- duplicateCount: {
1464
- gt: 0,
4200
+ filter.push( {
4201
+ range: {
4202
+ duplicateCount: {
4203
+ gt: 0,
4204
+ },
1465
4205
  },
1466
- },
1467
- } );
4206
+ } );
1468
4207
  source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1469
4208
  inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1470
4209
  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' ] : [];
4210
+ inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'junkCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] : [];
1472
4211
  }
1473
4212
 
1474
4213
  if ( inputData.action ) {
@@ -1545,7 +4284,7 @@ export async function downloadTickets( req, res ) {
1545
4284
  let temp = [];
1546
4285
  if ( inputData?.action ) {
1547
4286
  response.map( ( hit ) => {
1548
- const defaultData ={
4287
+ const defaultData = {
1549
4288
  storeId: hit._source.storeId,
1550
4289
  dateString: hit?._source?.dateString,
1551
4290
  ticketName: hit?._source?.ticketName,
@@ -1575,19 +4314,19 @@ export async function downloadTickets( req, res ) {
1575
4314
  // result.type = 'employee';
1576
4315
  result.matched = matched;
1577
4316
  }
1578
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
4317
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
1579
4318
  logger.info( { revop: inputData.revopsType } );
1580
4319
  result = defaultData;
1581
4320
  result.houseKeeping = hit?._source?.houseKeeping;
1582
4321
  result.matched = matched;
1583
4322
  }
1584
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
4323
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
1585
4324
  logger.info( { revop: inputData.revopsType } );
1586
4325
  result = defaultData;
1587
4326
  result.duplicateImages = hit?._source?.duplicateImages;
1588
4327
  result.matched = matched;
1589
4328
  }
1590
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
4329
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
1591
4330
  result = defaultData;
1592
4331
  result.junk = hit?._source?.junk;
1593
4332
  result.matched = matched;
@@ -1631,7 +4370,7 @@ export async function downloadTickets( req, res ) {
1631
4370
  revopsType: inputData?.revopsType,
1632
4371
  type: 'get-tickets',
1633
4372
  };
1634
- const record={
4373
+ const record = {
1635
4374
  stores: inputData?.storeId,
1636
4375
  fromDate: inputData?.fromDate,
1637
4376
  toDate: inputData?.toDate,
@@ -1649,7 +4388,7 @@ export async function downloadTickets( req, res ) {
1649
4388
  const sqs = JSON.parse( process.env.SQS );
1650
4389
  const sqsName = sqs.revopDownload;
1651
4390
 
1652
- const sqsProduceQueue ={
4391
+ const sqsProduceQueue = {
1653
4392
  QueueUrl: `${sqs.url}${sqsName}`,
1654
4393
  MessageBody: JSON.stringify( {
1655
4394
  _id: getId?._id,
@@ -1678,7 +4417,6 @@ export async function downloadTickets( req, res ) {
1678
4417
  }
1679
4418
 
1680
4419
  async function extractTempIds( document ) {
1681
- logger.info( { document: document } );
1682
4420
  const source = document?._source || {};
1683
4421
  const result = [];
1684
4422
 
@@ -1728,7 +4466,7 @@ export async function reviewerList( req, res ) {
1728
4466
  featureName: 'FootfallDirectory',
1729
4467
  modules: {
1730
4468
  $elemMatch: {
1731
- name: 'Reviewer',
4469
+ name: inputData?.type === 'review' ? 'reviewer' : 'approver',
1732
4470
  $or: [ { isAdd: true }, { isEdit: true } ],
1733
4471
  },
1734
4472
  },
@@ -1737,7 +4475,7 @@ export async function reviewerList( req, res ) {
1737
4475
  };
1738
4476
 
1739
4477
  const getUserlist = await findUser( reviewerRoleQuery, { userName: 1, email: 1, role: 1 } );
1740
- return res.sendSuccess( getUserlist|| [] );
4478
+ return res.sendSuccess( getUserlist || [] );
1741
4479
  } catch ( error ) {
1742
4480
  const err = error.message || 'Internal Server Error';
1743
4481
  return res.sendError( err, 500 );
@@ -1747,6 +4485,7 @@ export async function reviewerList( req, res ) {
1747
4485
  export async function openTicketList( req, res ) {
1748
4486
  try {
1749
4487
  const inputData = req.body;
4488
+ logger.info( { inputData } );
1750
4489
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1751
4490
 
1752
4491
  // INSERT_YOUR_CODE
@@ -1759,6 +4498,29 @@ export async function openTicketList( req, res ) {
1759
4498
  clientId: Array.isArray( clientId ) ? clientId : [ clientId ],
1760
4499
  },
1761
4500
  },
4501
+ {
4502
+ nested: {
4503
+ path: 'mappingInfo',
4504
+ query: {
4505
+ bool: {
4506
+ must: [
4507
+ {
4508
+ term: {
4509
+ 'mappingInfo.type': inputData.type,
4510
+ },
4511
+ },
4512
+ {
4513
+ term: {
4514
+ 'mappingInfo.status': 'Open',
4515
+ },
4516
+ },
4517
+
4518
+ ],
4519
+ },
4520
+ },
4521
+ },
4522
+ },
4523
+
1762
4524
  {
1763
4525
  range: {
1764
4526
  dateString: {
@@ -1770,6 +4532,7 @@ export async function openTicketList( req, res ) {
1770
4532
  },
1771
4533
  ];
1772
4534
 
4535
+
1773
4536
  const openSearchQuery = {
1774
4537
  size: 10000,
1775
4538
  query: {
@@ -1777,14 +4540,44 @@ export async function openTicketList( req, res ) {
1777
4540
  filter: filter,
1778
4541
  },
1779
4542
  },
1780
- _source: [ 'ticketId', 'storeName', 'revicedFootfall', 'footfallCount', 'revicedPerc' ],
4543
+ _source: [ 'ticketId', 'storeName', 'storeId', 'dateString', 'revicedFootfall', 'footfallCount', 'revicedPerc', 'type' ],
1781
4544
  };
1782
4545
 
4546
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
4547
+ openSearchQuery.query.bool['should'] = [];
4548
+ openSearchQuery.query.bool.should = [
4549
+
4550
+ {
4551
+ 'wildcard': {
4552
+ 'storeName.keyword': {
4553
+ 'value': `*${inputData.searchValue}*`,
4554
+ },
4555
+ },
4556
+ },
4557
+ {
4558
+ 'wildcard': {
4559
+ 'ticketId.keyword': {
4560
+ 'value': `*${inputData.searchValue}*`,
4561
+ },
4562
+ },
4563
+ },
4564
+
4565
+
4566
+ ];
4567
+ openSearchQuery.query.bool['minimum_should_match'] = 1;
4568
+ }
4569
+
4570
+ // INSERT_YOUR_CODE
4571
+ // Add sorting by revicedPerc descending (highest revised accuracy first)
4572
+ openSearchQuery.sort = [
4573
+ { 'revicedPerc.keyword': { order: inputData?.sortOrder === 1 ? 'asc' : 'desc' } },
4574
+ ];
4575
+
1783
4576
 
1784
4577
  // Assuming getOpenSearchData and openSearch.footfallDirectoryTagging are available
1785
4578
  const result = await getOpenSearchData( openSearch.footfallDirectory, openSearchQuery );
1786
4579
  const getUserlist = result?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1787
- return res.sendSuccess( getUserlist|| [] );
4580
+ return res.sendSuccess( getUserlist || [] );
1788
4581
  } catch ( error ) {
1789
4582
  const err = error.message || 'Internal Server Error';
1790
4583
  logger.error( { error: error, function: 'openTicketList' } );
@@ -1797,73 +4590,32 @@ export async function assignTicket( req, res ) {
1797
4590
  const inputData = req.body;
1798
4591
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1799
4592
 
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
-
4593
+ const { email, userName, role, storeId, dateString } = inputData;
4594
+ const _id = `${storeId}_${dateString}_footfall-directory-tagging`;
1863
4595
 
1864
- logger.info( { response } );
4596
+ const getTicket = await getOpenSearchById( openSearch.footfallDirectory, _id );
4597
+ if ( !getTicket ) {
4598
+ return res.sendError( 'Ticket is not found', 400 );
4599
+ }
4600
+ const source = getTicket?.body?._source;
4601
+ const mappingInfo = Array.isArray( source.mappingInfo ) ? source.mappingInfo : [];
4602
+ if ( mappingInfo.length === 0 ) {
4603
+ return res.sendError( 'Ticket is not found', 400 );
4604
+ }
4605
+ const lastIndex = mappingInfo.length - 1;
4606
+ const lastEntry = mappingInfo[lastIndex];
4607
+ if ( String( lastEntry.status ) !== 'In-Progress' ) {
4608
+ return res.sendError( 'Ticket is not in progress', 400 );
4609
+ }
1865
4610
 
1866
- return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
4611
+ const currentTime = new Date();
4612
+ const updatedMappingInfo = [ ...mappingInfo ];
4613
+ updatedMappingInfo[lastIndex] = { ...lastEntry, createdByEmail: email, updatedAt: currentTime, createdByUserName: userName, createdByRole: role };
4614
+ const updateResult = await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { mappingInfo: updatedMappingInfo } } );
4615
+ if ( !updateResult ) {
4616
+ return res.sendError( 'Failed to update ticket', 400 );
4617
+ }
4618
+ return res.sendSuccess( { message: 'Ticket assigned successfully' } );
1867
4619
  } catch ( error ) {
1868
4620
  const err = error.message || 'Internal Server Error';
1869
4621
  logger.error( { error: error, function: 'assignTicket' } );
@@ -1874,11 +4626,13 @@ export async function assignTicket( req, res ) {
1874
4626
  export async function updateTempStatus( req, res ) {
1875
4627
  try {
1876
4628
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1877
- const { id, status } = req.body;
4629
+ const inputData = req.body;
4630
+ const { id, type, status, comments } = inputData;
1878
4631
 
1879
4632
  // Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
1880
4633
  // 1. Search for all documents matching the ticket IDs
1881
4634
  const searchBody = {
4635
+ size: 10000,
1882
4636
  query: {
1883
4637
  bool: {
1884
4638
  must: [
@@ -1897,16 +4651,14 @@ export async function updateTempStatus( req, res ) {
1897
4651
  openSearch.revop,
1898
4652
  searchBody,
1899
4653
  );
1900
- logger.info( { searchResp: searchResp } );
4654
+
1901
4655
  // Extract bulk IDs to update
1902
4656
  const hits = searchResp?.body?.hits?.hits ?? [];
1903
- logger.info( { hits: hits } );
4657
+
1904
4658
  if ( !hits.length ) {
1905
4659
  return res.sendError( 'no data', 204 );
1906
4660
  }
1907
4661
 
1908
- // 2. Build bulk update commands
1909
- // Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
1910
4662
 
1911
4663
  // 1. Get all IDs from hits
1912
4664
  const docIdToIndex = {};
@@ -1914,7 +4666,6 @@ export async function updateTempStatus( req, res ) {
1914
4666
  docIdToIndex[doc._id] = doc._index;
1915
4667
  } );
1916
4668
  const docIds = hits.map( ( doc ) => doc._id );
1917
- logger.info( { docIds } );
1918
4669
  // 2. Fetch all docs by ID to get 'actions' (in chunks if large)
1919
4670
  const getBody = [];
1920
4671
  for ( const doc of hits ) {
@@ -1922,71 +4673,76 @@ export async function updateTempStatus( req, res ) {
1922
4673
  }
1923
4674
 
1924
4675
  let mgetResp;
1925
- try {
1926
- mgetResp = await getOpenSearchData(
1927
- openSearch.revop,
1928
- {
1929
- query: {
1930
- ids: {
1931
- values: docIds,
1932
- },
4676
+
4677
+ mgetResp = await getOpenSearchData(
4678
+ openSearch.revop,
4679
+ {
4680
+ size: 10000,
4681
+ query: {
4682
+ ids: {
4683
+ values: docIds,
1933
4684
  },
1934
- _source: true,
1935
4685
  },
1936
- );
1937
- } catch ( err ) {
1938
- logger.error( { error: err } );
1939
- mgetResp = undefined;
1940
- }
1941
- logger.info( { mgetResp } );
4686
+ _source: true,
4687
+ },
4688
+ );
1942
4689
  // (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
1943
4690
  // 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
- }
4691
+ let fullDocs = mgetResp?.body?.hits?.hits || searchResp?.body?.hits?.hits || [];
1951
4692
 
1952
4693
  // 3. Prepare the new actions array for each doc, and set up bulk update payloads
1953
4694
  const reviewActions = [ 'approved', 'rejected' ];
1954
4695
  const docsToUpdate = [];
1955
- logger.info( { fullDocs: fullDocs } );
1956
4696
  for ( const doc of fullDocs ) {
1957
4697
  const source = doc._source || doc.fields || {}; // support mget and search hits
1958
4698
  let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
1959
4699
  if ( reviewActions.includes( status ) ) {
1960
4700
  // for review: update or push 'review'
1961
4701
  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' } );
4702
+ switch ( type ) {
4703
+ case 'review':
4704
+ actions = actions.map( ( item ) => {
4705
+ if ( item.actionType === 'review' ) {
4706
+ found = true;
4707
+ return { ...item, action: status };
4708
+ }
4709
+ return item;
4710
+ } );
4711
+ if ( !found ) {
4712
+ actions.push( { actionType: 'review', action: status } );
4713
+ }
4714
+ break;
4715
+ case 'approve':
4716
+ actions = actions.map( ( item ) => {
4717
+ if ( item.actionType === 'approve' ) {
4718
+ found = true;
4719
+ return { ...item, action: status };
4720
+ }
4721
+ return item;
4722
+ } );
4723
+ if ( !found ) {
4724
+ actions.push( { actionType: 'approve', action: status } );
4725
+ }
4726
+ break;
4727
+ default:
4728
+ return res.sendError( 'wrong vaue', 400 );
1984
4729
  }
1985
4730
  }
4731
+ let isChecked = true;
4732
+ switch ( type ) {
4733
+ case 'review':
4734
+ isChecked = status === 'approved' ? true : false;
4735
+ break;
4736
+ case 'approve':
4737
+ isChecked = status === 'approved' ? doc?._source?.isChecked : !doc?._source?.isChecked;
4738
+ break;
4739
+ }
1986
4740
  docsToUpdate.push( {
1987
4741
  _index: doc._index || docIdToIndex[doc._id],
1988
4742
  _id: doc._id,
1989
4743
  actions,
4744
+ isChecked: isChecked,
4745
+ comments: comments || '',
1990
4746
  } );
1991
4747
  }
1992
4748
  const bulkPayload = [];
@@ -1996,11 +4752,9 @@ export async function updateTempStatus( req, res ) {
1996
4752
  update: { _index: doc._index, _id: doc._id },
1997
4753
  } );
1998
4754
  bulkPayload.push( {
1999
- doc: { actions: doc.actions },
4755
+ doc: { actions: doc.actions, isChecked: doc?.isChecked, comments: doc?.comments || '' },
2000
4756
  } );
2001
4757
  }
2002
- logger.info( { bulkPayload: bulkPayload } );
2003
-
2004
4758
 
2005
4759
  // 3. Execute bulk update
2006
4760
  const bulkResp = await bulkUpdate( bulkPayload );
@@ -2008,8 +4762,50 @@ export async function updateTempStatus( req, res ) {
2008
4762
  // Count successes
2009
4763
  const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
2010
4764
 
2011
- logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
4765
+ if ( inputData?.comments && inputData?.comments !== '' ) {
4766
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
4767
+ const searchBody1 = {
4768
+ size: 10000,
4769
+ query: {
4770
+ bool: {
4771
+ must: [
4772
+ {
4773
+ terms: {
4774
+ '_id': docIds,
4775
+ },
4776
+ },
4777
+ {
4778
+ term: {
4779
+ isParent: false,
4780
+ },
4781
+ },
4782
+ ],
4783
+ },
4784
+ },
4785
+ };
4786
+
4787
+ const getSearchResp = await getOpenSearchData(
4788
+ openSearch.revop,
4789
+ searchBody1,
4790
+ );
4791
+
2012
4792
 
4793
+ const taggedImages = getSearchResp?.body?.hits?.hits?.length > 0 ? getSearchResp?.body?.hits?.hits : [];
4794
+ const logs = {
4795
+ type: inputData.type,
4796
+ storeId: taggedImages?.[0]?._source?.storeId,
4797
+ dateString: taggedImages?.[0]?._source?.dateString,
4798
+ category: taggedImages?.[0]?._source?.revopsType || '',
4799
+ taggedImages: taggedImages,
4800
+ status: inputData?.status,
4801
+ createdByEmail: req?.user?.email,
4802
+ createdByUserName: req?.user?.userName,
4803
+ createdByRole: req?.user?.role,
4804
+ message: inputData.comments || '',
4805
+ createdAt: new Date(),
4806
+ };
4807
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
4808
+ }
2013
4809
  return res.sendSuccess( { updated: updatedCount } );
2014
4810
  } catch ( error ) {
2015
4811
  const err = error.message;
@@ -2018,3 +4814,283 @@ export async function updateTempStatus( req, res ) {
2018
4814
  }
2019
4815
  }
2020
4816
 
4817
+ export async function updateUserTicketStatus( req, res ) {
4818
+ try {
4819
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
4820
+ const { storeId, dateString } = req.body || {};
4821
+
4822
+ if ( !storeId || !dateString ) {
4823
+ return res.sendError( 'storeId and dateString are required', 400 );
4824
+ }
4825
+
4826
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
4827
+
4828
+ // Fetch existing ticket so we can validate mappingInfo state
4829
+ const existingDoc = await getOpenSearchById( openSearch.footfallDirectory, docId );
4830
+ const ticketSource = existingDoc?.body?._source;
4831
+
4832
+ if ( !ticketSource ) {
4833
+ return res.sendError( 'Ticket not found', 404 );
4834
+ }
4835
+
4836
+ const mappingInfo = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [];
4837
+ if ( mappingInfo.length === 0 ) {
4838
+ return res.sendError( 'mappingInfo is missing for this ticket', 400 );
4839
+ }
4840
+
4841
+ const lastIndex = mappingInfo.length - 1;
4842
+ const lastEntry = mappingInfo[lastIndex];
4843
+
4844
+ if ( !lastEntry ) {
4845
+ return res.sendError( 'Unable to determine current ticket status', 400 );
4846
+ }
4847
+
4848
+ if ( String( lastEntry.status ).toLowerCase() !== 'open' ) {
4849
+ return res.sendError( 'Ticket is already picked by another user', 409 );
4850
+ }
4851
+
4852
+ const currentTime = new Date();
4853
+ const updatedMappingInfo = [ ...mappingInfo ];
4854
+ updatedMappingInfo[lastIndex] = {
4855
+ ...lastEntry,
4856
+ status: 'In-Progress',
4857
+ updatedAt: currentTime,
4858
+ createdAt: currentTime,
4859
+ createdByRole: req?.user?.role || '',
4860
+ createdByEmail: req?.user?.email || '',
4861
+ createdByUserName: req?.user?.userName || '',
4862
+ };
4863
+
4864
+ const updatePayload = {
4865
+ doc: {
4866
+ status: lastEntry?.type == 'review'? 'Reviewer In progress':lastEntry?.type == 'approve'? 'Approver In progress':ticketSource?.status,
4867
+ mappingInfo: updatedMappingInfo,
4868
+ updatedAt: currentTime,
4869
+ },
4870
+ };
4871
+
4872
+ const updateResult = await updateOpenSearchData(
4873
+ openSearch.footfallDirectory,
4874
+ docId,
4875
+ updatePayload,
4876
+ );
4877
+
4878
+
4879
+ if ( !updateResult || !( updateResult.statusCode === 200 || updateResult.statusCode === 201 ) ) {
4880
+ return res.sendError( 'Failed to update ticket status', 500 );
4881
+ }
4882
+ return res.sendSuccess( 'Ticket status updated successfully' );
4883
+ } catch ( error ) {
4884
+ const err = error.message;
4885
+ logger.info( { error: err, function: 'updateUserTicketStatus' } );
4886
+ return res.sendError( err, 500 );
4887
+ }
4888
+ }
4889
+
4890
+ export async function multiCloseTicket( req, res ) {
4891
+ try {
4892
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
4893
+ const inputData = req.body;
4894
+
4895
+ // inputData structure should include an array of items to close
4896
+ // Accept both array or single ticket update
4897
+ const tickets = Array.isArray( inputData.ticketList ) ? inputData.ticketList : [ inputData.ticketList ];
4898
+ // const mode = inputData.mode || '';
4899
+
4900
+ if ( !tickets.length ) {
4901
+ return res.sendError( 'No tickets provided', 400 );
4902
+ }
4903
+
4904
+ const results = [];
4905
+ for ( const ticket of tickets ) {
4906
+ const { storeId, dateString } = ticket || {};
4907
+ if ( !storeId || !dateString ) {
4908
+ results.push( { storeId, dateString, success: false, error: 'Missing storeId or dateString' } );
4909
+ continue;
4910
+ }
4911
+ // 1. Update the ticket document in footfallDirectory index
4912
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
4913
+
4914
+ // Fetch existing doc to update mappingInfo
4915
+
4916
+ const doc = await getOpenSearchById( openSearch.footfallDirectory, docId );
4917
+
4918
+
4919
+ const ticketSource = doc?.body?._source;
4920
+ if ( !ticketSource || !ticketSource.mappingInfo ) {
4921
+ results.push( { storeId, dateString, success: false, error: 'Ticket or mappingInfo missing' } );
4922
+ continue;
4923
+ }
4924
+
4925
+ let mappingInfoArray = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [ ticketSource.mappingInfo ];
4926
+ // Find all mappingInfo items matching type 'approve'
4927
+ let updated = false;
4928
+ let newMappingInfoArray = mappingInfoArray.map( ( mi, i ) => {
4929
+ if ( mi?.type === 'approve' && mi?.status === 'Open' ) {
4930
+ updated = true;
4931
+ return {
4932
+ ...mi,
4933
+ status: 'Approver-Closed',
4934
+ status: 'Closed', // following the user's instruction to set sttaus property (assumed typo, but explicitly used)
4935
+ updatedAt: new Date(),
4936
+ };
4937
+ }
4938
+ return mi;
4939
+ } );
4940
+
4941
+ if ( !updated ) {
4942
+ // None found to update
4943
+ results.push( { storeId, dateString, success: false, error: `coudn't approve this store` } );
4944
+ continue;
4945
+ }
4946
+
4947
+ // Write update to footfallDirectory
4948
+ const ticketUpdatePayload = {
4949
+ doc: {
4950
+ mappingInfo: newMappingInfoArray,
4951
+ status: 'Approver-Closed', // status updated at top level as well
4952
+ updatedAt: new Date(),
4953
+ },
4954
+ };
4955
+ let ticketUpdateResult;
4956
+ try {
4957
+ ticketUpdateResult = await updateOpenSearchData( openSearch.footfallDirectory, docId, ticketUpdatePayload );
4958
+ if ( !( ticketUpdateResult && ( ticketUpdateResult.statusCode === 200 || ticketUpdateResult.statusCode === 201 ) ) ) {
4959
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
4960
+ continue;
4961
+ }
4962
+ } catch ( err ) {
4963
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
4964
+ continue;
4965
+ }
4966
+
4967
+
4968
+ // For each ticket, update actions array for all matching image docs (storeId & dateString)
4969
+
4970
+ // Query to find all matching docs in revopTagging index with storeId and dateString
4971
+ const revopImageQuery = {
4972
+ size: 10000, // assume there won't be more than 1000 images per ticket
4973
+ query: {
4974
+ bool: {
4975
+ must: [
4976
+ { term: { 'storeId.keyword': storeId } },
4977
+ { term: { dateString: dateString } },
4978
+ ],
4979
+ },
4980
+ },
4981
+ };
4982
+
4983
+ // Fetch matching docs
4984
+ const searchRes = await getOpenSearchData( openSearch.revop, revopImageQuery );
4985
+ const revopHits = searchRes?.body?.hits?.hits || [];
4986
+
4987
+ // Optimized: batch and parallelize image-doc updates to avoid long sequential waits
4988
+ const BATCH_SIZE = 100;
4989
+ const now = new Date();
4990
+
4991
+ for ( let i = 0; i < revopHits.length; i += BATCH_SIZE ) {
4992
+ const batch = revopHits.slice( i, i + BATCH_SIZE );
4993
+
4994
+ const updatePromises = batch.map( async ( hit ) => {
4995
+ const imageDocId = hit._id;
4996
+ const imageSource = hit._source || {};
4997
+ const imageActionsArray = Array.isArray( imageSource.actions ) ? [ ...imageSource.actions ] : [];
4998
+
4999
+ imageActionsArray.push( {
5000
+ actionType: 'approve',
5001
+ action: 'approved',
5002
+ } );
5003
+
5004
+ const imageUpdatePayload = {
5005
+ doc: {
5006
+ actions: imageActionsArray,
5007
+ updatedAt: now,
5008
+ },
5009
+ };
5010
+
5011
+ return updateOpenSearchData( openSearch.revop, imageDocId, imageUpdatePayload );
5012
+ } );
5013
+
5014
+ // Wait for this batch to finish before starting the next one
5015
+ await Promise.all( updatePromises );
5016
+ }
5017
+ }
5018
+ if ( results && results?.length > 0 ) {
5019
+ return res.sendError( results, 500 );
5020
+ }
5021
+ // Return batch summary
5022
+ } catch ( error ) {
5023
+ const err = error.message;
5024
+ logger.info( { error: err, function: 'multiCloseTicket' } );
5025
+ return res.sendError( err, 500 );
5026
+ }
5027
+ }
5028
+
5029
+
5030
+ export async function checkTicketExists( req, res ) {
5031
+ try {
5032
+ let inputData = req.body;
5033
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
5034
+ let findQuery = {
5035
+ size: 10000,
5036
+ query: {
5037
+ bool: {
5038
+ must: [
5039
+ {
5040
+ term: {
5041
+ 'storeId.keyword': inputData.storeId,
5042
+ },
5043
+ },
5044
+ {
5045
+ term: {
5046
+ 'dateString': inputData.dateString,
5047
+ },
5048
+ },
5049
+ ],
5050
+ },
5051
+ },
5052
+ };
5053
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
5054
+ let Ticket = findTicket.body?.hits?.hits;
5055
+
5056
+
5057
+ res.sendSuccess( Ticket );
5058
+ } catch ( error ) {
5059
+ const err = error.message;
5060
+ logger.info( { error: err, function: 'checkTicketExists' } );
5061
+ return res.sendError( err, 500 );
5062
+ }
5063
+ }
5064
+
5065
+
5066
+ export async function getAccuracyIssues( req, res ) {
5067
+ try {
5068
+ const inputData = req.query;
5069
+
5070
+
5071
+ const getIsues = await findStoreAccuracIssues( { clientId: inputData.clientId, isActive: true }, { issueName: 1 } );
5072
+ // const mode = inputData.mode || '';
5073
+ res.sendSuccess( { result: getIsues|| [] } );
5074
+ // Return batch summary
5075
+ } catch ( error ) {
5076
+ const err = error.message;
5077
+ logger.info( { error: err, function: 'getAccuracyIssues' } );
5078
+ return res.sendError( err, 500 );
5079
+ }
5080
+ }
5081
+
5082
+ export async function updateAccuracyIssues( req, res ) {
5083
+ try {
5084
+ const inputData = req.query;
5085
+
5086
+
5087
+ const getIsues = await upsertStoreAccuracIssues( { clientId: inputData.clientId, issueName: inputData?.issueName }, { issueName: inputData?.issueName, isActive: true } );
5088
+ // const mode = inputData.mode || '';
5089
+ res.sendSuccess( { result: getIsues|| [] } );
5090
+ // Return batch summary
5091
+ } catch ( error ) {
5092
+ const err = error.message;
5093
+ logger.info( { error: err, function: 'putAccuracyIssues' } );
5094
+ return res.sendError( err, 500 );
5095
+ }
5096
+ }