tango-app-api-infra 3.9.5-vms.7 → 3.9.5-vms.71

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,14 @@
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';
11
12
  dayjs.extend( utc );
12
13
  dayjs.extend( timezone );
13
14
 
@@ -60,6 +61,750 @@ export async function createTicket( req, res ) {
60
61
  }
61
62
  }
62
63
 
64
+ export async function createinternalTicket( req, res ) {
65
+ try {
66
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
67
+ let inputData = req.body;
68
+ let record = {
69
+
70
+ storeId: inputData.storeId,
71
+ type: 'internal',
72
+ dateString: inputData.dateString,
73
+ storeName: inputData?.storeName,
74
+ ticketName: inputData.ticketName || 'footfall-directory',
75
+ footfallCount: inputData.footfallCount,
76
+ clientId: inputData?.clientId,
77
+ ticketId: 'TE_FDT_' + new Date().valueOf(),
78
+ createdAt: new Date(),
79
+ updatedAt: new Date(),
80
+ status: 'open',
81
+ comments: inputData?.comments || '',
82
+ createdByEmail: req?.user?.email,
83
+ createdByUserName: req?.user?.userName,
84
+ createdByRole: req?.user?.role,
85
+ mappingInfo: [],
86
+ };
87
+ const id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
88
+ const insertResult = await insertWithId( openSearch.footfallDirectory, id, record );
89
+ if ( insertResult && insertResult.statusCode === 201 ) {
90
+ return res.sendSuccess( 'Ticket raised successfully' );
91
+ }
92
+ } catch ( error ) {
93
+ const err = error.message || 'Internal Server Error';
94
+ logger.error( { error: error, funtion: 'createinternalTicket' } );
95
+ return res.sendError( err, 500 );
96
+ }
97
+ }
98
+ export async function tangoReviewTicket( req, res ) {
99
+ try {
100
+ const inputData = req.body;
101
+
102
+ // get store info by the storeId into mongo db
103
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
104
+
105
+ if ( !getstoreName || getstoreName == null ) {
106
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
107
+ }
108
+
109
+ // get the footfall count from opensearch
110
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
111
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
112
+ const getQuery = {
113
+ query: {
114
+ terms: {
115
+ _id: [ dateString ],
116
+ },
117
+ },
118
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
119
+ sort: [
120
+ {
121
+ date_iso: {
122
+ order: 'desc',
123
+ },
124
+ },
125
+ ],
126
+ };
127
+
128
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
129
+ const hits = getFootfallCount?.body?.hits?.hits || [];
130
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
131
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
132
+ }
133
+
134
+ // get category details from the client level configuration
135
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
136
+ if ( !getConfig || getConfig == null ) {
137
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
138
+ }
139
+ let findQuery = {
140
+ size: 10000,
141
+ query: {
142
+ bool: {
143
+ must: [
144
+ {
145
+ term: {
146
+ 'storeId.keyword': inputData.storeId,
147
+ },
148
+ },
149
+ {
150
+ term: {
151
+ 'dateString': inputData.dateString,
152
+ },
153
+ },
154
+ ],
155
+ },
156
+ },
157
+ };
158
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
159
+ let Ticket = findTicket.body?.hits?.hits;
160
+
161
+ if ( Ticket.length === 0 ) {
162
+ return res.sendError( 'Ticket not found', 400 );
163
+ }
164
+ const getTicket = {
165
+ size: 10000,
166
+ query: {
167
+ bool: {
168
+ must: [
169
+ {
170
+ term: {
171
+ 'storeId.keyword': inputData.storeId,
172
+ },
173
+ },
174
+ {
175
+ term: {
176
+ 'dateString': inputData.dateString,
177
+ },
178
+ },
179
+
180
+ ],
181
+ },
182
+ },
183
+ };
184
+ if ( Ticket[0]?._source?.type != 'internal' ) {
185
+ getTicket.query.bool.must.push(
186
+ {
187
+ nested: {
188
+ path: 'mappingInfo',
189
+ query: {
190
+ bool: {
191
+ must: [
192
+ {
193
+ term: {
194
+ 'mappingInfo.type': 'tangoreview',
195
+ },
196
+ },
197
+
198
+ ],
199
+ },
200
+ },
201
+ },
202
+ },
203
+ );
204
+ }
205
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
206
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
207
+
208
+ if ( !ticketData || ticketData?.length == 0 ) {
209
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
210
+ }
211
+
212
+ const record = {
213
+
214
+ status: parseInt( inputData?.mappingInfo?.[0]?.revicedPerc || 0 ) < parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview|| 0 ) ? 'Open - Accuracy Issue' : 'Closed',
215
+ revicedFootfall: inputData.mappingInfo?.[0]?.revicedFootfall,
216
+ revicedPerc: inputData.mappingInfo?.[0]?.revicedPerc,
217
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
218
+ createdByEmail: req?.user?.email,
219
+ createdByUserName: req?.user?.userName,
220
+ createdByRole: req?.user?.role,
221
+
222
+ };
223
+
224
+
225
+ // Retrieve client footfallDirectoryConfigs revision
226
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
227
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
228
+
229
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
230
+
231
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
232
+ let revisedPercentage = inputData.mappingInfo?.revicedPerc;
233
+ const revised = Number( revisedPercentage?.split( '%' )[0] );
234
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
235
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
236
+ if (
237
+ isAutoCloseEnable === true &&
238
+ revisedPercentage >= autoCloseAccuracyValue
239
+ ) {
240
+ record.status = 'Closed';
241
+ // Only keep or modify mappingInfo items with type "review"
242
+ if ( Array.isArray( record.mappingInfo ) ) {
243
+ const temp = record.mappingInfo
244
+ .filter( ( item ) => item.type === 'tangoreview' )
245
+ .map( ( item ) => ( {
246
+ ...item,
247
+
248
+ mode: inputData.mappingInfo?.mode,
249
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
250
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
251
+ count: inputData.mappingInfo?.count,
252
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
253
+ status: 'Closed',
254
+ createdByEmail: req?.user?.email,
255
+ createdByUserName: req?.user?.userName,
256
+ createdByRole: req?.user?.role,
257
+ } ) );
258
+
259
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
260
+ ...temp ];
261
+ // If updating the mapping config to mark [i].status as 'Closed'
262
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
263
+ if ( Array.isArray( record.mappingInfo ) ) {
264
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
265
+ return {
266
+ ...item,
267
+ status: 'Closed',
268
+ };
269
+ } );
270
+ }
271
+ // If no review mapping existed, push a new one
272
+ // if ( record.mappingInfo.length === 0 ) {
273
+ // record.mappingInfo.push( {
274
+ // type: 'tangoreview',
275
+ // mode: inputData.mappingInfo?.mode,
276
+ // revicedFootfall: inputData.mappingInfo?.revicedFootfall,
277
+ // revicedPerc: inputData.mappingInfo?.revicedPerc,
278
+ // count: inputData.mappingInfo?.count,
279
+ // revisedDetail: inputData.mappingInfo?.revisedDetail,
280
+ // status: 'Closed',
281
+ // createdByEmail: req?.user?.email,
282
+ // createdByUserName: req?.user?.userName,
283
+ // createdByRole: req?.user?.role,
284
+ // } );
285
+ // }
286
+ }
287
+ record.mappingInfo.push(
288
+ {
289
+ type: 'finalRevision',
290
+ mode: inputData.mappingInfo?.mode,
291
+ revicedFootfall: revisedFootfall,
292
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
293
+ count: inputData.mappingInfo?.count,
294
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
295
+ status: 'Closed',
296
+ createdByEmail: req?.user?.email,
297
+ createdByUserName: req?.user?.userName,
298
+ createdByRole: req?.user?.role,
299
+ createdAt: new Date(),
300
+ },
301
+ );
302
+ } else if ( revised < tangoReview ) {
303
+ // If ticket is closed, do not proceed with revision mapping
304
+
305
+ record.status = 'Closed - Accuracy Issue';
306
+ // Only keep or modify mappingInfo items with type "review"
307
+ if ( Array.isArray( record.mappingInfo ) ) {
308
+ const temp = record.mappingInfo
309
+ .filter( ( item ) => item.type === 'tangoreview' )
310
+ .map( ( item ) => ( {
311
+ ...item,
312
+ mode: inputData.mappingInfo?.mode,
313
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
314
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
315
+ count: inputData.mappingInfo?.count,
316
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
317
+ status: 'Closed',
318
+ createdByEmail: req?.user?.email,
319
+ createdByUserName: req?.user?.userName,
320
+ createdByRole: req?.user?.role,
321
+ } ) );
322
+
323
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
324
+ ...temp ];
325
+ if ( Array.isArray( record.mappingInfo ) ) {
326
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
327
+ return {
328
+ ...item,
329
+ status: 'Closed',
330
+ };
331
+ } );
332
+ }
333
+ // If no review mapping existed, push a new one
334
+ // if ( record.mappingInfo.length === 0 ) {
335
+ // record.mappingInfo.push( {
336
+ // type: 'tangoreview',
337
+ // mode: inputData.mappingInfo?.mode,
338
+ // revicedFootfall: inputData.mappingInfo?.revicedFootfall,
339
+ // revicedPerc: inputData.mappingInfo?.revicedPerc,
340
+ // count: inputData.mappingInfo?.count,
341
+ // revisedDetail: inputData.mappingInfo?.revisedDetail,
342
+ // status: 'Closed',
343
+ // createdByEmail: req?.user?.email,
344
+ // createdByUserName: req?.user?.userName,
345
+ // createdByRole: req?.user?.role,
346
+ // } );
347
+ // }
348
+ }
349
+ } else {
350
+ if ( Array.isArray( record.mappingInfo ) ) {
351
+ const temp = record.mappingInfo
352
+ .filter( ( item ) => item.type === 'tangoreview' )
353
+ .map( ( item ) => ( {
354
+ ...item,
355
+ mode: inputData.mappingInfo?.mode,
356
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
357
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
358
+ count: inputData.mappingInfo?.count,
359
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
360
+ status: 'Closed',
361
+ createdByEmail: req?.user?.email,
362
+ createdByUserName: req?.user?.userName,
363
+ createdByRole: req?.user?.role,
364
+ } ) );
365
+
366
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
367
+ ...temp ];
368
+ if ( Array.isArray( record.mappingInfo ) ) {
369
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
370
+ return {
371
+ ...item,
372
+ status: 'Closed',
373
+ };
374
+ } );
375
+ }
376
+ // If no review mapping existed, push a new one
377
+ // if ( record.mappingInfo.length === 0 ) {
378
+ // record.mappingInfo.push( {
379
+ // type: 'tangoreview',
380
+ // mode: inputData.mappingInfo?.mode,
381
+ // revicedFootfall: inputData.mappingInfo?.revicedFootfall,
382
+ // revicedPerc: inputData.mappingInfo?.revicedPerc,
383
+ // count: inputData.mappingInfo?.count,
384
+ // revisedDetail: inputData.mappingInfo?.revisedDetail,
385
+ // status: 'Closed',
386
+ // createdByEmail: req?.user?.email,
387
+ // createdByUserName: req?.user?.userName,
388
+ // createdByRole: req?.user?.role,
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
+ count: inputData.mappingInfo?.count,
399
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
400
+ status: 'Closed',
401
+ createdByEmail: req?.user?.email,
402
+ createdByUserName: req?.user?.userName,
403
+ createdByRole: req?.user?.role,
404
+ createdAt: new Date(),
405
+ },
406
+ );
407
+ }
408
+
409
+ if ( Ticket[0]?._source?.type==='store' ) {
410
+ let findTagging = Ticket[0]?._source?.mappingInfo.filter( ( data ) => data.type==='tagging' );
411
+ if ( findTagging?.length>0&&findTagging[0].createdByEmail!='' ) {
412
+ let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
413
+ let title = `Received response for the Footfall ticket raised.`;
414
+ let createdOn = dayjs( Ticket[0]?._source?.dateString ).format( 'DD MMM YYYY' );
415
+ let description = `Raised on ${createdOn}`;
416
+
417
+ let Data = {
418
+ 'title': title,
419
+ 'body': description,
420
+ 'type': 'closed',
421
+ 'date': Ticket[0]?._source?.dateString,
422
+ 'storeId': Ticket[0]?._source?.storeId,
423
+ 'clientId': Ticket[0]?._source?.clientId,
424
+ 'ticketId': Ticket[0]?._source?.ticketId,
425
+ };
426
+ if ( userData && userData.fcmToken ) {
427
+ const fcmToken = userData.fcmToken;
428
+ await sendPushNotification( title, description, fcmToken, Data );
429
+ }
430
+ }
431
+ }
432
+ // return;
433
+
434
+ let id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
435
+ if ( inputData.ticketType === 'internal' ) {
436
+ id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
437
+ }
438
+
439
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
440
+
441
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
442
+ return res.sendSuccess( 'Ticket closed successfully' );
443
+ } else {
444
+ return res.sendError( 'Internal Server Error', 500 );
445
+ }
446
+ } catch ( error ) {
447
+ const err = error.message || 'Internal Server Error';
448
+ logger.error( { error: err, funtion: 'tangoReviewTicket' } );
449
+ return res.sendError( err, 500 );
450
+ }
451
+ }
452
+
453
+ export async function tangoReviewAccuracyClosedTicket( req, res ) {
454
+ try {
455
+ const inputData = req.body;
456
+
457
+ // get store info by the storeId into mongo db
458
+ const getstoreName = await findOneStore( { storeId: inputData.storeId, status: 'active' }, { storeId: 1, storeName: 1, clientId: 1 } );
459
+
460
+ if ( !getstoreName || getstoreName == null ) {
461
+ return res.sendError( 'The store ID is either inActive or not found', 400 );
462
+ }
463
+
464
+ // get the footfall count from opensearch
465
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
466
+ const dateString = `${inputData.storeId}_${inputData.dateString}`;
467
+ const getQuery = {
468
+ query: {
469
+ terms: {
470
+ _id: [ dateString ],
471
+ },
472
+ },
473
+ _source: [ 'footfall', 'date_string', 'store_id', 'down_time', 'footfall_count' ],
474
+ sort: [
475
+ {
476
+ date_iso: {
477
+ order: 'desc',
478
+ },
479
+ },
480
+ ],
481
+ };
482
+
483
+ const getFootfallCount = await getOpenSearchData( openSearch.footfall, getQuery );
484
+ const hits = getFootfallCount?.body?.hits?.hits || [];
485
+ if ( hits?.[0]?._source?.footfall_count <= 0 ) {
486
+ return res.sendError( 'You can’t create a ticket because this store has 0 footfall data' );
487
+ }
488
+
489
+ // get category details from the client level configuration
490
+ const getConfig = await findOneClient( { clientId: getstoreName.clientId }, { footfallDirectoryConfigs: 1 } );
491
+ if ( !getConfig || getConfig == null ) {
492
+ return res.sendError( 'The Client ID is either not configured or not found', 400 );
493
+ }
494
+ let findQuery = {
495
+ size: 10000,
496
+ query: {
497
+ bool: {
498
+ must: [
499
+ {
500
+ term: {
501
+ 'storeId.keyword': inputData.storeId,
502
+ },
503
+ },
504
+ {
505
+ term: {
506
+ 'dateString': inputData.dateString,
507
+ },
508
+ },
509
+ ],
510
+ },
511
+ },
512
+ };
513
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
514
+ let Ticket = findTicket.body?.hits?.hits;
515
+
516
+ if ( Ticket.length === 0 ) {
517
+ return res.sendError( 'Ticket not found', 400 );
518
+ }
519
+ const getTicket = {
520
+ size: 10000,
521
+ query: {
522
+ bool: {
523
+ must: [
524
+ {
525
+ term: {
526
+ 'storeId.keyword': inputData.storeId,
527
+ },
528
+ },
529
+ {
530
+ term: {
531
+ 'dateString': inputData.dateString,
532
+ },
533
+ },
534
+
535
+ ],
536
+ },
537
+ },
538
+ };
539
+ if ( Ticket[0]?._source?.type != 'internal' ) {
540
+ getTicket.query.bool.must.push(
541
+ {
542
+ nested: {
543
+ path: 'mappingInfo',
544
+ query: {
545
+ bool: {
546
+ must: [
547
+ {
548
+ term: {
549
+ 'mappingInfo.type': 'tangoreview',
550
+ },
551
+ },
552
+
553
+ ],
554
+ },
555
+ },
556
+ },
557
+ },
558
+ );
559
+ }
560
+ const getFootfallticketData = await getOpenSearchData( openSearch.footfallDirectory, getTicket );
561
+ const ticketData = getFootfallticketData?.body?.hits?.hits;
562
+
563
+ if ( !ticketData || ticketData?.length == 0 ) {
564
+ return res.sendError( 'You don’t have any tagged images right now', 400 );
565
+ }
566
+
567
+ const record = {
568
+
569
+ status: parseInt( inputData?.mappingInfo?.[0]?.revicedPerc || 0 ) < parseInt( getConfig?.footfallDirectoryConfigs?.tangoReview|| 0 ) ? 'Open - Accuracy Issue' : 'Closed',
570
+ revicedFootfall: inputData.mappingInfo?.[0]?.revicedFootfall,
571
+ revicedPerc: inputData.mappingInfo?.[0]?.revicedPerc,
572
+ mappingInfo: ticketData?.[0]?._source?.mappingInfo,
573
+ createdByEmail: req?.user?.email,
574
+ createdByUserName: req?.user?.userName,
575
+ createdByRole: req?.user?.role,
576
+
577
+ };
578
+
579
+
580
+ // Retrieve client footfallDirectoryConfigs revision
581
+ let isAutoCloseEnable = getConfig.footfallDirectoryConfigs.isAutoCloseEnable;
582
+ let autoCloseAccuracy = getConfig?.footfallDirectoryConfigs?.autoCloseAccuracy;
583
+
584
+ const getNumber = autoCloseAccuracy.split( '%' )[0];
585
+
586
+ let autoCloseAccuracyValue = parseFloat( ( autoCloseAccuracy || getNumber ).replace( '%', '' ) );
587
+ let revisedPercentage = inputData.mappingInfo?.revicedPerc;
588
+ const revised = Number( revisedPercentage?.split( '%' )[0] );
589
+ const tangoReview = Number( getConfig?.footfallDirectoryConfigs?.tangoReview?.split( '%' )[0] );
590
+ // If autoclose enabled and revisedPercentage meets/exceeds threshold, close ticket and skip revision
591
+ if (
592
+ isAutoCloseEnable === true &&
593
+ revisedPercentage >= autoCloseAccuracyValue
594
+ ) {
595
+ record.status = 'Closed';
596
+ // Only keep or modify mappingInfo items with type "review"
597
+ if ( Array.isArray( record.mappingInfo ) ) {
598
+ const temp = record.mappingInfo
599
+ .filter( ( item ) => item.type === 'tangoreview' )
600
+ .map( ( item ) => ( {
601
+ ...item,
602
+
603
+ mode: inputData.mappingInfo?.mode,
604
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
605
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
606
+ count: inputData.mappingInfo?.count,
607
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
608
+ status: 'Closed',
609
+ createdByEmail: req?.user?.email,
610
+ createdByUserName: req?.user?.userName,
611
+ createdByRole: req?.user?.role,
612
+ } ) );
613
+
614
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
615
+ ...temp ];
616
+ // If updating the mapping config to mark [i].status as 'Closed'
617
+ // Make sure all relevant mappingInfo items of type 'approve' are set to status 'Closed'
618
+ if ( Array.isArray( record.mappingInfo ) ) {
619
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
620
+ return {
621
+ ...item,
622
+ status: 'Closed',
623
+ };
624
+ } );
625
+ }
626
+ // If no review mapping existed, push a new one
627
+ // if ( record.mappingInfo.length === 0 ) {
628
+ // record.mappingInfo.push( {
629
+ // type: 'tangoreview',
630
+ // mode: inputData.mappingInfo?.mode,
631
+ // revicedFootfall: inputData.mappingInfo?.revicedFootfall,
632
+ // revicedPerc: inputData.mappingInfo?.revicedPerc,
633
+ // count: inputData.mappingInfo?.count,
634
+ // revisedDetail: inputData.mappingInfo?.revisedDetail,
635
+ // status: 'Closed',
636
+ // createdByEmail: req?.user?.email,
637
+ // createdByUserName: req?.user?.userName,
638
+ // createdByRole: req?.user?.role,
639
+ // } );
640
+ // }
641
+ }
642
+ record.mappingInfo.push(
643
+ {
644
+ type: 'finalRevision',
645
+ mode: inputData.mappingInfo?.mode,
646
+ revicedFootfall: revisedFootfall,
647
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
648
+ count: inputData.mappingInfo?.count,
649
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
650
+ status: 'Closed',
651
+ createdByEmail: req?.user?.email,
652
+ createdByUserName: req?.user?.userName,
653
+ createdByRole: req?.user?.role,
654
+ createdAt: new Date(),
655
+ },
656
+ );
657
+ } else if ( revised < tangoReview ) {
658
+ // If ticket is closed, do not proceed with revision mapping
659
+
660
+ record.status = 'Closed - Accuracy Issue';
661
+ // Only keep or modify mappingInfo items with type "review"
662
+ if ( Array.isArray( record.mappingInfo ) ) {
663
+ const temp = record.mappingInfo
664
+ .filter( ( item ) => item.type === 'tangoreview' )
665
+ .map( ( item ) => ( {
666
+ ...item,
667
+ mode: inputData.mappingInfo?.mode,
668
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
669
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
670
+ count: inputData.mappingInfo?.count,
671
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
672
+ status: 'Closed',
673
+ createdByEmail: req?.user?.email,
674
+ createdByUserName: req?.user?.userName,
675
+ createdByRole: req?.user?.role,
676
+ } ) );
677
+
678
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
679
+ ...temp ];
680
+ if ( Array.isArray( record.mappingInfo ) ) {
681
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
682
+ return {
683
+ ...item,
684
+ status: 'Closed',
685
+ };
686
+ } );
687
+ }
688
+ // If no review mapping existed, push a new one
689
+ // if ( record.mappingInfo.length === 0 ) {
690
+ // record.mappingInfo.push( {
691
+ // type: 'tangoreview',
692
+ // mode: inputData.mappingInfo?.mode,
693
+ // revicedFootfall: inputData.mappingInfo?.revicedFootfall,
694
+ // revicedPerc: inputData.mappingInfo?.revicedPerc,
695
+ // count: inputData.mappingInfo?.count,
696
+ // revisedDetail: inputData.mappingInfo?.revisedDetail,
697
+ // status: 'Closed',
698
+ // createdByEmail: req?.user?.email,
699
+ // createdByUserName: req?.user?.userName,
700
+ // createdByRole: req?.user?.role,
701
+ // } );
702
+ // }
703
+ }
704
+ } else {
705
+ if ( Array.isArray( record.mappingInfo ) ) {
706
+ const temp = record.mappingInfo
707
+ .filter( ( item ) => item.type === 'tangoreview' )
708
+ .map( ( item ) => ( {
709
+ ...item,
710
+ mode: inputData.mappingInfo?.mode,
711
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
712
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
713
+ count: inputData.mappingInfo?.count,
714
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
715
+ status: 'Closed',
716
+ createdByEmail: req?.user?.email,
717
+ createdByUserName: req?.user?.userName,
718
+ createdByRole: req?.user?.role,
719
+ } ) );
720
+
721
+ record.mappingInfo = [ ...ticketData?.[0]?._source?.mappingInfo.slice( 0, -1 ),
722
+ ...temp ];
723
+ if ( Array.isArray( record.mappingInfo ) ) {
724
+ record.mappingInfo = record.mappingInfo.map( ( item ) => {
725
+ return {
726
+ ...item,
727
+ status: 'Closed',
728
+ };
729
+ } );
730
+ }
731
+ // If no review mapping existed, push a new one
732
+ // if ( record.mappingInfo.length === 0 ) {
733
+ // record.mappingInfo.push( {
734
+ // type: 'tangoreview',
735
+ // mode: inputData.mappingInfo?.mode,
736
+ // revicedFootfall: inputData.mappingInfo?.revicedFootfall,
737
+ // revicedPerc: inputData.mappingInfo?.revicedPerc,
738
+ // count: inputData.mappingInfo?.count,
739
+ // revisedDetail: inputData.mappingInfo?.revisedDetail,
740
+ // status: 'Closed',
741
+ // createdByEmail: req?.user?.email,
742
+ // createdByUserName: req?.user?.userName,
743
+ // createdByRole: req?.user?.role,
744
+ // } );
745
+ // }
746
+ }
747
+ record.mappingInfo.push(
748
+ {
749
+ type: 'finalRevision',
750
+ mode: inputData.mappingInfo?.mode,
751
+ revicedFootfall: inputData.mappingInfo?.revicedFootfall,
752
+ revicedPerc: inputData.mappingInfo?.revicedPerc,
753
+ count: inputData.mappingInfo?.count,
754
+ revisedDetail: inputData.mappingInfo?.revisedDetail,
755
+ status: 'Closed',
756
+ createdByEmail: req?.user?.email,
757
+ createdByUserName: req?.user?.userName,
758
+ createdByRole: req?.user?.role,
759
+ createdAt: new Date(),
760
+ },
761
+ );
762
+ }
763
+
764
+ if ( Ticket[0]?._source?.type==='store' ) {
765
+ let findTagging = Ticket[0]?._source?.mappingInfo.filter( ( data ) => data.type==='tagging' );
766
+ if ( findTagging?.length>0&&findTagging[0].createdByEmail!='' ) {
767
+ let userData = await findOneUser( { email: findTagging[0]?.createdByEmail } );
768
+ let title = `Received response for the Footfall ticket raised.`;
769
+ let createdOn = dayjs( Ticket[0]?._source?.dateString ).format( 'DD MMM YYYY' );
770
+ let description = `Raised on ${createdOn}`;
771
+
772
+ let Data = {
773
+ 'title': title,
774
+ 'body': description,
775
+ 'type': 'closed',
776
+ 'date': Ticket[0]?._source?.dateString,
777
+ 'storeId': Ticket[0]?._source?.storeId,
778
+ 'clientId': Ticket[0]?._source?.clientId,
779
+ 'ticketId': Ticket[0]?._source?.ticketId,
780
+ };
781
+ if ( userData && userData.fcmToken ) {
782
+ const fcmToken = userData.fcmToken;
783
+ await sendPushNotification( title, description, fcmToken, Data );
784
+ }
785
+ }
786
+ }
787
+ // return;
788
+
789
+ let id = `${inputData.storeId}_${inputData.dateString}_footfall-directory-tagging`;
790
+ if ( inputData.ticketType === 'internal' ) {
791
+ id = `${inputData.storeId}_${inputData.dateString}_internal_footfall-directory-tagging`;
792
+ }
793
+
794
+ const insertResult = await updateOpenSearchData( openSearch.footfallDirectory, id, { doc: record } );
795
+
796
+ if ( insertResult && ( insertResult.statusCode === 201 || insertResult.statusCode === 200 ) ) {
797
+ return res.sendSuccess( 'Ticket closed successfully' );
798
+ } else {
799
+ return res.sendError( 'Internal Server Error', 500 );
800
+ }
801
+ } catch ( error ) {
802
+ const err = error.message || 'Internal Server Error';
803
+ logger.error( { error: err, funtion: 'tangoReviewTicket' } );
804
+ return res.sendError( err, 500 );
805
+ }
806
+ }
807
+
63
808
  async function bulkUpdateStatusToPending( indexName, inputData ) {
64
809
  const bulkBody = [];
65
810
 
@@ -195,7 +940,6 @@ export async function ticketSummary1( req, res ) {
195
940
 
196
941
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
197
942
  const aggs = getData?.body?.aggregations;
198
- logger.info( { aggs: aggs, body: getData?.body } );
199
943
 
200
944
  const result = {
201
945
  totalTickets: aggs.totalTicketCount.value,
@@ -220,10 +964,10 @@ export async function ticketSummary( req, res ) {
220
964
  try {
221
965
  let result = '';
222
966
  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 ) ) ) );
967
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
224
968
 
225
- if ( req.user.userType =='tango' ) {
226
- result ={
969
+ if ( req.user.userType == 'tango' ) {
970
+ result = {
227
971
  totalTickets: 0,
228
972
  averageAccuracyOverAll: 0,
229
973
  openTickets: 0,
@@ -234,30 +978,30 @@ export async function ticketSummary( req, res ) {
234
978
  ticketAccuracyBelow: '0%',
235
979
  };
236
980
  } 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';
981
+ result = req.user.role === 'superadmin' ?
982
+ {
983
+ totalTickets: 0,
984
+ openTickets: 0,
985
+ inprogress: 0,
986
+ closedTickets: 0,
987
+ dueToday: 0,
988
+ Expired: 0,
989
+ underTangoReview: 0,
990
+ avgTicket: '0%',
991
+ avgAccuracy: '0%',
992
+ } :
993
+ req.user.role === 'user' ? 'NA' :
994
+ ticketsFeature ?
995
+ {
996
+ totalTickets: 0,
997
+ openTickets: 0,
998
+ inprogress: 0,
999
+ closedTickets: 0,
1000
+ dueToday: 0,
1001
+ Expired: 0,
1002
+ avgTicket: '0%',
1003
+ avgAccuracy: '0%',
1004
+ } : 'NA';
261
1005
  }
262
1006
 
263
1007
  return res.sendSuccess( { result: result } );
@@ -293,7 +1037,7 @@ export async function ticketList1( req, res ) {
293
1037
  terms: {
294
1038
  'clientId.keyword': Array.isArray( inputData.clientId ) ?
295
1039
  inputData.clientId :
296
- [ inputData.clientId ],
1040
+ [ inputData.clientId ],
297
1041
  },
298
1042
  },
299
1043
  ];
@@ -470,12 +1214,12 @@ export async function ticketList1( req, res ) {
470
1214
  'Ticket raised on': dayjs( item._source.createdAt ).format( 'DD MMM, YYYY' ),
471
1215
  'Issue Date': dayjs( item._source.dateString ).format( 'DD MMM, YYYY' ),
472
1216
  '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 )} %`,
1217
+ 'Duplicates': item?._source?.status === 'closed' ? item._source.duplicateACCount : item._source.duplicateCount,
1218
+ 'Employee/Staff': item?._source?.status === 'closed' ? item._source.employeeACCount : item._source.employeeCount,
1219
+ 'HouseKeeping': item?._source?.status === 'closed' ? item._source.houseKeepingACCount : item._source.houseKeepingCount,
1220
+ 'Junk': item?._source?.status === 'closed' ? ( item._source.junkACCount || 0 ) : ( item._source.junkCount || 0 ),
1221
+ '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 ) ),
1222
+ '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
1223
  'Status': item._source.status,
480
1224
  } );
481
1225
  }
@@ -491,671 +1235,439 @@ export async function ticketList1( req, res ) {
491
1235
 
492
1236
  export async function ticketList( req, res ) {
493
1237
  try {
494
- let result = '';
495
- const inputData= req.query;
1238
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1239
+ const inputData = req.query;
496
1240
  const userInfo = req.user;
497
- const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name =='reviewer' && ( m.isAdd==true || m.isEdit==true ) ) ) );
1241
+ const limit = inputData?.limit || 10;
1242
+ const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
1243
+ inputData.clientId = inputData?.clientId?.split( ',' ); // convert strig to array
498
1244
 
499
- if ( req.user.userType =='tango' ) {
500
- result =inputData.tangotype == 'store'?
501
1245
 
1246
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
502
1247
 
503
- [
504
- {
505
- ticketId: 'TE_FDT_1763539990306',
506
- storeId: '11-1716',
507
- storeName: 'LKST1916',
508
- ticketRaised: '2025-11-16',
509
- issueDate: '2025-11-16',
510
- dueDate: '2025-11-18',
511
- footfall: 1200,
512
- storeRevisedAccuracy: '98%',
513
- reviewerRevisedAccuracy: '97%',
514
- approverRevisedAccuracy: '98%',
515
- tangoRevisedAccuracy: '98%',
516
- status: 'Closed',
517
- },
518
- {
519
- ticketId: 'TE_FDT_1763860421803',
520
- storeId: '11-1716',
521
- storeName: 'LKST1916',
522
- ticketRaised: '2025-11-21',
523
- issueDate: '2025-11-20',
524
- dueDate: '2025-11-26',
525
- footfall: 94,
526
- storeRevisedAccuracy: '90%',
527
- reviewerRevisedAccuracy: '--',
528
- approverRevisedAccuracy: '--',
529
- tangoRevisedAccuracy: '--',
530
- status: 'Open',
1248
+ const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
1249
+
1250
+ const searchQuery = {
1251
+
1252
+ size: limit, // or use parseInt(req.query.limit) for dynamic
1253
+ from: offset, // or use parseInt(req.query.offset) for dynamic
1254
+ sort: [ { 'createdAt': { order: 'desc' } } ],
1255
+
1256
+ query: {
1257
+ bool: {
1258
+ must: [
1259
+ {
1260
+ 'range': {
1261
+ 'dateString': {
1262
+ 'gte': inputData.fromDate,
1263
+ 'lte': inputData.toDate,
1264
+ 'format': 'yyyy-MM-dd',
1265
+ },
1266
+ },
1267
+ },
1268
+ {
1269
+ terms: {
1270
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
1271
+ inputData.clientId :
1272
+ [ inputData.clientId ],
1273
+ },
1274
+
1275
+ },
1276
+ ],
531
1277
  },
532
- {
533
- ticketId: 'TE_FDT_1763711403163',
534
- storeId: '11-1716',
535
- storeName: 'LKST1916',
536
- ticketRaised: '2025-11-20',
537
- issueDate: '2025-11-19',
538
- dueDate: 'Due Today',
539
- footfall: 94,
540
- storeRevisedAccuracy: '95%',
541
- reviewerRevisedAccuracy: '--',
542
- approverRevisedAccuracy: '--',
543
- tangoRevisedAccuracy: '--',
544
- status: 'Open',
545
- },
546
- {
547
- ticketId: 'TE_FDT_1763539990309',
548
- storeId: '11-2000',
549
- storeName: 'LKST2368',
550
- ticketRaised: '2025-11-13',
551
- issueDate: '2025-11-13',
552
- dueDate: '2025-11-15',
553
- footfall: 1280,
554
- storeRevisedAccuracy: '98%',
555
- reviewerRevisedAccuracy: '98%',
556
- approverRevisedAccuracy: '97%',
557
- tangoRevisedAccuracy: '97%',
558
- status: 'Closed',
559
- },
560
- {
561
- ticketId: 'TE_FDT_1763539990310',
562
- storeId: '11-2000',
563
- storeName: 'LKST2368',
564
- ticketRaised: '2025-11-14',
565
- issueDate: '2025-11-14',
566
- dueDate: '2025-11-16',
567
- footfall: 1300,
568
- storeRevisedAccuracy: '96%',
569
- reviewerRevisedAccuracy: '95%',
570
- approverRevisedAccuracy: '96%',
571
- tangoRevisedAccuracy: '95%',
572
- status: 'Open',
573
- },
574
- {
575
- ticketId: 'TE_FDT_1763539990311',
576
- storeId: '11-2000',
577
- storeName: 'LKST2368',
578
- ticketRaised: '2025-11-15',
579
- issueDate: '2025-11-15',
580
- dueDate: '2025-11-17',
581
- footfall: 1350,
582
- storeRevisedAccuracy: '99%',
583
- reviewerRevisedAccuracy: '98%',
584
- approverRevisedAccuracy: '98%',
585
- tangoRevisedAccuracy: '99%',
586
- status: 'Closed',
587
- },
588
- {
589
- ticketId: 'TE_FDT_1763539990312',
590
- storeId: '11-2000',
591
- storeName: 'LKST2368',
592
- ticketRaised: '2025-11-16',
593
- issueDate: '2025-11-16',
594
- dueDate: '2025-11-18',
595
- footfall: 1400,
596
- storeRevisedAccuracy: '97%',
597
- reviewerRevisedAccuracy: '96%',
598
- approverRevisedAccuracy: '97%',
599
- tangoRevisedAccuracy: '96%',
600
- status: 'Closed',
601
- },
602
- {
603
- ticketId: 'TE_FDT_1763539990313',
604
- storeId: '11-10',
605
- storeName: 'LKST80',
606
- ticketRaised: '2023-11-14',
607
- issueDate: '2023-11-14',
608
- dueDate: '2023-11-16',
609
- footfall: 900,
610
- storeRevisedAccuracy: '95%',
611
- reviewerRevisedAccuracy: '94%',
612
- approverRevisedAccuracy: '95%',
613
- tangoRevisedAccuracy: '95%',
614
- status: 'Inprogress',
615
- },
616
- {
617
- ticketId: 'TE_FDT_1763539990314',
618
- storeId: '11-10',
619
- storeName: 'LKST80',
620
- ticketRaised: '2023-11-15',
621
- issueDate: '2023-11-15',
622
- dueDate: '2023-11-17',
623
- footfall: 1000,
624
- storeRevisedAccuracy: '98%',
625
- reviewerRevisedAccuracy: '97%',
626
- approverRevisedAccuracy: '97%',
627
- tangoRevisedAccuracy: '98%',
628
- status: 'Closed',
629
- },
630
- {
631
- ticketId: 'TE_FDT_1763539990315',
632
- storeId: '11-10',
633
- storeName: 'LKST80',
634
- ticketRaised: '2023-11-16',
635
- issueDate: '2023-11-16',
636
- dueDate: '2023-11-18',
637
- footfall: 1050,
638
- storeRevisedAccuracy: '96%',
639
- reviewerRevisedAccuracy: '96%',
640
- approverRevisedAccuracy: '96%',
641
- tangoRevisedAccuracy: '97%',
642
- status: 'Open',
643
- },
644
- {
645
- ticketId: 'TE_FDT_1763539990316',
646
- storeId: '11-10',
647
- storeName: 'LKST80',
648
- ticketRaised: '2023-11-17',
649
- issueDate: '2023-11-17',
650
- dueDate: '2023-11-19',
651
- footfall: 1100,
652
- storeRevisedAccuracy: '97%',
653
- reviewerRevisedAccuracy: '97%',
654
- approverRevisedAccuracy: '97%',
655
- tangoRevisedAccuracy: '97%',
656
- status: 'Closed',
657
- },
658
- {
659
- ticketId: 'TE_FDT_1763539990317',
660
- storeId: '12-1111',
661
- storeName: 'LKST3030',
662
- ticketRaised: '2025-10-12',
663
- issueDate: '2025-10-12',
664
- dueDate: '2025-10-14',
665
- footfall: 1200,
666
- storeRevisedAccuracy: '97%',
667
- reviewerRevisedAccuracy: '97%',
668
- approverRevisedAccuracy: '97%',
669
- tangoRevisedAccuracy: '96%',
670
- status: 'Open',
671
- },
672
- {
673
- ticketId: 'TE_FDT_176353999018',
674
- storeId: '12-1111',
675
- storeName: 'LKST3030',
676
- ticketRaised: '2025-10-13',
677
- issueDate: '2025-10-13',
678
- dueDate: '2025-10-15',
679
- footfall: 1095,
680
- storeRevisedAccuracy: '95%',
681
- reviewerRevisedAccuracy: '96%',
682
- approverRevisedAccuracy: '97%',
683
- tangoRevisedAccuracy: '95%',
684
- status: 'Open-Accuracy Issue',
685
- },
686
- {
687
- ticketId: 'TE_FDT_1763539990319',
688
- storeId: '12-1111',
689
- storeName: 'LKST3030',
690
- ticketRaised: '2025-10-14',
691
- issueDate: '2025-10-14',
692
- dueDate: '2025-10-16',
693
- footfall: 987,
694
- storeRevisedAccuracy: '98%',
695
- reviewerRevisedAccuracy: '99%',
696
- approverRevisedAccuracy: '99%',
697
- tangoRevisedAccuracy: '99%',
698
- status: 'closed-Accuracy Issue',
699
- },
700
- {
701
- ticketId: 'TE_FDT_1763539990320',
702
- storeId: '14-8002',
703
- storeName: 'LKST4590',
704
- ticketRaised: '2025-09-12',
705
- issueDate: '2025-09-12',
706
- dueDate: '2025-09-14',
707
- footfall: 1080,
708
- storeRevisedAccuracy: '98%',
709
- reviewerRevisedAccuracy: '97%',
710
- approverRevisedAccuracy: '99%',
711
- tangoRevisedAccuracy: '99%',
712
- status: 'Closed',
1278
+ },
1279
+ };
1280
+
1281
+ if ( inputData.sortBy ) {
1282
+ let sortOrder = inputData.sortOrder === 1 ? 'asc' : 'desc';
1283
+
1284
+ // Remove default sort so we don't duplicate/conflict
1285
+ // INSERT_YOUR_CODE
1286
+ // If sortBy is present, check if the field needs ".keyword" (for string fields like storeName, storeId, ticketId)
1287
+ // This avoids OpenSearch errors about sorting on text fields.
1288
+ const stringKeywordFields = [ 'storeName', 'storeId', 'ticketId', 'status', 'type', 'clientId' ];
1289
+ let sortField = inputData.sortBy == 'footfall' ? 'footfallCount' : inputData.sortBy == 'issueDate' ? 'dateString' : inputData.sortBy;
1290
+ if ( stringKeywordFields.includes( sortField ) ) {
1291
+ sortField = `${sortField}.keyword`;
1292
+ }
1293
+
1294
+ // Remove default sort so we don't duplicate/conflict
1295
+ searchQuery.sort = [
1296
+ { [sortField]: { order: sortOrder } },
1297
+ ];
1298
+ }
1299
+ // Example: Filtering by storeId if present in the query
1300
+ if ( inputData.storeId ) {
1301
+ inputData.storeId = inputData?.storeId?.split( ',' );
1302
+ searchQuery.query.bool.must.push( {
1303
+ terms: {
1304
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
1305
+ inputData.storeId :
1306
+ [ inputData.storeId ],
713
1307
  },
1308
+ } );
1309
+ }
1310
+ if ( inputData.status && inputData.status!=='' ) {
1311
+ inputData.status = inputData?.status?.split( ',' );
1312
+ if ( req.user.userType === 'tango' ) {
1313
+ searchQuery.query.bool.must.push( {
1314
+ terms: {
1315
+ 'status': Array.isArray( inputData?.status ) ?
1316
+ inputData?.status :
1317
+ [ inputData?.status ],
1318
+ },
1319
+ } );
1320
+ } else if ( inputData?.permissionType === 'approve' ) {
1321
+ searchQuery.query.bool.must.push( {
1322
+ nested: {
1323
+ path: 'mappingInfo',
1324
+ query: {
1325
+ bool: {
1326
+ must: [
1327
+ {
1328
+ term: {
1329
+ 'mappingInfo.type': 'approve',
1330
+ },
1331
+ },
1332
+ {
1333
+ term: {
1334
+ 'mappingInfo.status': inputData.status[0],
1335
+ },
1336
+ },
1337
+ ],
1338
+ },
1339
+ },
1340
+ },
1341
+ } );
1342
+ } else if ( inputData?.permissionType === 'review' ) {
1343
+ searchQuery.query.bool.must.push( {
1344
+ nested: {
1345
+ path: 'mappingInfo',
1346
+ query: {
1347
+ bool: {
1348
+ must: [
1349
+ {
1350
+ term: {
1351
+ 'mappingInfo.type': 'review',
1352
+ },
1353
+ },
1354
+ {
1355
+ term: {
1356
+ 'mappingInfo.status': inputData.status[0],
1357
+ },
1358
+ },
1359
+ ],
1360
+ },
1361
+ },
1362
+ },
1363
+ } );
1364
+ } else if ( ticketsFeature ) {
1365
+ searchQuery.query.bool.must.push( {
1366
+ nested: {
1367
+ path: 'mappingInfo',
1368
+ query: {
1369
+ bool: {
1370
+ must: [
1371
+ {
1372
+ term: {
1373
+ 'mappingInfo.type': 'review',
1374
+ },
1375
+ },
1376
+ {
1377
+ term: {
1378
+ 'mappingInfo.status': inputData.status[0],
1379
+ },
1380
+ },
1381
+ ],
1382
+ },
1383
+ },
1384
+ },
1385
+ } );
1386
+ } else if ( ticketsApproveFeature ) {
1387
+ searchQuery.query.bool.must.push( {
1388
+ nested: {
1389
+ path: 'mappingInfo',
1390
+ query: {
1391
+ bool: {
1392
+ must: [
1393
+ {
1394
+ term: {
1395
+ 'mappingInfo.type': 'approve',
1396
+ },
1397
+ },
1398
+ {
1399
+ term: {
1400
+ 'mappingInfo.status': inputData.status[0],
1401
+ },
1402
+ },
1403
+ ],
1404
+ },
1405
+ },
1406
+ },
1407
+ } );
1408
+ }
1409
+ }
714
1410
 
715
- ] :
716
- [
1411
+ if ( req?.user?.userType === 'tango' && inputData.tangoType !== 'internal' ) {
1412
+ searchQuery.query.bool.must.push(
1413
+ {
1414
+ term: {
1415
+ 'type.keyword': 'store',
1416
+ },
1417
+ },
1418
+ {
1419
+ nested: {
1420
+ path: 'mappingInfo',
1421
+ query: {
1422
+ bool: {
1423
+ must: [
1424
+ {
1425
+ term: {
1426
+ 'mappingInfo.type': 'tangoreview',
1427
+ },
1428
+ },
1429
+
1430
+ ],
1431
+ },
1432
+ },
1433
+ },
1434
+
1435
+ },
1436
+
1437
+
1438
+ );
1439
+ } else if ( req?.user?.userType === 'client' && inputData.tangoType !== 'internal' ) {
1440
+ searchQuery.query.bool.must.push(
1441
+ {
1442
+ term: {
1443
+ 'type.keyword': 'store',
1444
+ },
1445
+ },
1446
+ {
1447
+ nested: {
1448
+ path: 'mappingInfo',
1449
+ query: {
1450
+ bool: {
1451
+ must: [
1452
+ {
1453
+ term: {
1454
+ 'mappingInfo.type': ticketsFeature ? 'review' : ticketsApproveFeature ?
1455
+ 'approve' : 'tagging',
1456
+ },
1457
+ },
1458
+
1459
+ ],
1460
+ },
1461
+ },
1462
+ },
1463
+ },
1464
+
1465
+ );
1466
+ }
1467
+
1468
+ if ( inputData?.permissionType ) {
1469
+ searchQuery.query.bool.must.push(
1470
+ {
1471
+ nested: {
1472
+ path: 'mappingInfo',
1473
+ query: {
1474
+ bool: {
1475
+ must: [
1476
+ {
1477
+ term: {
1478
+ 'mappingInfo.type': inputData?.permissionType == 'review' ? 'review' :
1479
+ 'approve',
1480
+ },
1481
+ },
1482
+
1483
+ ],
1484
+ },
1485
+ },
1486
+ },
1487
+ },
1488
+ );
1489
+ }
1490
+
1491
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
1492
+ searchQuery.query.bool['should'] = [];
1493
+ searchQuery.query.bool.should = [
717
1494
 
718
1495
  {
719
- ticketId: 'TE_FDT_1763860421803',
720
- storeId: '11-1716',
721
- storeName: 'LKST1916',
722
- ticketRaised: '2025-11-21',
723
- issueDate: '2025-11-20',
724
- footfall: 94,
725
- type: 'store',
726
- storeRevisedAccuracy: '95%',
727
- reviewerRevisedAccuracy: '--',
728
- approverRevisedAccuracy: '--',
729
- tangoRevisedAccuracy: '--',
730
- status: 'Open',
731
- tangoStatus: 'Open',
732
- },
733
- {
734
- ticketId: 'TE_FDT_1763539990307',
735
- storeId: '11-1716',
736
- storeName: 'LKST1916',
737
- ticketRaised: '2025-11-13',
738
- issueDate: '2025-11-13',
739
- footfall: 1150,
740
- type: 'store',
741
- storeRevisedAccuracy: '99%',
742
- reviewerRevisedAccuracy: '99%',
743
- approverRevisedAccuracy: '98%',
744
- tangoRevisedAccuracy: '97%',
745
- status: 'Closed',
746
- tangoStatus: 'open',
747
- },
748
- {
749
- ticketId: 'TE_FDT_1763539990308',
750
- storeId: '11-1716',
751
- storeName: 'LKST1916',
752
- ticketRaised: '2025-11-14',
753
- issueDate: '2025-11-14',
754
- footfall: 1100,
755
- type: 'store',
756
- storeRevisedAccuracy: '97%',
757
- reviewerRevisedAccuracy: '96%',
758
- approverRevisedAccuracy: '97%',
759
- status: 'Closed',
760
- tangoStatus: 'In-Progress',
761
- },
762
- {
763
- ticketId: 'TE_FDT_1763539990309',
764
- storeId: '11-2000',
765
- storeName: 'LKST2368',
766
- ticketRaised: '2025-11-13',
767
- issueDate: '2025-11-13',
768
- footfall: 1280,
769
- type: 'internal',
770
- storeRevisedAccuracy: '98%',
771
- reviewerRevisedAccuracy: '98%',
772
- approverRevisedAccuracy: '97%',
773
- tangoRevisedAccuracy: '97%',
774
- status: 'Closed-Accuracy Issue',
775
- tangoStatus: 'Open',
776
- },
777
- {
778
- ticketId: 'TE_FDT_1763539990310',
779
- storeId: '11-2000',
780
- storeName: 'LKST2368',
781
- ticketRaised: '2025-11-14',
782
- issueDate: '2025-11-14',
783
- footfall: 300,
784
- type: 'store',
785
- storeRevisedAccuracy: '96%',
786
- reviewerRevisedAccuracy: '95%',
787
- approverRevisedAccuracy: '96%',
788
- tangoRevisedAccuracy: '95%',
789
- status: 'Closed',
790
- tangoStatus: 'In-Progress',
791
- },
792
- {
793
- ticketId: 'TE_FDT_1763539990311',
794
- storeId: '11-2000',
795
- storeName: 'LKST2368',
796
- ticketRaised: '2025-11-15',
797
- issueDate: '2025-11-15',
798
- footfall: 350,
799
- type: 'internal',
800
- storeRevisedAccuracy: '99%',
801
- reviewerRevisedAccuracy: '98%',
802
- approverRevisedAccuracy: '98%',
803
- tangoRevisedAccuracy: '99%',
804
- status: 'Closed',
805
- tangoStatus: 'In-Progress',
806
- },
807
- {
808
- ticketId: 'TE_FDT_1763539990312',
809
- storeId: '11-2000',
810
- storeName: 'LKST2368',
811
- ticketRaised: '2025-11-16',
812
- issueDate: '2025-11-16',
813
- footfall: 400,
814
- type: 'internal',
815
- storeRevisedAccuracy: '97%',
816
- reviewerRevisedAccuracy: '96%',
817
- approverRevisedAccuracy: '97%',
818
- tangoRevisedAccuracy: '96%',
819
- status: 'Open-Accuracy Issue',
820
- tangoStatus: 'Open',
821
- },
822
- {
823
- ticketId: 'TE_FDT_1763539990313',
824
- storeId: '11-10',
825
- storeName: 'LKST80',
826
- ticketRaised: '2023-11-14',
827
- issueDate: '2023-11-14',
828
- footfall: 900,
829
- type: 'internal',
830
- storeRevisedAccuracy: '95%',
831
- reviewerRevisedAccuracy: '94%',
832
- approverRevisedAccuracy: '95%',
833
- tangoRevisedAccuracy: '95%',
834
- status: 'Closed',
835
- tangoStatus: 'Closed',
1496
+ 'wildcard': {
1497
+ 'storeName.keyword': {
1498
+ 'value': `*${inputData.searchValue}*`,
1499
+ },
1500
+ },
836
1501
  },
837
1502
  {
838
- ticketId: 'TE_FDT_1763539990314',
839
- storeId: '11-10',
840
- storeName: 'LKST80',
841
- ticketRaised: '2023-11-15',
842
- issueDate: '2023-11-15',
843
- type: 'store',
844
- footfall: 100,
845
- storeRevisedAccuracy: '98%',
846
- reviewerRevisedAccuracy: '97%',
847
- approverRevisedAccuracy: '97%',
848
- tangoRevisedAccuracy: '98%',
849
- status: 'closed-Accuracy Issue',
850
- tangoStatus: 'Closed',
1503
+ 'wildcard': {
1504
+ 'storeId.keyword': {
1505
+ 'value': `*${inputData.searchValue}*`,
1506
+ },
1507
+ },
851
1508
  },
852
1509
  {
853
- ticketId: 'TE_FDT_1763539990320',
854
- storeId: '14-8002',
855
- storeName: 'LKST4590',
856
- ticketRaised: '2025-09-12',
857
- issueDate: '2025-09-12',
858
- type: 'internal',
859
- footfall: 80,
860
- storeRevisedAccuracy: '98%',
861
- reviewerRevisedAccuracy: '97%',
862
- approverRevisedAccuracy: '99%',
863
- tangoRevisedAccuracy: '99%',
864
- status: 'Closed-Accuracy Issue',
865
- tangoStatus: 'Closed',
1510
+ 'wildcard': {
1511
+ 'ticketId.keyword': {
1512
+ 'value': `*${inputData.searchValue}*`,
1513
+ },
1514
+ },
866
1515
  },
1516
+
1517
+
867
1518
  ];
1519
+ searchQuery.query.bool['minimum_should_match'] = 1;
1520
+ }
1521
+ // You can add more filters as needed
1522
+ const searchResult = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
1523
+ const count = searchResult?.body?.hits?.total?.value || 0;
1524
+
1525
+ if ( count === 0 ) {
1526
+ return res.sendError( 'no data found', 204 );
1527
+ }
1528
+ const ticketListData = searchResult?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1529
+
1530
+ let temp = [];
1531
+ if ( req.user.userType === 'tango' ) {
1532
+ if ( inputData.tangoType === 'store' ) {
1533
+ for ( let item of ticketListData ) {
1534
+ temp.push( {
1535
+
1536
+ ticketId: item?.ticketId,
1537
+ storeId: item?.storeId,
1538
+ storeName: item?.storeName,
1539
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1540
+ issueDate: item?.dateString,
1541
+ dueDate: '',
1542
+ footfall: item?.footfallCount,
1543
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1544
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1545
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1546
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1547
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
1548
+
1549
+ } );
1550
+ }
1551
+ } else {
1552
+ for ( let item of ticketListData ) {
1553
+ temp.push( {
1554
+
1555
+ ticketId: item?.ticketId,
1556
+ storeId: item?.storeId,
1557
+ storeName: item?.storeName,
1558
+
1559
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1560
+ issueDate: item?.dateString,
1561
+ footfall: item?.footfallCount,
1562
+ dueDate: '',
1563
+ type: item?.type || 'store',
1564
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1565
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1566
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1567
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1568
+ status: item?.status,
1569
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.status || '--',
1570
+
1571
+ } );
1572
+ }
1573
+ }
868
1574
  } else {
869
- result = req.user.role === 'superadmin'?
870
- [
871
- {
872
- ticketId: 'TE_FDT_1763539990320',
873
- storeId: '11-1716',
874
- storeName: 'LKST1916',
875
- ticketRaised: '2025-11-13',
876
- issueDate: '2025-11-12',
877
- dueDate: '2025-11-14',
878
- footfall: 60,
879
- storeRevisedAccuracy: '45%',
880
- reviewerRevisedAccuracy: '67%',
881
- approverRevisedAccuracy: '87%',
882
- tangoRevisedAccuracy: '93%',
883
- status: 'Open',
884
- approvedBy: '',
885
- },
886
- {
887
- ticketId: 'TE_FDT_1763539990321',
888
- storeId: '11-1716',
889
- storeName: 'LKST1916',
890
- ticketRaised: '2025-11-11',
891
- issueDate: '2025-11-10',
892
- dueDate: '2025-11-12',
893
- footfall: 69,
894
- storeRevisedAccuracy: '79%',
895
- reviewerRevisedAccuracy: '80%',
896
- approverRevisedAccuracy: '90%',
897
- tangoRevisedAccuracy: '90%',
898
- status: 'In-Progress',
899
- approvedBy: 'mu_mu@yopmail.com',
900
- },
901
- {
902
- ticketId: 'TE_FDT_1763860421803',
903
- storeId: '11-1716',
904
- storeName: 'LKST1916',
905
- ticketRaised: '2025-11-21',
906
- issueDate: '2025-11-20',
907
- dueDate: '2025-11-26',
908
- footfall: 94,
909
- storeRevisedAccuracy: '90%',
910
- reviewerRevisedAccuracy: '90%',
911
- approverRevisedAccuracy: '90%',
912
- tangoRevisedAccuracy: '90%',
913
- status: 'Closed',
914
- approvedBy: 'mu_mu@yopmail.com',
915
- },
916
- {
917
- ticketId: 'TE_FDT_1763711403163',
918
- storeId: '11-1716',
919
- storeName: 'LKST1916',
920
- ticketRaised: '2025-11-20',
921
- issueDate: '2025-11-19',
922
- dueDate: 'Due Today',
923
- footfall: 94,
924
- storeRevisedAccuracy: '95%',
925
- reviewerRevisedAccuracy: '--',
926
- approverRevisedAccuracy: '--',
927
- tangoRevisedAccuracy: '--',
928
- status: 'In-Progress',
929
- approvedBy: 'mu_mu@yopmail.com',
930
- },
931
- {
932
- ticketId: 'TE_FDT_1763539990320',
933
- storeId: '11-1716',
934
- storeName: 'LKST1916',
935
- ticketRaised: '2025-11-15',
936
- issueDate: '2025-11-14',
937
- dueDate: '2025-11-17',
938
- footfall: 110,
939
- storeRevisedAccuracy: '90%',
940
- reviewerRevisedAccuracy: '--',
941
- approverRevisedAccuracy: '--',
942
- tangoRevisedAccuracy: '--',
943
- status: 'Open',
944
- approvedBy: '',
945
- },
946
- {
947
- ticketId: 'TE_FDT_1763539990323',
948
- storeId: '11-10',
949
- storeName: 'LKST80',
950
- ticketRaised: '2025-11-13',
951
- issueDate: '2025-11-12',
952
- dueDate: '2025-11-14',
953
- footfall: 170,
954
- storeRevisedAccuracy: '90%',
955
- reviewerRevisedAccuracy: '90%',
956
- approverRevisedAccuracy: '--',
957
- tangoRevisedAccuracy: '--',
958
- status: 'Open',
959
- approvedBy: '',
960
- },
961
- {
962
- ticketId: 'TE_FDT_1763539990328',
963
- storeId: '11-10',
964
- storeName: 'LKST80',
965
- ticketRaised: '2025-11-12',
966
- issueDate: '2025-11-11',
967
- dueDate: '2025-11-14',
968
- footfall: 170,
969
- storeRevisedAccuracy: '90%',
970
- reviewerRevisedAccuracy: '90%',
971
- approverRevisedAccuracy: '90%',
972
- tangoRevisedAccuracy: '--',
973
- status: 'Expired',
974
- approvedBy: 'mu_mu@yopmail.com',
975
- },
976
- {
977
- ticketId: 'TE_FDT_1763539990330',
978
- storeId: '11-10',
979
- storeName: 'LKST80',
980
- ticketRaised: '2025-11-18',
981
- issueDate: '2025-11-15',
982
- dueDate: 'Due Today',
983
- footfall: 230,
984
- storeRevisedAccuracy: '90%',
985
- reviewerRevisedAccuracy: '90%',
986
- approverRevisedAccuracy: '90%',
987
- tangoRevisedAccuracy: '90%',
988
- status: 'Closed',
989
- approvedBy: 'mu_mu@yopmail.com',
990
- },
991
- {
992
- ticketId: 'TE_FDT_1763539990332',
993
- storeId: '11-10',
994
- storeName: 'LKST80',
995
- ticketRaised: '2025-11-17',
996
- issueDate: '2025-11-16',
997
- dueDate: '2025-11-20',
998
- footfall: 812,
999
- storeRevisedAccuracy: '80%',
1000
- reviewerRevisedAccuracy: '80%',
1001
- approverRevisedAccuracy: '80%',
1002
- tangoRevisedAccuracy: '--',
1003
- status: 'Open',
1004
- approvedBy: 'mu_mu@yopmail.com',
1005
- },
1006
- {
1007
- ticketId: 'TE_FDT_176353999034',
1008
- storeId: '11-2000',
1009
- storeName: 'LKST2368',
1010
- ticketRaised: '2025-11-15',
1011
- issueDate: '2025-11-14',
1012
- dueDate: '2025-11-19',
1013
- footfall: '',
1014
- storeRevisedAccuracy: '--',
1015
- reviewerRevisedAccuracy: '--',
1016
- approverRevisedAccuracy: '--',
1017
- tangoRevisedAccuracy: '--',
1018
- status: 'Open',
1019
- approvedBy: '',
1020
- },
1021
- ] :
1022
- req.user.role === 'user'? 'NA':
1023
- ticketsFeature?
1024
- [
1025
- {
1026
- ticketId: 'TE_FDT_1763860421803',
1027
- storeId: '11-1716',
1028
- storeName: 'LKST1916',
1029
- ticketRaised: '2025-11-21',
1030
- issueDate: '2025-11-20',
1031
- dueDate: 'Due Today',
1032
- footfall: 90,
1033
- storeRevisedAccuracy: '90%',
1034
- reviewerRevisedAccuracy: '0%',
1035
- status: 'Open',
1036
- ReviewedBy: '',
1037
- },
1038
- {
1039
- ticketId: 'TE_FDT_1763539990346',
1040
- storeId: '11-2000',
1041
- storeName: 'LKST2368',
1042
- ticketRaised: '2025-11-21',
1043
- issueDate: '2025-11-20',
1044
- dueDate: '2025-11-26',
1045
- footfall: 90,
1046
- storeRevisedAccuracy: '90%',
1047
- reviewerRevisedAccuracy: '--',
1048
- status: 'In-Progress',
1049
- ReviewedBy: 'mu_mu@yopmail.com',
1050
- },
1051
- {
1052
- ticketId: 'TE_FDT_176353999048',
1053
- storeId: '11-2000',
1054
- storeName: 'LKST2368',
1055
- ticketRaised: '2025-11-16',
1056
- issueDate: '2025-11-15',
1057
- dueDate: '2025-11-19',
1058
- footfall: 100,
1059
- storeRevisedAccuracy: '90%',
1060
- reviewerRevisedAccuracy: '90%',
1061
- status: 'Closed',
1062
- ReviewedBy: 'ayyanar@yopmail.com',
1063
- },
1064
- {
1065
- ticketId: 'TE_FDT_176353999048',
1066
- storeId: '11-10',
1067
- storeName: 'LKST80',
1068
- ticketRaised: '2025-11-08',
1069
- issueDate: '2025-11-06',
1070
- dueDate: '2025-11-09',
1071
- footfall: 120,
1072
- storeRevisedAccuracy: '90%',
1073
- reviewerRevisedAccuracy: '0%',
1074
- status: 'Expired',
1075
- ReviewedBy: '',
1076
- },
1077
- {
1078
- ticketId: 'TE_FDT_1763539990341',
1079
- storeId: '11-2000',
1080
- storeName: 'LKST2368',
1081
- ticketRaised: '2025-11-6',
1082
- issueDate: '2025-11-15',
1083
- dueDate: 'Due Today',
1084
- footfall: 510,
1085
- storeRevisedAccuracy: '90%',
1086
- reviewerRevisedAccuracy: '--',
1087
- status: 'Open',
1088
- ReviewedBy: '',
1089
- },
1090
- {
1091
- ticketId: 'TE_FDT_1763539990340',
1092
- storeId: '11-10',
1093
- storeName: 'LKST80',
1094
- ticketRaised: '2025-11-14',
1095
- issueDate: '2025-11-12',
1096
- dueDate: '2025-11-15',
1097
- footfall: 100,
1098
- storeRevisedAccuracy: '0%',
1099
- reviewerRevisedAccuracy: '0%',
1100
- status: 'Expired',
1101
- ReviewedBy: '',
1102
- },
1103
- {
1104
- ticketId: 'TE_FDT_1763539990339',
1105
- storeId: '11-2000',
1106
- storeName: 'LKST2368',
1107
- ticketRaised: '2025-11-16',
1108
- issueDate: '2025-11-15',
1109
- dueDate: '2025-11-17',
1110
- footfall: 140,
1111
- storeRevisedAccuracy: '90%',
1112
- reviewerRevisedAccuracy: '90%',
1113
- status: 'Closed',
1114
- ReviewedBy: 'sornanithya@yopmail.com',
1115
- },
1116
- {
1117
- ticketId: 'TE_FDT_1763539990337',
1118
- storeId: '11-10',
1119
- storeName: 'LKST80',
1120
- ticketRaised: '2025-11-16',
1121
- issueDate: '2025-11-15',
1122
- dueDate: '2025-11-18',
1123
- footfall: '',
1124
- storeRevisedAccuracy: '90%',
1125
- reviewerRevisedAccuracy: '--',
1126
- status: 'Expired',
1127
- ReviewedBy: '',
1128
- },
1129
- {
1130
- ticketId: 'TE_FDT_1763539990338',
1131
- storeId: '11-2000',
1132
- storeName: 'LKST2368',
1133
- ticketRaised: '2025-11-17',
1134
- issueDate: '2025-11-16',
1135
- dueDate: 'Due today',
1136
- footfall: 110,
1137
- storeRevisedAccuracy: '90%',
1138
- reviewerRevisedAccuracy: '--',
1139
- status: 'In-Progress',
1140
- ReviewedBy: 'vinoth@yopmail.com',
1141
- },
1142
- {
1143
- ticketId: 'TE_FDT_1763539990335',
1144
- storeId: '11-2000',
1145
- storeName: 'LKST2368',
1146
- ticketRaised: '2025-11-17',
1147
- issueDate: '2025-11-16',
1148
- dueDate: 'Due today',
1149
- footfall: 100,
1150
- storeRevisedAccuracy: '90%',
1151
- reviewerRevisedAccuracy: '0%',
1152
- status: 'In-Progress',
1153
- ReviewedBy: 'sornanithya@yopmail.com',
1154
- },
1155
- ]: 'NA';
1575
+ if ( inputData?.permissionType === 'approve' ) {
1576
+ for ( let item of ticketListData ) {
1577
+ temp.push( {
1578
+
1579
+ ticketId: item?.ticketId,
1580
+ storeId: item?.storeId,
1581
+ storeName: item?.storeName,
1582
+
1583
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1584
+ issueDate: item?.dateString,
1585
+ dueDate: '',
1586
+ footfall: item?.footfallCount,
1587
+
1588
+ type: item.type || 'store',
1589
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1590
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1591
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1592
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1593
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
1594
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1595
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
1596
+
1597
+ } );
1598
+ }
1599
+ } else if ( inputData?.permissionType === 'review' ) {
1600
+ for ( let item of ticketListData ) {
1601
+ temp.push( {
1602
+
1603
+ ticketId: item?.ticketId,
1604
+ storeId: item?.storeId,
1605
+ storeName: item?.storeName,
1606
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1607
+ issueDate: item?.dateString,
1608
+ footfall: item?.footfallCount,
1609
+ dueDate: '',
1610
+ type: item.type || 'store',
1611
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1612
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1613
+
1614
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
1615
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
1616
+
1617
+ } );
1618
+ }
1619
+ } else if ( req.user.role === 'user' ) {
1620
+ temp = [];
1621
+ } else if ( ticketsFeature ) {
1622
+ for ( let item of ticketListData ) {
1623
+ temp.push( {
1624
+
1625
+ ticketId: item?.ticketId,
1626
+ storeId: item?.storeId,
1627
+ storeName: item?.storeName,
1628
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1629
+ issueDate: item?.dateString,
1630
+ footfall: item?.footfallCount,
1631
+ dueDate: '',
1632
+ type: item.type || 'store',
1633
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1634
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1635
+
1636
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
1637
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
1638
+
1639
+ } );
1640
+ }
1641
+ } else if ( ticketsApproveFeature ) {
1642
+ for ( let item of ticketListData ) {
1643
+ temp.push( {
1644
+
1645
+ ticketId: item?.ticketId,
1646
+ storeId: item?.storeId,
1647
+ storeName: item?.storeName,
1648
+
1649
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1650
+ issueDate: item?.dateString,
1651
+ dueDate: '',
1652
+ footfall: item?.footfallCount,
1653
+
1654
+ type: item.type || 'store',
1655
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1656
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1657
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1658
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1659
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
1660
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1661
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
1662
+
1663
+ } );
1664
+ }
1665
+ } else {
1666
+ temp = [];
1667
+ }
1156
1668
  }
1157
1669
 
1158
- return res.sendSuccess( { result: result } );
1670
+ return res.sendSuccess( { result: temp, count: count } );
1159
1671
  } catch ( error ) {
1160
1672
  const err = error.message || 'Internal Server Error';
1161
1673
  logger.error( { error: error, messgage: req.query } );
@@ -1167,149 +1679,19 @@ export async function getTickets( req, res ) {
1167
1679
  try {
1168
1680
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1169
1681
  const inputData = req.query;
1170
- const limit = inputData.limit;
1171
- const skip = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
1172
- inputData.storeId = inputData.storeId.split( ',' ); // convert strig to array
1173
- logger.info( { inputData: inputData, limit: limit, skip: skip } );
1174
- 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' ];
1682
+
1683
+
1684
+ 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' ];
1175
1685
  let filter = [
1176
1686
 
1177
1687
  {
1178
- range: {
1179
- dateString: {
1180
- gte: inputData.fromDate,
1181
- lte: inputData.toDate,
1182
- format: 'yyyy-MM-dd',
1183
- },
1688
+ term: {
1689
+ 'ticketId.keyword': inputData.ticketId,
1184
1690
  },
1185
1691
  },
1186
1692
  ];
1187
- if ( inputData?.storeId ) {
1188
- filter.push(
1189
- {
1190
- terms: {
1191
- 'storeId.keyword': Array.isArray( inputData.storeId ) ?
1192
- inputData.storeId :
1193
- inputData.storeId,
1194
- },
1195
- },
1196
- );
1197
- }
1198
- if ( inputData?.dateString ) {
1199
- filter.push(
1200
- {
1201
- term: {
1202
- 'dateString': inputData.dateString,
1203
- },
1204
- },
1205
- );
1206
- }
1207
- if ( inputData.status ) {
1208
- filter.push(
1209
- {
1210
- term: {
1211
- 'status.keyword': inputData.status,
1212
- },
1213
- },
1214
- );
1215
- }
1216
- if (
1217
- inputData.revopsType
1218
- ) {
1219
- inputData.revopsType === 'employee' ?
1220
- filter.push( {
1221
- range: {
1222
- employeeCount: {
1223
- gt: 0,
1224
- },
1225
- },
1226
- } ) :
1227
- inputData.revopsType === 'houseKeeping' ?
1228
- filter.push( {
1229
- range: {
1230
- houseKeepingCount: {
1231
- gt: 0,
1232
- },
1233
- },
1234
- } ) :
1235
- inputData.revopsType === 'junk' ?
1236
- filter.push( {
1237
- range: {
1238
- junkCount: {
1239
- gt: 0,
1240
- },
1241
- },
1242
- } ) :
1243
- filter.push( {
1244
- range: {
1245
- duplicateCount: {
1246
- gt: 0,
1247
- },
1248
- },
1249
- } );
1250
- 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' ] :
1251
- 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' ] :
1252
- 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' ] :
1253
- 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' ] : [];
1254
- }
1255
-
1256
- if ( inputData.action ) {
1257
- filter.push( {
1258
- bool: {
1259
- should: [
1260
- {
1261
- constant_score: {
1262
- filter: { term: { 'houseKeepingStatus.keyword': inputData.action } },
1263
- boost: 1,
1264
- _name: 'matched_housekeeping',
1265
- },
1266
- },
1267
- {
1268
- constant_score: {
1269
- filter: { term: { 'employeeStatus.keyword': inputData.action } },
1270
- boost: 1,
1271
- _name: 'matched_employee',
1272
- },
1273
- },
1274
- {
1275
- constant_score: {
1276
- filter: { term: { 'duplicateStatus.keyword': inputData.action } },
1277
- boost: 1,
1278
- _name: 'matched_duplicate',
1279
- },
1280
- },
1281
- {
1282
- constant_score: {
1283
- filter: { term: { 'junkStatus.keyword': inputData.action } },
1284
- boost: 1,
1285
- _name: 'matched_junk',
1286
- },
1287
- },
1288
- ],
1289
- minimum_should_match: 1,
1290
- },
1291
- } );
1292
- }
1293
1693
 
1294
1694
 
1295
- let getRevCount = {};
1296
- if ( inputData.revopsType ) {
1297
- getRevCount = {
1298
- size: 0,
1299
- query: {
1300
- bool: {
1301
- filter: filter,
1302
- },
1303
- },
1304
- aggs: {
1305
- totalCount: {
1306
- sum: {
1307
- field: inputData.revopsType == 'employee' ? 'employeeCount' : inputData.revopsType == 'houseKeeping' ? 'houseKeepingCount' :inputData.revopsType == 'junk' ? 'junkCount': 'duplicateCount',
1308
- },
1309
- },
1310
- },
1311
- };
1312
- }
1313
1695
  const getCount = {
1314
1696
  query: {
1315
1697
  bool: {
@@ -1320,20 +1702,14 @@ export async function getTickets( req, res ) {
1320
1702
 
1321
1703
 
1322
1704
  const geteDataCount = await getOpenSearchCount( openSearch.footfallDirectory, getCount );
1323
- const geteRevDataCount = inputData?.revopsType ? await getOpenSearchData( openSearch.footfallDirectory, getRevCount ) : null;
1324
- const revCount = inputData?.revopsType ? geteRevDataCount?.body?.aggregations?.totalCount?.value : 0;
1325
- const count = geteDataCount?.body?.count;
1326
- if ( !geteDataCount || count == 0 ) {
1327
- if ( inputData.storeId?.length > 0 ) {
1328
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
1329
- return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
1330
- }
1331
- return res.sendError( 'No data found', 204 );
1705
+
1706
+
1707
+ if ( !geteDataCount ) {
1708
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
1332
1709
  }
1333
1710
 
1334
1711
  const getQuery = {
1335
- size: limit,
1336
- from: skip,
1712
+ size: 1,
1337
1713
  query: {
1338
1714
  bool: {
1339
1715
  filter: filter,
@@ -1343,14 +1719,9 @@ export async function getTickets( req, res ) {
1343
1719
  };
1344
1720
 
1345
1721
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
1346
-
1347
1722
  const response = getData?.body?.hits?.hits;
1348
1723
  if ( !response || response.length == 0 ) {
1349
- if ( inputData.storeId?.length > 0 ) {
1350
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
1351
- return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
1352
- }
1353
- return res.sendError( 'No data', 204 );
1724
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
1354
1725
  }
1355
1726
  let temp = [];
1356
1727
  if ( inputData?.action ) {
@@ -1388,56 +1759,307 @@ export async function getTickets( req, res ) {
1388
1759
  approverUserName: hit?._source?.approverUserName,
1389
1760
  approverEmail: hit?._source?.approverEmail,
1390
1761
  approverRole: hit?._source?.approverRole,
1762
+ type: hit?._source?.type,
1391
1763
  };
1392
1764
  let result;
1393
1765
 
1766
+
1394
1767
  const matched = hit.matched_queries;
1395
1768
  // Add only matched data array
1396
- if ( matched.includes( 'matched_employee' )&& ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
1769
+ if ( matched.includes( 'matched_employee' ) && ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
1397
1770
  result = defaultData;
1398
1771
  result.employee = hit?._source?.employee;
1399
1772
  // result.type = 'employee';
1400
1773
  result.matched = matched;
1401
1774
  }
1402
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
1775
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
1403
1776
  result = defaultData;
1404
1777
  result.houseKeeping = hit?._source?.houseKeeping;
1405
1778
  result.matched = matched;
1406
1779
  }
1407
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
1780
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
1408
1781
  result = defaultData;
1409
1782
  result.duplicateImages = hit?._source?.duplicateImages;
1410
1783
  result.matched = matched;
1411
1784
  }
1412
1785
 
1413
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
1414
- result = defaultData;
1415
- result.junk = hit?._source?.junk;
1416
- result.matched = matched;
1417
- }
1786
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
1787
+ result = defaultData;
1788
+ result.junk = hit?._source?.junk;
1789
+ result.matched = matched;
1790
+ }
1791
+
1792
+ if ( result ) {
1793
+ const nested = [
1794
+ {
1795
+ _id: hit._id,
1796
+ _index: hit._index,
1797
+ _score: 0,
1798
+ _source: result,
1799
+ },
1800
+ ];
1801
+ temp.push( ...nested );
1802
+ }
1803
+ } );
1804
+ }
1805
+
1806
+ const finalResponse = inputData.action ? temp : response;
1807
+ const getRevopQuery = {
1808
+ size: 10000,
1809
+ query: {
1810
+ bool: {
1811
+ must: [
1812
+ { term: { 'storeId.keyword': response?.[0]?._source?.storeId } }, // assuming inputData.storeId is an array
1813
+ { term: { 'dateString': response?.[0]?._source?.dateString } },
1814
+ ],
1815
+ },
1816
+ },
1817
+ };
1818
+
1819
+
1820
+ const revopResp = await getOpenSearchData( openSearch.revop, getRevopQuery );
1821
+
1822
+
1823
+ // Map revopResp.body.hits.hits to revopSources
1824
+ const revopSources = Array.isArray( revopResp?.body?.hits?.hits ) ?
1825
+ revopResp?.body?.hits?.hits?.map( ( hit ) => hit._source ) :
1826
+ [];
1827
+
1828
+ // Create a map of revopSources by id for quick lookup
1829
+ const revopSourcesMap = new Map();
1830
+ revopSources.forEach( ( item ) => {
1831
+ if ( item?.id ) {
1832
+ revopSourcesMap.set( String( item.id ), item );
1833
+ }
1834
+ } );
1835
+
1836
+ // Process revopSources to replace duplicateImage entries with full objects from the array
1837
+ const processedRevopSources = revopSources.map( ( item ) => {
1838
+ // Check if this is a duplicate parent item
1839
+ if ( item?.revopsType === 'duplicate' && item?.isParent === true && Array.isArray( item?.duplicateImage ) ) {
1840
+ // Map each duplicateImage entry to the full object from revopSources
1841
+ const updatedDuplicateImage = item.duplicateImage.map( ( duplicateImg ) => {
1842
+ const duplicateId = String( duplicateImg?.id );
1843
+ // Find the full object in revopSources that matches this id
1844
+ const fullObject = revopSourcesMap.get( duplicateId );
1845
+ // Return the full object if found, otherwise return the original duplicateImg
1846
+ return fullObject || duplicateImg;
1847
+ } );
1848
+
1849
+ return {
1850
+ ...item,
1851
+ duplicateImage: updatedDuplicateImage,
1852
+ };
1853
+ }
1854
+ // Return item as-is if it doesn't meet the criteria
1855
+ return item;
1856
+ } );
1418
1857
 
1419
- if ( result ) {
1420
- const nested = [
1421
- {
1422
- _id: hit._id,
1423
- _index: hit._index,
1424
- _score: 0,
1425
- _source: result,
1426
- },
1427
- ];
1428
- temp.push( ...nested );
1429
- }
1430
- } );
1431
- }
1432
- const finalResponse = inputData.action ? temp : response;
1433
1858
  if ( finalResponse?.length == 0 || !finalResponse ) {
1434
1859
  if ( inputData.storeId?.length > 0 ) {
1435
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
1860
+ const getStoreName = await findOneStore( { storeId: response?.[0]?._source?.storeId }, { storeName: 1 } );
1436
1861
  return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
1437
1862
  }
1438
1863
  return res.sendError( 'No Data found', 204 );
1439
1864
  }
1440
- return res.sendSuccess( { result: finalResponse, count: count, revopCount: revCount } );
1865
+
1866
+
1867
+ // replace the mappingInfo.revisedDetail with processedRevopSources
1868
+ if ( Array.isArray( finalResponse ) ) {
1869
+ for ( let item of finalResponse ) {
1870
+ if (
1871
+ item &&
1872
+ item._source &&
1873
+ item._source.mappingInfo
1874
+
1875
+ ) {
1876
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
1877
+ const vmsCommentsLogIndex = openSearch.vmsCommentsLog || 'vms-comments-log-dev';
1878
+ let commentsResponse = [];
1879
+ const commentsFilter = [
1880
+ { term: { 'storeId.keyword': item?._source?.storeId } },
1881
+ { term: { dateString: item?._source?.dateString } },
1882
+
1883
+ ];
1884
+
1885
+ const commentsQuery = {
1886
+ size: 10000,
1887
+ sort: [
1888
+ { createdAt: { order: 'desc' } }, // Sort descending by createdAt
1889
+ ],
1890
+ query: {
1891
+ bool: {
1892
+ filter: commentsFilter,
1893
+ },
1894
+ },
1895
+ };
1896
+
1897
+ const commentsRes = await getOpenSearchData( vmsCommentsLogIndex, commentsQuery );
1898
+ // If mappingInfo is an array, update revisedDetail for each mappingInfo object
1899
+ if ( Array.isArray( item._source.mappingInfo ) ) {
1900
+ item._source.mappingInfo.forEach( ( mappingObj ) => {
1901
+ commentsResponse = commentsRes?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1902
+
1903
+ // Check if duplicate condition exists in commentsResponse
1904
+
1905
+ // Structure comments output
1906
+ let commentsDetails = [];
1907
+
1908
+
1909
+ const types = [ 'tagging', 'review', 'approve' ];
1910
+
1911
+ // Process each type
1912
+ types.forEach( ( typeValue ) => {
1913
+ if ( typeValue === 'tagging' ) {
1914
+ // For tagging, group by category and create separate objects for each category
1915
+ const taggingComments = commentsResponse.filter( ( c ) => c.type === typeValue );
1916
+
1917
+ // Group by category
1918
+ const categoryGroups = {};
1919
+ taggingComments.forEach( ( c ) => {
1920
+ const category = c.category || 'other';
1921
+ if ( !categoryGroups[category] ) {
1922
+ categoryGroups[category] = [];
1923
+ }
1924
+ categoryGroups[category].push( c );
1925
+ } );
1926
+
1927
+ // Create separate objects for each category
1928
+ Object.keys( categoryGroups ).forEach( ( category ) => {
1929
+ const categoryComments = categoryGroups[category];
1930
+ let parent = null;
1931
+
1932
+ const comms = categoryComments.map( ( c ) => {
1933
+ if ( category === 'duplicate' ) {
1934
+ if ( !parent && c.parent ) {
1935
+ parent = c.parent;
1936
+ }
1937
+ return {
1938
+ createdByEmail: c.createdByEmail,
1939
+ createdByUserName: c.createdByUserName,
1940
+ createdByRole: c.createdByRole,
1941
+ message: c.message,
1942
+ };
1943
+ } else {
1944
+ return {
1945
+ id: c.id,
1946
+ tempId: c.tempId,
1947
+ timeRange: c.timeRange,
1948
+ entryTime: c.entryTime,
1949
+ exitTime: c.exitTime,
1950
+ filePath: c.filePath,
1951
+ isChecked: c.isChecked,
1952
+ createdAt: c.createdAt,
1953
+ message: c.message,
1954
+ createdByEmail: c.createdByEmail,
1955
+ createdByUserName: c.createdByUserName,
1956
+ createdByRole: c.createdByRole,
1957
+ };
1958
+ }
1959
+ } );
1960
+
1961
+ const taggingObj = {
1962
+ category: category,
1963
+ type: typeValue,
1964
+ comments: comms,
1965
+ };
1966
+
1967
+ // Add parent only for duplicate category
1968
+ if ( category === 'duplicate' && parent !== null ) {
1969
+ taggingObj.parent = parent;
1970
+ }
1971
+
1972
+ commentsDetails.push( taggingObj );
1973
+ } );
1974
+ } else if ( typeValue === 'review' || typeValue === 'approve' ) {
1975
+ // For review and approve, keep existing structure
1976
+ const comms = commentsResponse
1977
+ .filter( ( c ) => c.type === typeValue )
1978
+ .map( ( c ) => {
1979
+ if ( c.category === 'duplicate' ) {
1980
+ return {
1981
+ parent: c?.taggedImages[0]?._source?.parent,
1982
+ category: c.category,
1983
+ taggedImages: Array.isArray( c.taggedImages ) ?
1984
+ c.taggedImages.map( ( img ) => ( {
1985
+ id: img?._source?.id,
1986
+ tempId: img?._source?.tempId,
1987
+ timeRange: img?._source?.timeRange,
1988
+ entryTime: img?._source?.entryTime,
1989
+ exitTime: img?._source?.exitTime,
1990
+ filePath: img?._source?.filePath,
1991
+ isChecked: img?._source?.isChecked,
1992
+ } ) ) :
1993
+ [],
1994
+ createdByEmail: c.createdByEmail,
1995
+ createdByUserName: c.createdByUserName,
1996
+ createdByRole: c.createdByRole,
1997
+ status: c.status,
1998
+ message: c.message,
1999
+ };
2000
+ } else {
2001
+ return {
2002
+ category: c.category,
2003
+ taggedImages: Array.isArray( c.taggedImages ) ?
2004
+ c.taggedImages.map( ( img ) => ( {
2005
+ id: img?._source?.id,
2006
+ tempId: img?._source?.tempId,
2007
+ timeRange: img?._source?.timeRange,
2008
+ entryTime: img?._source?.entryTime,
2009
+ exitTime: img?._source?.exitTime,
2010
+ filePath: img?._source?.filePath,
2011
+ isChecked: img?._source?.isChecked,
2012
+ } ) ) :
2013
+ [],
2014
+ createdByEmail: c.createdByEmail,
2015
+ createdByUserName: c.createdByUserName,
2016
+ createdByRole: c.createdByRole,
2017
+ status: c.status,
2018
+ message: c.message,
2019
+ };
2020
+ }
2021
+ } );
2022
+
2023
+ // Only add if there are comments
2024
+ if ( comms.length > 0 ) {
2025
+ commentsDetails.push( {
2026
+ type: typeValue,
2027
+ comments: comms,
2028
+ } );
2029
+ } else {
2030
+ // Add empty comments array if no comments
2031
+ commentsDetails.push( {
2032
+ type: typeValue,
2033
+ comments: [],
2034
+ } );
2035
+ }
2036
+ }
2037
+ } );
2038
+
2039
+
2040
+ item._source.commentsDetails = commentsDetails;
2041
+
2042
+ if (
2043
+ Object.prototype.hasOwnProperty.call( mappingObj, 'revisedDetail' ) &&
2044
+ mappingObj.type !== 'tangoreview'
2045
+ ) {
2046
+ mappingObj.revisedDetail = processedRevopSources;
2047
+ }
2048
+ } );
2049
+ } else {
2050
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
2051
+ }
2052
+ }
2053
+ }
2054
+ } else if (
2055
+ finalResponse &&
2056
+ finalResponse._source &&
2057
+ finalResponse._source.mappingInfo
2058
+ ) {
2059
+ finalResponse._source.mappingInfo.revisedDetail = processedRevopSources;
2060
+ }
2061
+
2062
+ return res.sendSuccess( { result: finalResponse } );
1441
2063
  } catch ( error ) {
1442
2064
  const err = error.message || 'Internal Server Error';
1443
2065
  logger.error( { error: error, messgage: req.query } );
@@ -1509,7 +2131,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1509
2131
  item.isChecked == true ? tempId.push( { tempId: item.tempId, timeRange: item.timeRange } ) : null;
1510
2132
  bulkBody.push(
1511
2133
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${item.timeRange}_${item.tempId}` } },
1512
- { doc: { isChecked: item.isChecked, status: item?.isChecked == true? 'approved':'rejected' } },
2134
+ { doc: { isChecked: item.isChecked, status: item?.isChecked == true ? 'approved' : 'rejected' } },
1513
2135
  );
1514
2136
  } );
1515
2137
  }
@@ -1524,11 +2146,11 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1524
2146
  updateData.employee = updatedEmployee;
1525
2147
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: updateData } );
1526
2148
  for ( let employee of updateData?.employee ) {
1527
- ( employee.isChecked == true ) ? tempId.push( { tempId: employee.tempId, timeRange: employee.timeRange } ) : null;
1528
- bulkBody.push(
1529
- { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
1530
- { doc: { isChecked: employee.isChecked, status: employee?.isChecked == true? 'approved':'rejected' } },
1531
- );
2149
+ ( employee.isChecked == true ) ? tempId.push( { tempId: employee.tempId, timeRange: employee.timeRange } ) : null;
2150
+ bulkBody.push(
2151
+ { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
2152
+ { doc: { isChecked: employee.isChecked, status: employee?.isChecked == true ? 'approved' : 'rejected' } },
2153
+ );
1532
2154
  }
1533
2155
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { employeeACCount: tempId?.length || 0 } } );
1534
2156
  }
@@ -1547,7 +2169,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1547
2169
  houseKeeping.isChecked == true ? tempId.push( { tempId: houseKeeping.tempId, timeRange: houseKeeping.timeRange } ) : null;
1548
2170
  bulkBody.push(
1549
2171
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${houseKeeping.timeRange}_${houseKeeping.tempId}` } },
1550
- { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true? 'approved':'rejected' } },
2172
+ { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true ? 'approved' : 'rejected' } },
1551
2173
  );
1552
2174
  }
1553
2175
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { houseKeepingACCount: tempId?.length || 0 } } );
@@ -1568,7 +2190,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1568
2190
  junk.isChecked == true ? tempId.push( { tempId: junk.tempId, timeRange: junk.timeRange } ) : null;
1569
2191
  bulkBody.push(
1570
2192
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${junk.timeRange}_${junk.tempId}` } },
1571
- { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true? 'approved':'rejected' } },
2193
+ { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true ? 'approved' : 'rejected' } },
1572
2194
  );
1573
2195
  }
1574
2196
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { junkACCount: tempId?.length || 0 } } );
@@ -1593,7 +2215,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1593
2215
  let getUpdateExistingOne = await getOpenSearchById( openSearch.footfallDirectory, _id );
1594
2216
  const tempIdList = await extractCheckedTempIds( getUpdateExistingOne?.body );
1595
2217
  const isSendMessge = await sendSqsMessage( data, tempIdList, getStoreType, storeId );
1596
- if ( isSendMessge ==true ) {
2218
+ if ( isSendMessge == true ) {
1597
2219
  return true; // res.sendSuccess( 'Ticket has been updated successfully' );
1598
2220
  } else {
1599
2221
  return false; // res.sendError( 'No SQS message sent', 500 );
@@ -1656,7 +2278,7 @@ function updateEmployeeCheckFlags( existingEmployees, inputEmployees, status ) {
1656
2278
  // Step 2: Loop through all existing and update isChecked accordingly
1657
2279
  const updatedEmployees = existingEmployees.map( ( emp ) => ( {
1658
2280
  ...emp,
1659
- isChecked: status === 'rejected'? !checkedTempIds.has( emp.tempId ):checkedTempIds.has( emp.tempId ),
2281
+ isChecked: status === 'rejected' ? !checkedTempIds.has( emp.tempId ) : checkedTempIds.has( emp.tempId ),
1660
2282
  } ) );
1661
2283
 
1662
2284
  return updatedEmployees;
@@ -1706,7 +2328,7 @@ function mergeDuplicateImagesWithUncheck( existingData, inputData, status ) {
1706
2328
  export async function sendSqsMessage( inputData, tempId, getStoreType, storeId ) {
1707
2329
  const sqs = JSON.parse( process.env.SQS );
1708
2330
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1709
- const sqsName = getStoreType > 0? sqs.revopTrackTicket: sqs.revopTicket;
2331
+ const sqsName = getStoreType > 0 ? sqs.revopTrackTicket : sqs.revopTicket;
1710
2332
  const sqsProduceQueue = getStoreType > 0 ? {
1711
2333
  QueueUrl: `${sqs.url}${sqsName}`,
1712
2334
  MessageBody: JSON.stringify( {
@@ -1734,12 +2356,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1734
2356
 
1735
2357
  } ),
1736
2358
  };
1737
- const sqsQueue = getStoreType > 0? await sendMessageToFIFOQueue( sqsProduceQueue ):
1738
- await sendMessageToQueue(
1739
- sqsProduceQueue.QueueUrl,
1740
- sqsProduceQueue.MessageBody,
1741
-
1742
- );
2359
+ const sqsQueue = getStoreType > 0 ? await sendMessageToFIFOQueue( sqsProduceQueue ) :
2360
+ await sendMessageToQueue(
2361
+ sqsProduceQueue.QueueUrl,
2362
+ sqsProduceQueue.MessageBody,
2363
+ );
1743
2364
  if ( sqsQueue.statusCode ) {
1744
2365
  logger.error( {
1745
2366
  error: `${sqsQueue}`,
@@ -1747,7 +2368,7 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1747
2368
  } );
1748
2369
  return false;
1749
2370
  } else {
1750
- const id =`${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0? 'live':'reduction'}_${Date.now()}`;
2371
+ const id = `${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0 ? 'live' : 'reduction'}_${Date.now()}`;
1751
2372
  const logs = {
1752
2373
  QueueUrl: sqsProduceQueue.QueueUrl,
1753
2374
  MessageBody: sqsProduceQueue.MessageBody,
@@ -1797,13 +2418,7 @@ export async function getTaggedStores( req, res ) {
1797
2418
  },
1798
2419
  },
1799
2420
  ];
1800
- // if ( req?.user?.userType == 'client' && req?.user?.role !== 'superadmin' && req?.stores?.length > 0 ) {
1801
- // filter.push(
1802
- // {
1803
- // terms: { 'storeId.keyword': req.stores },
1804
- // },
1805
- // );
1806
- // }
2421
+
1807
2422
  filter.push(
1808
2423
  {
1809
2424
  terms: { 'storeId.keyword': req?.stores || [] },
@@ -1918,8 +2533,8 @@ export async function downloadTickets( req, res ) {
1918
2533
  {
1919
2534
  terms: {
1920
2535
  'storeId.keyword': Array.isArray( inputData.storeId ) ?
1921
- inputData.storeId :
1922
- inputData.storeId,
2536
+ inputData.storeId :
2537
+ inputData.storeId,
1923
2538
  },
1924
2539
  },
1925
2540
  );
@@ -1967,26 +2582,26 @@ export async function downloadTickets( req, res ) {
1967
2582
  },
1968
2583
 
1969
2584
  } ) :
1970
- inputData.revopsType === 'junk' ?
1971
- filter.push( {
1972
- range: {
1973
- junkCount: {
1974
- gt: 0,
2585
+ inputData.revopsType === 'junk' ?
2586
+ filter.push( {
2587
+ range: {
2588
+ junkCount: {
2589
+ gt: 0,
2590
+ },
1975
2591
  },
1976
- },
1977
- } ):
2592
+ } ) :
1978
2593
 
1979
- filter.push( {
1980
- range: {
1981
- duplicateCount: {
1982
- gt: 0,
2594
+ filter.push( {
2595
+ range: {
2596
+ duplicateCount: {
2597
+ gt: 0,
2598
+ },
1983
2599
  },
1984
- },
1985
- } );
2600
+ } );
1986
2601
  source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1987
2602
  inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1988
2603
  inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1989
- inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'junkCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] : [];
2604
+ inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'junkCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] : [];
1990
2605
  }
1991
2606
 
1992
2607
  if ( inputData.action ) {
@@ -2063,7 +2678,7 @@ export async function downloadTickets( req, res ) {
2063
2678
  let temp = [];
2064
2679
  if ( inputData?.action ) {
2065
2680
  response.map( ( hit ) => {
2066
- const defaultData ={
2681
+ const defaultData = {
2067
2682
  storeId: hit._source.storeId,
2068
2683
  dateString: hit?._source?.dateString,
2069
2684
  ticketName: hit?._source?.ticketName,
@@ -2093,19 +2708,19 @@ export async function downloadTickets( req, res ) {
2093
2708
  // result.type = 'employee';
2094
2709
  result.matched = matched;
2095
2710
  }
2096
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
2711
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
2097
2712
  logger.info( { revop: inputData.revopsType } );
2098
2713
  result = defaultData;
2099
2714
  result.houseKeeping = hit?._source?.houseKeeping;
2100
2715
  result.matched = matched;
2101
2716
  }
2102
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
2717
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
2103
2718
  logger.info( { revop: inputData.revopsType } );
2104
2719
  result = defaultData;
2105
2720
  result.duplicateImages = hit?._source?.duplicateImages;
2106
2721
  result.matched = matched;
2107
2722
  }
2108
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
2723
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
2109
2724
  result = defaultData;
2110
2725
  result.junk = hit?._source?.junk;
2111
2726
  result.matched = matched;
@@ -2149,7 +2764,7 @@ export async function downloadTickets( req, res ) {
2149
2764
  revopsType: inputData?.revopsType,
2150
2765
  type: 'get-tickets',
2151
2766
  };
2152
- const record={
2767
+ const record = {
2153
2768
  stores: inputData?.storeId,
2154
2769
  fromDate: inputData?.fromDate,
2155
2770
  toDate: inputData?.toDate,
@@ -2167,7 +2782,7 @@ export async function downloadTickets( req, res ) {
2167
2782
  const sqs = JSON.parse( process.env.SQS );
2168
2783
  const sqsName = sqs.revopDownload;
2169
2784
 
2170
- const sqsProduceQueue ={
2785
+ const sqsProduceQueue = {
2171
2786
  QueueUrl: `${sqs.url}${sqsName}`,
2172
2787
  MessageBody: JSON.stringify( {
2173
2788
  _id: getId?._id,
@@ -2196,7 +2811,6 @@ export async function downloadTickets( req, res ) {
2196
2811
  }
2197
2812
 
2198
2813
  async function extractTempIds( document ) {
2199
- logger.info( { document: document } );
2200
2814
  const source = document?._source || {};
2201
2815
  const result = [];
2202
2816
 
@@ -2246,7 +2860,7 @@ export async function reviewerList( req, res ) {
2246
2860
  featureName: 'FootfallDirectory',
2247
2861
  modules: {
2248
2862
  $elemMatch: {
2249
- name: 'Reviewer',
2863
+ name: inputData?.type === 'review' ? 'reviewer' : 'approver',
2250
2864
  $or: [ { isAdd: true }, { isEdit: true } ],
2251
2865
  },
2252
2866
  },
@@ -2255,7 +2869,7 @@ export async function reviewerList( req, res ) {
2255
2869
  };
2256
2870
 
2257
2871
  const getUserlist = await findUser( reviewerRoleQuery, { userName: 1, email: 1, role: 1 } );
2258
- return res.sendSuccess( getUserlist|| [] );
2872
+ return res.sendSuccess( getUserlist || [] );
2259
2873
  } catch ( error ) {
2260
2874
  const err = error.message || 'Internal Server Error';
2261
2875
  return res.sendError( err, 500 );
@@ -2277,6 +2891,16 @@ export async function openTicketList( req, res ) {
2277
2891
  clientId: Array.isArray( clientId ) ? clientId : [ clientId ],
2278
2892
  },
2279
2893
  },
2894
+ {
2895
+ term: {
2896
+ 'mappingInfo.type': inputData.type,
2897
+ },
2898
+ },
2899
+ {
2900
+ term: {
2901
+ 'mappingInfo.status.keyword': 'Open',
2902
+ },
2903
+ },
2280
2904
  {
2281
2905
  range: {
2282
2906
  dateString: {
@@ -2295,14 +2919,19 @@ export async function openTicketList( req, res ) {
2295
2919
  filter: filter,
2296
2920
  },
2297
2921
  },
2298
- _source: [ 'ticketId', 'storeName', 'revicedFootfall', 'footfallCount', 'revicedPerc' ],
2922
+ _source: [ 'ticketId', 'storeName', 'storeId', 'dateString', 'revicedFootfall', 'footfallCount', 'revicedPerc', 'type' ],
2299
2923
  };
2924
+ // INSERT_YOUR_CODE
2925
+ // Add sorting by revicedPerc descending (highest revised accuracy first)
2926
+ openSearchQuery.sort = [
2927
+ { 'revicedPerc.keyword': { order: inputData?.sortOrder === 1 ? 'asc' : 'desc' } },
2928
+ ];
2300
2929
 
2301
2930
 
2302
2931
  // Assuming getOpenSearchData and openSearch.footfallDirectoryTagging are available
2303
2932
  const result = await getOpenSearchData( openSearch.footfallDirectory, openSearchQuery );
2304
2933
  const getUserlist = result?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
2305
- return res.sendSuccess( getUserlist|| [] );
2934
+ return res.sendSuccess( getUserlist || [] );
2306
2935
  } catch ( error ) {
2307
2936
  const err = error.message || 'Internal Server Error';
2308
2937
  logger.error( { error: error, function: 'openTicketList' } );
@@ -2315,73 +2944,32 @@ export async function assignTicket( req, res ) {
2315
2944
  const inputData = req.body;
2316
2945
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2317
2946
 
2318
- // INSERT_YOUR_CODE
2319
- // Build the query to match storeId(s) and dateString range [fromDate, toDate], format: 'yyyy-mm-dd'
2320
- const { email, userName, role, actionType } = inputData;
2321
-
2322
- // INSERT_YOUR_CODE
2323
-
2324
- // Find and update mappingInfo fields for the provided ticketId and actionType
2325
- // Requires ticketId in inputData
2326
- const { ticketId } = inputData;
2327
- if ( !ticketId ) {
2328
- return res.sendError( 'ticketId is required', 400 );
2329
- }
2330
-
2331
- // Build the OpenSearch update-by-query body
2332
- const updateBody = {
2333
- script: {
2334
- source: `
2335
- if (ctx._source.mappingInfo != null) {
2336
- for (int i = 0; i < ctx._source.mappingInfo.length; i++) {
2337
- if (ctx._source.mappingInfo[i].type == params.actionType) {
2338
- ctx._source.mappingInfo[i].createdByEmail = params.email;
2339
- ctx._source.mappingInfo[i].createdByUserName = params.userName;
2340
- ctx._source.mappingInfo[i].createdByRole = params.role;
2341
- }
2342
- }
2343
- }
2344
- `,
2345
- lang: 'painless',
2346
- params: {
2347
- email,
2348
- userName,
2349
- role,
2350
- actionType,
2351
- },
2352
- },
2353
- query: {
2354
- bool: {
2355
- must: [
2356
- { term: { 'ticketId.keyword': ticketId } },
2357
- {
2358
- nested: {
2359
- path: 'mappingInfo',
2360
- query: {
2361
- bool: {
2362
- must: [
2363
- { match: { 'mappingInfo.type': actionType } },
2364
- ],
2365
- },
2366
- },
2367
- },
2368
- },
2369
- ],
2370
- },
2371
- },
2372
- };
2373
-
2374
- // Call OpenSearch _update_by_query to update doc(s) where ticketId and mappingInfo[i].type == actionType
2375
- const response = await upsertOpenSearchData(
2376
- openSearch.footfallDirectory,
2377
- '11-1716_2025-11-20_footfall-directory-tagging',
2378
- updateBody, // custom arg to indicate passthrough for update-by-query, depends on helper implementation
2379
- );
2380
-
2947
+ const { email, userName, role, storeId, dateString } = inputData;
2948
+ const _id = `${storeId}_${dateString}_footfall-directory-tagging`;
2381
2949
 
2382
- logger.info( { response } );
2950
+ const getTicket = await getOpenSearchById( openSearch.footfallDirectory, _id );
2951
+ if ( !getTicket ) {
2952
+ return res.sendError( 'Ticket is not found', 400 );
2953
+ }
2954
+ const source = getTicket?.body?._source;
2955
+ const mappingInfo = Array.isArray( source.mappingInfo ) ? source.mappingInfo : [];
2956
+ if ( mappingInfo.length === 0 ) {
2957
+ return res.sendError( 'Ticket is not found', 400 );
2958
+ }
2959
+ const lastIndex = mappingInfo.length - 1;
2960
+ const lastEntry = mappingInfo[lastIndex];
2961
+ if ( String( lastEntry.status ) !== 'In-Progress' ) {
2962
+ return res.sendError( 'Ticket is not in progress', 400 );
2963
+ }
2383
2964
 
2384
- return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
2965
+ const currentTime = new Date();
2966
+ const updatedMappingInfo = [ ...mappingInfo ];
2967
+ updatedMappingInfo[lastIndex] = { ...lastEntry, createdByEmail: email, updatedAt: currentTime, createdByUserName: userName, createdByRole: role };
2968
+ const updateResult = await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { mappingInfo: updatedMappingInfo } } );
2969
+ if ( !updateResult ) {
2970
+ return res.sendError( 'Failed to update ticket', 400 );
2971
+ }
2972
+ return res.sendSuccess( { message: 'Ticket assigned successfully' } );
2385
2973
  } catch ( error ) {
2386
2974
  const err = error.message || 'Internal Server Error';
2387
2975
  logger.error( { error: error, function: 'assignTicket' } );
@@ -2392,11 +2980,13 @@ export async function assignTicket( req, res ) {
2392
2980
  export async function updateTempStatus( req, res ) {
2393
2981
  try {
2394
2982
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2395
- const { id, status } = req.body;
2983
+ const inputData = req.body;
2984
+ const { id, type, status, comments } = inputData;
2396
2985
 
2397
2986
  // Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
2398
2987
  // 1. Search for all documents matching the ticket IDs
2399
2988
  const searchBody = {
2989
+ size: 10000,
2400
2990
  query: {
2401
2991
  bool: {
2402
2992
  must: [
@@ -2415,16 +3005,14 @@ export async function updateTempStatus( req, res ) {
2415
3005
  openSearch.revop,
2416
3006
  searchBody,
2417
3007
  );
2418
- logger.info( { searchResp: searchResp } );
3008
+
2419
3009
  // Extract bulk IDs to update
2420
3010
  const hits = searchResp?.body?.hits?.hits ?? [];
2421
- logger.info( { hits: hits } );
3011
+
2422
3012
  if ( !hits.length ) {
2423
3013
  return res.sendError( 'no data', 204 );
2424
3014
  }
2425
3015
 
2426
- // 2. Build bulk update commands
2427
- // Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
2428
3016
 
2429
3017
  // 1. Get all IDs from hits
2430
3018
  const docIdToIndex = {};
@@ -2432,7 +3020,6 @@ export async function updateTempStatus( req, res ) {
2432
3020
  docIdToIndex[doc._id] = doc._index;
2433
3021
  } );
2434
3022
  const docIds = hits.map( ( doc ) => doc._id );
2435
- logger.info( { docIds } );
2436
3023
  // 2. Fetch all docs by ID to get 'actions' (in chunks if large)
2437
3024
  const getBody = [];
2438
3025
  for ( const doc of hits ) {
@@ -2440,71 +3027,76 @@ export async function updateTempStatus( req, res ) {
2440
3027
  }
2441
3028
 
2442
3029
  let mgetResp;
2443
- try {
2444
- mgetResp = await getOpenSearchData(
2445
- openSearch.revop,
2446
- {
2447
- query: {
2448
- ids: {
2449
- values: docIds,
2450
- },
3030
+
3031
+ mgetResp = await getOpenSearchData(
3032
+ openSearch.revop,
3033
+ {
3034
+ size: 10000,
3035
+ query: {
3036
+ ids: {
3037
+ values: docIds,
2451
3038
  },
2452
- _source: true,
2453
3039
  },
2454
- );
2455
- } catch ( err ) {
2456
- logger.error( { error: err } );
2457
- mgetResp = undefined;
2458
- }
2459
- logger.info( { mgetResp } );
3040
+ _source: true,
3041
+ },
3042
+ );
2460
3043
  // (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
2461
3044
  // For fallback, fetch all source fields via another search
2462
- let fullDocs = [];
2463
- if ( mgetResp && mgetResp.body && mgetResp.body.docs && Array.isArray( mgetResp.body.docs ) ) {
2464
- fullDocs = mgetResp.body.docs;
2465
- } else if ( searchResp.body && searchResp.body.hits && searchResp.body.hits.hits ) {
2466
- // fallback: use searchResp docs (request _source above)
2467
- fullDocs = searchResp.body.hits.hits;
2468
- }
3045
+ let fullDocs = mgetResp?.body?.hits?.hits || searchResp?.body?.hits?.hits || [];
2469
3046
 
2470
3047
  // 3. Prepare the new actions array for each doc, and set up bulk update payloads
2471
3048
  const reviewActions = [ 'approved', 'rejected' ];
2472
3049
  const docsToUpdate = [];
2473
- logger.info( { fullDocs: fullDocs } );
2474
3050
  for ( const doc of fullDocs ) {
2475
3051
  const source = doc._source || doc.fields || {}; // support mget and search hits
2476
3052
  let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
2477
3053
  if ( reviewActions.includes( status ) ) {
2478
3054
  // for review: update or push 'review'
2479
3055
  let found = false;
2480
- actions = actions.map( ( item ) => {
2481
- if ( item.actionType === 'review' ) {
2482
- found = true;
2483
- return { ...item, action: status };
2484
- }
2485
- return item;
2486
- } );
2487
- if ( !found ) {
2488
- actions.push( { actionType: 'review', action: status } );
2489
- }
2490
- } else {
2491
- // tagging: update or push 'tagging'
2492
- let found = false;
2493
- actions = actions.map( ( item ) => {
2494
- if ( item.actionType === 'tagging' ) {
2495
- found = true;
2496
- return { ...item, action: 'submitted' };
2497
- }
2498
- return item;
2499
- } );
2500
- if ( !found ) {
2501
- actions.push( { actionType: 'tagging', action: 'submitted' } );
3056
+ switch ( type ) {
3057
+ case 'review':
3058
+ actions = actions.map( ( item ) => {
3059
+ if ( item.actionType === 'review' ) {
3060
+ found = true;
3061
+ return { ...item, action: status };
3062
+ }
3063
+ return item;
3064
+ } );
3065
+ if ( !found ) {
3066
+ actions.push( { actionType: 'review', action: status } );
3067
+ }
3068
+ break;
3069
+ case 'approve':
3070
+ actions = actions.map( ( item ) => {
3071
+ if ( item.actionType === 'approve' ) {
3072
+ found = true;
3073
+ return { ...item, action: status };
3074
+ }
3075
+ return item;
3076
+ } );
3077
+ if ( !found ) {
3078
+ actions.push( { actionType: 'approve', action: status } );
3079
+ }
3080
+ break;
3081
+ default:
3082
+ return res.sendError( 'wrong vaue', 400 );
2502
3083
  }
2503
3084
  }
3085
+ let isChecked = true;
3086
+ switch ( type ) {
3087
+ case 'review':
3088
+ isChecked = status === 'approved' ? true : false;
3089
+ break;
3090
+ case 'approve':
3091
+ isChecked = status === 'approved' ? doc?._source?.isChecked : !doc?._source?.isChecked;
3092
+ break;
3093
+ }
2504
3094
  docsToUpdate.push( {
2505
3095
  _index: doc._index || docIdToIndex[doc._id],
2506
3096
  _id: doc._id,
2507
3097
  actions,
3098
+ isChecked: isChecked,
3099
+ comments: comments || '',
2508
3100
  } );
2509
3101
  }
2510
3102
  const bulkPayload = [];
@@ -2514,11 +3106,9 @@ export async function updateTempStatus( req, res ) {
2514
3106
  update: { _index: doc._index, _id: doc._id },
2515
3107
  } );
2516
3108
  bulkPayload.push( {
2517
- doc: { actions: doc.actions },
3109
+ doc: { actions: doc.actions, isChecked: doc?.isChecked, comments: doc?.comments || '' },
2518
3110
  } );
2519
3111
  }
2520
- logger.info( { bulkPayload: bulkPayload } );
2521
-
2522
3112
 
2523
3113
  // 3. Execute bulk update
2524
3114
  const bulkResp = await bulkUpdate( bulkPayload );
@@ -2526,8 +3116,50 @@ export async function updateTempStatus( req, res ) {
2526
3116
  // Count successes
2527
3117
  const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
2528
3118
 
2529
- logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
3119
+ if ( inputData?.comments && inputData?.comments !== '' ) {
3120
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
3121
+ const searchBody1 = {
3122
+ size: 10000,
3123
+ query: {
3124
+ bool: {
3125
+ must: [
3126
+ {
3127
+ terms: {
3128
+ '_id': docIds,
3129
+ },
3130
+ },
3131
+ {
3132
+ term: {
3133
+ isParent: false,
3134
+ },
3135
+ },
3136
+ ],
3137
+ },
3138
+ },
3139
+ };
3140
+
3141
+ const getSearchResp = await getOpenSearchData(
3142
+ openSearch.revop,
3143
+ searchBody1,
3144
+ );
3145
+
2530
3146
 
3147
+ const taggedImages = getSearchResp?.body?.hits?.hits?.length > 0 ? getSearchResp?.body?.hits?.hits : [];
3148
+ const logs = {
3149
+ type: inputData.type,
3150
+ storeId: taggedImages?.[0]?._source?.storeId,
3151
+ dateString: taggedImages?.[0]?._source?.dateString,
3152
+ category: taggedImages?.[0]?._source?.revopsType || '',
3153
+ taggedImages: taggedImages,
3154
+ status: inputData?.status,
3155
+ createdByEmail: req?.user?.email,
3156
+ createdByUserName: req?.user?.userName,
3157
+ createdByRole: req?.user?.role,
3158
+ message: inputData.comments || '',
3159
+ createdAt: new Date(),
3160
+ };
3161
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
3162
+ }
2531
3163
  return res.sendSuccess( { updated: updatedCount } );
2532
3164
  } catch ( error ) {
2533
3165
  const err = error.message;
@@ -2536,3 +3168,249 @@ export async function updateTempStatus( req, res ) {
2536
3168
  }
2537
3169
  }
2538
3170
 
3171
+ export async function updateUserTicketStatus( req, res ) {
3172
+ try {
3173
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
3174
+ const { storeId, dateString } = req.body || {};
3175
+
3176
+ if ( !storeId || !dateString ) {
3177
+ return res.sendError( 'storeId and dateString are required', 400 );
3178
+ }
3179
+
3180
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
3181
+
3182
+ // Fetch existing ticket so we can validate mappingInfo state
3183
+ const existingDoc = await getOpenSearchById( openSearch.footfallDirectory, docId );
3184
+ const ticketSource = existingDoc?.body?._source;
3185
+
3186
+ if ( !ticketSource ) {
3187
+ return res.sendError( 'Ticket not found', 404 );
3188
+ }
3189
+
3190
+ const mappingInfo = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [];
3191
+ if ( mappingInfo.length === 0 ) {
3192
+ return res.sendError( 'mappingInfo is missing for this ticket', 400 );
3193
+ }
3194
+
3195
+ const lastIndex = mappingInfo.length - 1;
3196
+ const lastEntry = mappingInfo[lastIndex];
3197
+
3198
+ if ( !lastEntry ) {
3199
+ return res.sendError( 'Unable to determine current ticket status', 400 );
3200
+ }
3201
+
3202
+ if ( String( lastEntry.status ).toLowerCase() !== 'open' ) {
3203
+ return res.sendError( 'Ticket is already picked by another user', 409 );
3204
+ }
3205
+
3206
+ const currentTime = new Date();
3207
+ const updatedMappingInfo = [ ...mappingInfo ];
3208
+ updatedMappingInfo[lastIndex] = {
3209
+ ...lastEntry,
3210
+ status: 'In-Progress',
3211
+ updatedAt: currentTime,
3212
+ createdByRole: req?.user?.role || '',
3213
+ createdByEmail: req?.user?.email || '',
3214
+ createdByUserName: req?.user?.userName || '',
3215
+ };
3216
+
3217
+ const updatePayload = {
3218
+ doc: {
3219
+ status: 'In-Progress',
3220
+ mappingInfo: updatedMappingInfo,
3221
+ updatedAt: currentTime,
3222
+ },
3223
+ };
3224
+
3225
+ const updateResult = await updateOpenSearchData(
3226
+ openSearch.footfallDirectory,
3227
+ docId,
3228
+ updatePayload,
3229
+ );
3230
+
3231
+
3232
+ if ( !updateResult || !( updateResult.statusCode === 200 || updateResult.statusCode === 201 ) ) {
3233
+ return res.sendError( 'Failed to update ticket status', 500 );
3234
+ }
3235
+ return res.sendSuccess( 'Ticket status updated successfully' );
3236
+ } catch ( error ) {
3237
+ const err = error.message;
3238
+ logger.info( { error: err, function: 'updateUserTicketStatus' } );
3239
+ return res.sendError( err, 500 );
3240
+ }
3241
+ }
3242
+
3243
+ export async function multiCloseTicket( req, res ) {
3244
+ try {
3245
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
3246
+ const inputData = req.body;
3247
+
3248
+ // inputData structure should include an array of items to close
3249
+ // Accept both array or single ticket update
3250
+ const tickets = Array.isArray( inputData.ticketList ) ? inputData.ticketList : [ inputData.ticketList ];
3251
+ // const mode = inputData.mode || '';
3252
+
3253
+ if ( !tickets.length ) {
3254
+ return res.sendError( 'No tickets provided', 400 );
3255
+ }
3256
+
3257
+ const results = [];
3258
+ for ( const ticket of tickets ) {
3259
+ const { storeId, dateString } = ticket || {};
3260
+ if ( !storeId || !dateString ) {
3261
+ results.push( { storeId, dateString, success: false, error: 'Missing storeId or dateString' } );
3262
+ continue;
3263
+ }
3264
+ // 1. Update the ticket document in footfallDirectory index
3265
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
3266
+
3267
+ // Fetch existing doc to update mappingInfo
3268
+
3269
+ const doc = await getOpenSearchById( openSearch.footfallDirectory, docId );
3270
+
3271
+
3272
+ const ticketSource = doc?.body?._source;
3273
+ if ( !ticketSource || !ticketSource.mappingInfo ) {
3274
+ results.push( { storeId, dateString, success: false, error: 'Ticket or mappingInfo missing' } );
3275
+ continue;
3276
+ }
3277
+
3278
+ let mappingInfoArray = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [ ticketSource.mappingInfo ];
3279
+ // Find all mappingInfo items matching type 'approve'
3280
+ let updated = false;
3281
+ let newMappingInfoArray = mappingInfoArray.map( ( mi, i ) => {
3282
+ if ( mi?.type === 'approve' && mi?.status === 'Open' ) {
3283
+ updated = true;
3284
+ return {
3285
+ ...mi,
3286
+ status: 'Approver-Closed',
3287
+ status: 'Closed', // following the user's instruction to set sttaus property (assumed typo, but explicitly used)
3288
+ updatedAt: new Date(),
3289
+ };
3290
+ }
3291
+ return mi;
3292
+ } );
3293
+
3294
+ if ( !updated ) {
3295
+ // None found to update
3296
+ results.push( { storeId, dateString, success: false, error: `coudn't approve this store` } );
3297
+ continue;
3298
+ }
3299
+
3300
+ // Write update to footfallDirectory
3301
+ const ticketUpdatePayload = {
3302
+ doc: {
3303
+ mappingInfo: newMappingInfoArray,
3304
+ status: 'Approver-Closed', // status updated at top level as well
3305
+ updatedAt: new Date(),
3306
+ },
3307
+ };
3308
+ let ticketUpdateResult;
3309
+ try {
3310
+ ticketUpdateResult = await updateOpenSearchData( openSearch.footfallDirectory, docId, ticketUpdatePayload );
3311
+ if ( !( ticketUpdateResult && ( ticketUpdateResult.statusCode === 200 || ticketUpdateResult.statusCode === 201 ) ) ) {
3312
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
3313
+ continue;
3314
+ }
3315
+ } catch ( err ) {
3316
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
3317
+ continue;
3318
+ }
3319
+
3320
+
3321
+ // For each ticket, update actions array for all matching image docs (storeId & dateString)
3322
+
3323
+ // Query to find all matching docs in revopTagging index with storeId and dateString
3324
+ const revopImageQuery = {
3325
+ size: 10000, // assume there won't be more than 1000 images per ticket
3326
+ query: {
3327
+ bool: {
3328
+ must: [
3329
+ { term: { 'storeId.keyword': storeId } },
3330
+ { term: { dateString: dateString } },
3331
+ ],
3332
+ },
3333
+ },
3334
+ };
3335
+
3336
+ // Fetch matching docs
3337
+ const searchRes = await getOpenSearchData( openSearch.revop, revopImageQuery );
3338
+ const revopHits = searchRes?.body?.hits?.hits || [];
3339
+
3340
+ // Optimized: batch and parallelize image-doc updates to avoid long sequential waits
3341
+ const BATCH_SIZE = 100;
3342
+ const now = new Date();
3343
+
3344
+ for ( let i = 0; i < revopHits.length; i += BATCH_SIZE ) {
3345
+ const batch = revopHits.slice( i, i + BATCH_SIZE );
3346
+
3347
+ const updatePromises = batch.map( async ( hit ) => {
3348
+ const imageDocId = hit._id;
3349
+ const imageSource = hit._source || {};
3350
+ const imageActionsArray = Array.isArray( imageSource.actions ) ? [ ...imageSource.actions ] : [];
3351
+
3352
+ imageActionsArray.push( {
3353
+ actionType: 'approve',
3354
+ action: 'approved',
3355
+ } );
3356
+
3357
+ const imageUpdatePayload = {
3358
+ doc: {
3359
+ actions: imageActionsArray,
3360
+ updatedAt: now,
3361
+ },
3362
+ };
3363
+
3364
+ return updateOpenSearchData( openSearch.revop, imageDocId, imageUpdatePayload );
3365
+ } );
3366
+
3367
+ // Wait for this batch to finish before starting the next one
3368
+ await Promise.all( updatePromises );
3369
+ }
3370
+ }
3371
+ if ( results && results?.length > 0 ) {
3372
+ return res.sendError( results, 500 );
3373
+ }
3374
+ // Return batch summary
3375
+ } catch ( error ) {
3376
+ const err = error.message;
3377
+ logger.info( { error: err, function: 'multiCloseTicket' } );
3378
+ return res.sendError( err, 500 );
3379
+ }
3380
+ }
3381
+
3382
+
3383
+ export async function checkTicketExists( req, res ) {
3384
+ try {
3385
+ let inputData = req.body;
3386
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
3387
+ let findQuery = {
3388
+ size: 10000,
3389
+ query: {
3390
+ bool: {
3391
+ must: [
3392
+ {
3393
+ term: {
3394
+ 'storeId.keyword': inputData.storeId,
3395
+ },
3396
+ },
3397
+ {
3398
+ term: {
3399
+ 'dateString': inputData.dateString,
3400
+ },
3401
+ },
3402
+ ],
3403
+ },
3404
+ },
3405
+ };
3406
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
3407
+ let Ticket = findTicket.body?.hits?.hits;
3408
+
3409
+
3410
+ res.sendSuccess( Ticket );
3411
+ } catch ( error ) {
3412
+ const err = error.message;
3413
+ logger.info( { error: err, function: 'checkTicketExists' } );
3414
+ return res.sendError( err, 500 );
3415
+ }
3416
+ }