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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -1,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,395 @@ 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
+
63
453
  async function bulkUpdateStatusToPending( indexName, inputData ) {
64
454
  const bulkBody = [];
65
455
 
@@ -195,7 +585,6 @@ export async function ticketSummary1( req, res ) {
195
585
 
196
586
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
197
587
  const aggs = getData?.body?.aggregations;
198
- logger.info( { aggs: aggs, body: getData?.body } );
199
588
 
200
589
  const result = {
201
590
  totalTickets: aggs.totalTicketCount.value,
@@ -220,10 +609,10 @@ export async function ticketSummary( req, res ) {
220
609
  try {
221
610
  let result = '';
222
611
  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 ) ) ) );
612
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
224
613
 
225
- if ( req.user.userType =='tango' ) {
226
- result ={
614
+ if ( req.user.userType == 'tango' ) {
615
+ result = {
227
616
  totalTickets: 0,
228
617
  averageAccuracyOverAll: 0,
229
618
  openTickets: 0,
@@ -234,30 +623,30 @@ export async function ticketSummary( req, res ) {
234
623
  ticketAccuracyBelow: '0%',
235
624
  };
236
625
  } 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';
626
+ result = req.user.role === 'superadmin' ?
627
+ {
628
+ totalTickets: 0,
629
+ openTickets: 0,
630
+ inprogress: 0,
631
+ closedTickets: 0,
632
+ dueToday: 0,
633
+ Expired: 0,
634
+ underTangoReview: 0,
635
+ avgTicket: '0%',
636
+ avgAccuracy: '0%',
637
+ } :
638
+ req.user.role === 'user' ? 'NA' :
639
+ ticketsFeature ?
640
+ {
641
+ totalTickets: 0,
642
+ openTickets: 0,
643
+ inprogress: 0,
644
+ closedTickets: 0,
645
+ dueToday: 0,
646
+ Expired: 0,
647
+ avgTicket: '0%',
648
+ avgAccuracy: '0%',
649
+ } : 'NA';
261
650
  }
262
651
 
263
652
  return res.sendSuccess( { result: result } );
@@ -293,7 +682,7 @@ export async function ticketList1( req, res ) {
293
682
  terms: {
294
683
  'clientId.keyword': Array.isArray( inputData.clientId ) ?
295
684
  inputData.clientId :
296
- [ inputData.clientId ],
685
+ [ inputData.clientId ],
297
686
  },
298
687
  },
299
688
  ];
@@ -470,12 +859,12 @@ export async function ticketList1( req, res ) {
470
859
  'Ticket raised on': dayjs( item._source.createdAt ).format( 'DD MMM, YYYY' ),
471
860
  'Issue Date': dayjs( item._source.dateString ).format( 'DD MMM, YYYY' ),
472
861
  '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 )} %`,
862
+ 'Duplicates': item?._source?.status === 'closed' ? item._source.duplicateACCount : item._source.duplicateCount,
863
+ 'Employee/Staff': item?._source?.status === 'closed' ? item._source.employeeACCount : item._source.employeeCount,
864
+ 'HouseKeeping': item?._source?.status === 'closed' ? item._source.houseKeepingACCount : item._source.houseKeepingCount,
865
+ 'Junk': item?._source?.status === 'closed' ? ( item._source.junkACCount || 0 ) : ( item._source.junkCount || 0 ),
866
+ '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 ) ),
867
+ '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
868
  'Status': item._source.status,
480
869
  } );
481
870
  }
@@ -491,825 +880,463 @@ export async function ticketList1( req, res ) {
491
880
 
492
881
  export async function ticketList( req, res ) {
493
882
  try {
494
- let result = '';
495
- const inputData= req.query;
883
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
884
+ const inputData = req.query;
496
885
  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 ) ) ) );
886
+ const limit = inputData?.limit || 10;
887
+ const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
888
+ inputData.clientId = inputData?.clientId?.split( ',' ); // convert strig to array
498
889
 
499
- if ( req.user.userType =='tango' ) {
500
- result =inputData.tangotype == 'store'?
501
890
 
891
+ const ticketsFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'reviewer' && ( m.isAdd == true || m.isEdit == true ) ) ) );
502
892
 
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',
531
- },
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',
713
- },
893
+ const ticketsApproveFeature = userInfo?.rolespermission?.some( ( f ) => f.featureName === 'FootfallDirectory' && ( f.modules.find( ( m ) => m.name == 'approver' && ( m.isAdd == true || m.isEdit == true ) ) ) );
714
894
 
715
- ] :
716
- [
895
+ const searchQuery = {
717
896
 
718
- {
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',
836
- },
837
- {
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',
851
- },
852
- {
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',
866
- },
867
- ];
868
- } 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';
1156
- }
897
+ size: limit, // or use parseInt(req.query.limit) for dynamic
898
+ from: offset, // or use parseInt(req.query.offset) for dynamic
899
+ sort: [ { 'createdAt': { order: 'desc' } } ],
1157
900
 
1158
- return res.sendSuccess( { result: result } );
1159
- } catch ( error ) {
1160
- const err = error.message || 'Internal Server Error';
1161
- logger.error( { error: error, messgage: req.query } );
1162
- return res.sendSuccess( err, 500 );
1163
- }
1164
- }
1165
-
1166
- export async function getTickets( req, res ) {
1167
- try {
1168
- const openSearch = JSON.parse( process.env.OPENSEARCH );
1169
- 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' ];
1175
- let filter = [
901
+ query: {
902
+ bool: {
903
+ must: [
904
+ {
905
+ 'range': {
906
+ 'dateString': {
907
+ 'gte': inputData.fromDate,
908
+ 'lte': inputData.toDate,
909
+ 'format': 'yyyy-MM-dd',
910
+ },
911
+ },
912
+ },
913
+ {
914
+ terms: {
915
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
916
+ inputData.clientId :
917
+ [ inputData.clientId ],
918
+ },
1176
919
 
1177
- {
1178
- range: {
1179
- dateString: {
1180
- gte: inputData.fromDate,
1181
- lte: inputData.toDate,
1182
- format: 'yyyy-MM-dd',
1183
- },
920
+ },
921
+ ],
1184
922
  },
1185
923
  },
1186
- ];
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
- );
924
+ };
925
+
926
+ if ( inputData.sortBy ) {
927
+ let sortOrder = inputData.sortOrder === 1 ? 'asc' : 'desc';
928
+
929
+ // Remove default sort so we don't duplicate/conflict
930
+ // INSERT_YOUR_CODE
931
+ // If sortBy is present, check if the field needs ".keyword" (for string fields like storeName, storeId, ticketId)
932
+ // This avoids OpenSearch errors about sorting on text fields.
933
+ const stringKeywordFields = [ 'storeName', 'storeId', 'ticketId', 'status', 'type', 'clientId' ];
934
+ let sortField = inputData.sortBy == 'footfall' ? 'footfallCount' : inputData.sortBy == 'issueDate' ? 'dateString' : inputData.sortBy;
935
+ if ( stringKeywordFields.includes( sortField ) ) {
936
+ sortField = `${sortField}.keyword`;
937
+ }
938
+
939
+ // Remove default sort so we don't duplicate/conflict
940
+ searchQuery.sort = [
941
+ { [sortField]: { order: sortOrder } },
942
+ ];
1197
943
  }
1198
- if ( inputData?.dateString ) {
1199
- filter.push(
1200
- {
1201
- term: {
1202
- 'dateString': inputData.dateString,
1203
- },
1204
- },
1205
- );
944
+ // Example: Filtering by storeId if present in the query
945
+ if ( inputData.storeId ) {
946
+ inputData.storeId = inputData?.storeId?.split( ',' );
947
+ searchQuery.query.bool.must.push( {
948
+ terms: {
949
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
950
+ inputData.storeId :
951
+ [ inputData.storeId ],
952
+ },
953
+ } );
1206
954
  }
1207
- if ( inputData.status ) {
1208
- filter.push(
1209
- {
1210
- term: {
1211
- 'status.keyword': inputData.status,
1212
- },
955
+ if ( inputData.status && inputData.status!=='' ) {
956
+ inputData.status = inputData?.status?.split( ',' );
957
+ if ( req.user.userType === 'tango' ) {
958
+ searchQuery.query.bool.must.push( {
959
+ terms: {
960
+ 'status': Array.isArray( inputData?.status ) ?
961
+ inputData?.status :
962
+ [ inputData?.status ],
1213
963
  },
1214
- );
1215
- }
1216
- if (
1217
- inputData.revopsType
1218
- ) {
1219
- inputData.revopsType === 'employee' ?
1220
- filter.push( {
1221
- range: {
1222
- employeeCount: {
1223
- gt: 0,
964
+ } );
965
+ } else if ( inputData?.permissionType === 'approve' ) {
966
+ searchQuery.query.bool.must.push( {
967
+ nested: {
968
+ path: 'mappingInfo',
969
+ query: {
970
+ bool: {
971
+ must: [
972
+ {
973
+ term: {
974
+ 'mappingInfo.type': 'approve',
975
+ },
976
+ },
977
+ {
978
+ term: {
979
+ 'mappingInfo.status': inputData.status[0],
980
+ },
981
+ },
982
+ ],
983
+ },
1224
984
  },
1225
985
  },
1226
- } ) :
1227
- inputData.revopsType === 'houseKeeping' ?
1228
- filter.push( {
1229
- range: {
1230
- houseKeepingCount: {
1231
- gt: 0,
986
+ } );
987
+ } else if ( inputData?.permissionType === 'review' ) {
988
+ searchQuery.query.bool.must.push( {
989
+ nested: {
990
+ path: 'mappingInfo',
991
+ query: {
992
+ bool: {
993
+ must: [
994
+ {
995
+ term: {
996
+ 'mappingInfo.type': 'review',
997
+ },
998
+ },
999
+ {
1000
+ term: {
1001
+ 'mappingInfo.status': inputData.status[0],
1002
+ },
1003
+ },
1004
+ ],
1232
1005
  },
1233
1006
  },
1234
- } ) :
1235
- inputData.revopsType === 'junk' ?
1236
- filter.push( {
1237
- range: {
1238
- junkCount: {
1239
- gt: 0,
1007
+ },
1008
+ } );
1009
+ } else if ( ticketsFeature ) {
1010
+ searchQuery.query.bool.must.push( {
1011
+ nested: {
1012
+ path: 'mappingInfo',
1013
+ query: {
1014
+ bool: {
1015
+ must: [
1016
+ {
1017
+ term: {
1018
+ 'mappingInfo.type': 'review',
1019
+ },
1020
+ },
1021
+ {
1022
+ term: {
1023
+ 'mappingInfo.status': inputData.status[0],
1024
+ },
1025
+ },
1026
+ ],
1240
1027
  },
1241
1028
  },
1242
- } ) :
1243
- filter.push( {
1244
- range: {
1245
- duplicateCount: {
1246
- gt: 0,
1029
+ },
1030
+ } );
1031
+ } else if ( ticketsApproveFeature ) {
1032
+ searchQuery.query.bool.must.push( {
1033
+ nested: {
1034
+ path: 'mappingInfo',
1035
+ query: {
1036
+ bool: {
1037
+ must: [
1038
+ {
1039
+ term: {
1040
+ 'mappingInfo.type': 'approve',
1041
+ },
1042
+ },
1043
+ {
1044
+ term: {
1045
+ 'mappingInfo.status': inputData.status[0],
1046
+ },
1047
+ },
1048
+ ],
1247
1049
  },
1248
1050
  },
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' ] : [];
1051
+ },
1052
+ } );
1053
+ }
1254
1054
  }
1255
1055
 
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
- },
1056
+ if ( req?.user?.userType == 'tango' && inputData.tangoType !== 'internal' ) {
1057
+ searchQuery.query.bool.must.push(
1058
+ {
1059
+ term: {
1060
+ 'type.keyword': 'store',
1266
1061
  },
1267
- {
1268
- constant_score: {
1269
- filter: { term: { 'employeeStatus.keyword': inputData.action } },
1270
- boost: 1,
1271
- _name: 'matched_employee',
1062
+ },
1063
+ {
1064
+ nested: {
1065
+ path: 'mappingInfo',
1066
+ query: {
1067
+ bool: {
1068
+ must: [
1069
+ {
1070
+ term: {
1071
+ 'mappingInfo.type': 'tangoreview',
1072
+ },
1073
+ },
1074
+
1075
+ ],
1076
+ },
1272
1077
  },
1273
1078
  },
1274
- {
1275
- constant_score: {
1276
- filter: { term: { 'duplicateStatus.keyword': inputData.action } },
1277
- boost: 1,
1278
- _name: 'matched_duplicate',
1079
+
1080
+ },
1081
+
1082
+
1083
+ );
1084
+ } else if ( req?.user?.userType == 'client' && inputData.tangoType !== 'internal' ) {
1085
+ searchQuery.query.bool.must.push(
1086
+ {
1087
+ term: {
1088
+ 'type.keyword': 'store',
1089
+ },
1090
+ },
1091
+ {
1092
+ nested: {
1093
+ path: 'mappingInfo',
1094
+ query: {
1095
+ bool: {
1096
+ must: [
1097
+ {
1098
+ term: {
1099
+ 'mappingInfo.type': ticketsFeature ? 'review' : ticketsApproveFeature ?
1100
+ 'approve' : 'tagging',
1101
+ },
1102
+ },
1103
+
1104
+ ],
1105
+ },
1279
1106
  },
1280
1107
  },
1281
- {
1282
- constant_score: {
1283
- filter: { term: { 'junkStatus.keyword': inputData.action } },
1284
- boost: 1,
1285
- _name: 'matched_junk',
1108
+ },
1109
+
1110
+ );
1111
+ }
1112
+
1113
+ if ( inputData?.permissionType ) {
1114
+ searchQuery.query.bool.must.push(
1115
+ {
1116
+ nested: {
1117
+ path: 'mappingInfo',
1118
+ query: {
1119
+ bool: {
1120
+ must: [
1121
+ {
1122
+ term: {
1123
+ 'mappingInfo.type': inputData?.permissionType == 'review' ? 'review' :
1124
+ 'approve',
1125
+ },
1126
+ },
1127
+
1128
+ ],
1129
+ },
1286
1130
  },
1287
1131
  },
1288
- ],
1289
- minimum_should_match: 1,
1290
- },
1291
- } );
1132
+ },
1133
+ );
1292
1134
  }
1293
1135
 
1136
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
1137
+ searchQuery.query.bool['should'] = [];
1138
+ searchQuery.query.bool.should = [
1294
1139
 
1295
- let getRevCount = {};
1296
- if ( inputData.revopsType ) {
1297
- getRevCount = {
1298
- size: 0,
1299
- query: {
1300
- bool: {
1301
- filter: filter,
1140
+ {
1141
+ 'wildcard': {
1142
+ 'storeName.keyword': {
1143
+ 'value': `*${inputData.searchValue}*`,
1144
+ },
1302
1145
  },
1303
1146
  },
1304
- aggs: {
1305
- totalCount: {
1306
- sum: {
1307
- field: inputData.revopsType == 'employee' ? 'employeeCount' : inputData.revopsType == 'houseKeeping' ? 'houseKeepingCount' :inputData.revopsType == 'junk' ? 'junkCount': 'duplicateCount',
1147
+ {
1148
+ 'wildcard': {
1149
+ 'storeId.keyword': {
1150
+ 'value': `*${inputData.searchValue}*`,
1308
1151
  },
1309
1152
  },
1310
1153
  },
1311
- };
1154
+ {
1155
+ 'wildcard': {
1156
+ 'ticketId.keyword': {
1157
+ 'value': `*${inputData.searchValue}*`,
1158
+ },
1159
+ },
1160
+ },
1161
+
1162
+
1163
+ ];
1164
+ searchQuery.query.bool['minimum_should_match'] = 1;
1165
+ }
1166
+ // You can add more filters as needed
1167
+ const searchResult = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
1168
+ const count = searchResult?.body?.hits?.total?.value || 0;
1169
+
1170
+ if ( count === 0 ) {
1171
+ return res.sendError( 'no data found', 204 );
1172
+ }
1173
+ const ticketListData = searchResult?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1174
+
1175
+ let temp = [];
1176
+ if ( req.user.userType == 'tango' ) {
1177
+ if ( inputData.tangotype == 'store' ) {
1178
+ for ( let item of ticketListData ) {
1179
+ temp.push( {
1180
+
1181
+ ticketId: item?.ticketId,
1182
+ storeId: item?.storeId,
1183
+ storeName: item?.storeName,
1184
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1185
+ issueDate: item?.dateString,
1186
+ dueDate: '',
1187
+ footfall: item?.footfallCount,
1188
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1189
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1190
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1191
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1192
+ status: item?.status,
1193
+
1194
+ } );
1195
+ }
1196
+ } else {
1197
+ for ( let item of ticketListData ) {
1198
+ temp.push( {
1199
+
1200
+ ticketId: item?.ticketId,
1201
+ storeId: item?.storeId,
1202
+ storeName: item?.storeName,
1203
+
1204
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1205
+ issueDate: item?.dateString,
1206
+ footfall: item?.footfallCount,
1207
+ dueDate: '',
1208
+ type: item?.type || 'store',
1209
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1210
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1211
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1212
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1213
+ status: item?.status,
1214
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1215
+
1216
+ } );
1217
+ }
1218
+ }
1219
+ } else {
1220
+ if ( inputData?.permissionType === 'approve' ) {
1221
+ for ( let item of ticketListData ) {
1222
+ temp.push( {
1223
+
1224
+ ticketId: item?.ticketId,
1225
+ storeId: item?.storeId,
1226
+ storeName: item?.storeName,
1227
+
1228
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1229
+ issueDate: item?.dateString,
1230
+ dueDate: '',
1231
+ footfall: item?.footfallCount,
1232
+
1233
+ type: item.type || 'store',
1234
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1235
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1236
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1237
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1238
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
1239
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1240
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
1241
+
1242
+ } );
1243
+ }
1244
+ } else if ( inputData?.permissionType === 'review' ) {
1245
+ for ( let item of ticketListData ) {
1246
+ temp.push( {
1247
+
1248
+ ticketId: item?.ticketId,
1249
+ storeId: item?.storeId,
1250
+ storeName: item?.storeName,
1251
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1252
+ issueDate: item?.dateString,
1253
+ footfall: item?.footfallCount,
1254
+ dueDate: '',
1255
+ type: item.type || 'store',
1256
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1257
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1258
+
1259
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
1260
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
1261
+
1262
+ } );
1263
+ }
1264
+ } else if ( req.user.role === 'user' ) {
1265
+ temp = [];
1266
+ } else if ( ticketsFeature ) {
1267
+ for ( let item of ticketListData ) {
1268
+ temp.push( {
1269
+
1270
+ ticketId: item?.ticketId,
1271
+ storeId: item?.storeId,
1272
+ storeName: item?.storeName,
1273
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1274
+ issueDate: item?.dateString,
1275
+ footfall: item?.footfallCount,
1276
+ dueDate: '',
1277
+ type: item.type || 'store',
1278
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1279
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1280
+
1281
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.status || '--',
1282
+ ReviewedBy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.createdByEmail || '--',
1283
+
1284
+ } );
1285
+ }
1286
+ } else if ( ticketsApproveFeature ) {
1287
+ for ( let item of ticketListData ) {
1288
+ temp.push( {
1289
+
1290
+ ticketId: item?.ticketId,
1291
+ storeId: item?.storeId,
1292
+ storeName: item?.storeName,
1293
+
1294
+ ticketRaised: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.createdAt,
1295
+ issueDate: item?.dateString,
1296
+ dueDate: '',
1297
+ footfall: item?.footfallCount,
1298
+
1299
+ type: item.type || 'store',
1300
+ storeRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tagging' )?.revicedPerc || '--',
1301
+ reviewerRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'review' )?.revicedPerc || '--',
1302
+ approverRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.revicedPerc || '--',
1303
+ tangoRevisedAccuracy: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1304
+ status: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.status || '--',
1305
+ tangoStatus: item?.mappingInfo?.find( ( f ) => f.type === 'tangoreview' )?.revicedPerc || '--',
1306
+ approvedBy: item?.mappingInfo?.find( ( f ) => f.type === 'approve' )?.createdByEmail || '--',
1307
+
1308
+ } );
1309
+ }
1310
+ } else {
1311
+ temp = [];
1312
+ }
1312
1313
  }
1314
+
1315
+ return res.sendSuccess( { result: temp, count: count } );
1316
+ } catch ( error ) {
1317
+ const err = error.message || 'Internal Server Error';
1318
+ logger.error( { error: error, messgage: req.query } );
1319
+ return res.sendSuccess( err, 500 );
1320
+ }
1321
+ }
1322
+
1323
+ export async function getTickets( req, res ) {
1324
+ try {
1325
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1326
+ const inputData = req.query;
1327
+
1328
+
1329
+ 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' ];
1330
+ let filter = [
1331
+
1332
+ {
1333
+ term: {
1334
+ 'ticketId.keyword': inputData.ticketId,
1335
+ },
1336
+ },
1337
+ ];
1338
+
1339
+
1313
1340
  const getCount = {
1314
1341
  query: {
1315
1342
  bool: {
@@ -1320,20 +1347,14 @@ export async function getTickets( req, res ) {
1320
1347
 
1321
1348
 
1322
1349
  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 );
1350
+
1351
+
1352
+ if ( !geteDataCount ) {
1353
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
1332
1354
  }
1333
1355
 
1334
1356
  const getQuery = {
1335
- size: limit,
1336
- from: skip,
1357
+ size: 1,
1337
1358
  query: {
1338
1359
  bool: {
1339
1360
  filter: filter,
@@ -1343,14 +1364,9 @@ export async function getTickets( req, res ) {
1343
1364
  };
1344
1365
 
1345
1366
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
1346
-
1347
1367
  const response = getData?.body?.hits?.hits;
1348
1368
  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 );
1369
+ return res.sendError( `No Pending items in the selected dates for the store`, 400 );
1354
1370
  }
1355
1371
  let temp = [];
1356
1372
  if ( inputData?.action ) {
@@ -1388,56 +1404,307 @@ export async function getTickets( req, res ) {
1388
1404
  approverUserName: hit?._source?.approverUserName,
1389
1405
  approverEmail: hit?._source?.approverEmail,
1390
1406
  approverRole: hit?._source?.approverRole,
1407
+ type: hit?._source?.type,
1391
1408
  };
1392
1409
  let result;
1393
1410
 
1411
+
1394
1412
  const matched = hit.matched_queries;
1395
1413
  // Add only matched data array
1396
- if ( matched.includes( 'matched_employee' )&& ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
1414
+ if ( matched.includes( 'matched_employee' ) && ( !inputData.revopsType || inputData.revopsType == 'employee' ) ) {
1397
1415
  result = defaultData;
1398
1416
  result.employee = hit?._source?.employee;
1399
1417
  // result.type = 'employee';
1400
1418
  result.matched = matched;
1401
1419
  }
1402
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
1420
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
1403
1421
  result = defaultData;
1404
1422
  result.houseKeeping = hit?._source?.houseKeeping;
1405
1423
  result.matched = matched;
1406
1424
  }
1407
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
1425
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
1408
1426
  result = defaultData;
1409
1427
  result.duplicateImages = hit?._source?.duplicateImages;
1410
1428
  result.matched = matched;
1411
1429
  }
1412
1430
 
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
- }
1431
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
1432
+ result = defaultData;
1433
+ result.junk = hit?._source?.junk;
1434
+ result.matched = matched;
1435
+ }
1436
+
1437
+ if ( result ) {
1438
+ const nested = [
1439
+ {
1440
+ _id: hit._id,
1441
+ _index: hit._index,
1442
+ _score: 0,
1443
+ _source: result,
1444
+ },
1445
+ ];
1446
+ temp.push( ...nested );
1447
+ }
1448
+ } );
1449
+ }
1450
+
1451
+ const finalResponse = inputData.action ? temp : response;
1452
+ const getRevopQuery = {
1453
+ size: 10000,
1454
+ query: {
1455
+ bool: {
1456
+ must: [
1457
+ { term: { 'storeId.keyword': response?.[0]?._source?.storeId } }, // assuming inputData.storeId is an array
1458
+ { term: { 'dateString': response?.[0]?._source?.dateString } },
1459
+ ],
1460
+ },
1461
+ },
1462
+ };
1463
+
1464
+
1465
+ const revopResp = await getOpenSearchData( openSearch.revop, getRevopQuery );
1466
+
1467
+
1468
+ // Map revopResp.body.hits.hits to revopSources
1469
+ const revopSources = Array.isArray( revopResp?.body?.hits?.hits ) ?
1470
+ revopResp?.body?.hits?.hits?.map( ( hit ) => hit._source ) :
1471
+ [];
1472
+
1473
+ // Create a map of revopSources by id for quick lookup
1474
+ const revopSourcesMap = new Map();
1475
+ revopSources.forEach( ( item ) => {
1476
+ if ( item?.id ) {
1477
+ revopSourcesMap.set( String( item.id ), item );
1478
+ }
1479
+ } );
1480
+
1481
+ // Process revopSources to replace duplicateImage entries with full objects from the array
1482
+ const processedRevopSources = revopSources.map( ( item ) => {
1483
+ // Check if this is a duplicate parent item
1484
+ if ( item?.revopsType === 'duplicate' && item?.isParent === true && Array.isArray( item?.duplicateImage ) ) {
1485
+ // Map each duplicateImage entry to the full object from revopSources
1486
+ const updatedDuplicateImage = item.duplicateImage.map( ( duplicateImg ) => {
1487
+ const duplicateId = String( duplicateImg?.id );
1488
+ // Find the full object in revopSources that matches this id
1489
+ const fullObject = revopSourcesMap.get( duplicateId );
1490
+ // Return the full object if found, otherwise return the original duplicateImg
1491
+ return fullObject || duplicateImg;
1492
+ } );
1493
+
1494
+ return {
1495
+ ...item,
1496
+ duplicateImage: updatedDuplicateImage,
1497
+ };
1498
+ }
1499
+ // Return item as-is if it doesn't meet the criteria
1500
+ return item;
1501
+ } );
1418
1502
 
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
1503
  if ( finalResponse?.length == 0 || !finalResponse ) {
1434
1504
  if ( inputData.storeId?.length > 0 ) {
1435
- const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1 } );
1505
+ const getStoreName = await findOneStore( { storeId: response?.[0]?._source?.storeId }, { storeName: 1 } );
1436
1506
  return res.sendError( `No Pending items in the selected dates for the store ${getStoreName?.storeName || ''}`, 400 );
1437
1507
  }
1438
1508
  return res.sendError( 'No Data found', 204 );
1439
1509
  }
1440
- return res.sendSuccess( { result: finalResponse, count: count, revopCount: revCount } );
1510
+
1511
+
1512
+ // replace the mappingInfo.revisedDetail with processedRevopSources
1513
+ if ( Array.isArray( finalResponse ) ) {
1514
+ for ( let item of finalResponse ) {
1515
+ if (
1516
+ item &&
1517
+ item._source &&
1518
+ item._source.mappingInfo
1519
+
1520
+ ) {
1521
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
1522
+ const vmsCommentsLogIndex = openSearch.vmsCommentsLog || 'vms-comments-log-dev';
1523
+ let commentsResponse = [];
1524
+ const commentsFilter = [
1525
+ { term: { 'storeId.keyword': item?._source?.storeId } },
1526
+ { term: { dateString: item?._source?.dateString } },
1527
+
1528
+ ];
1529
+
1530
+ const commentsQuery = {
1531
+ size: 10000,
1532
+ sort: [
1533
+ { createdAt: { order: 'desc' } }, // Sort descending by createdAt
1534
+ ],
1535
+ query: {
1536
+ bool: {
1537
+ filter: commentsFilter,
1538
+ },
1539
+ },
1540
+ };
1541
+
1542
+ const commentsRes = await getOpenSearchData( vmsCommentsLogIndex, commentsQuery );
1543
+ // If mappingInfo is an array, update revisedDetail for each mappingInfo object
1544
+ if ( Array.isArray( item._source.mappingInfo ) ) {
1545
+ item._source.mappingInfo.forEach( ( mappingObj ) => {
1546
+ commentsResponse = commentsRes?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
1547
+
1548
+ // Check if duplicate condition exists in commentsResponse
1549
+
1550
+ // Structure comments output
1551
+ let commentsDetails = [];
1552
+
1553
+
1554
+ const types = [ 'tagging', 'review', 'approve' ];
1555
+
1556
+ // Process each type
1557
+ types.forEach( ( typeValue ) => {
1558
+ if ( typeValue === 'tagging' ) {
1559
+ // For tagging, group by category and create separate objects for each category
1560
+ const taggingComments = commentsResponse.filter( ( c ) => c.type === typeValue );
1561
+
1562
+ // Group by category
1563
+ const categoryGroups = {};
1564
+ taggingComments.forEach( ( c ) => {
1565
+ const category = c.category || 'other';
1566
+ if ( !categoryGroups[category] ) {
1567
+ categoryGroups[category] = [];
1568
+ }
1569
+ categoryGroups[category].push( c );
1570
+ } );
1571
+
1572
+ // Create separate objects for each category
1573
+ Object.keys( categoryGroups ).forEach( ( category ) => {
1574
+ const categoryComments = categoryGroups[category];
1575
+ let parent = null;
1576
+
1577
+ const comms = categoryComments.map( ( c ) => {
1578
+ if ( category === 'duplicate' ) {
1579
+ if ( !parent && c.parent ) {
1580
+ parent = c.parent;
1581
+ }
1582
+ return {
1583
+ createdByEmail: c.createdByEmail,
1584
+ createdByUserName: c.createdByUserName,
1585
+ createdByRole: c.createdByRole,
1586
+ message: c.message,
1587
+ };
1588
+ } else {
1589
+ return {
1590
+ id: c.id,
1591
+ tempId: c.tempId,
1592
+ timeRange: c.timeRange,
1593
+ entryTime: c.entryTime,
1594
+ exitTime: c.exitTime,
1595
+ filePath: c.filePath,
1596
+ isChecked: c.isChecked,
1597
+ createdAt: c.createdAt,
1598
+ message: c.message,
1599
+ createdByEmail: c.createdByEmail,
1600
+ createdByUserName: c.createdByUserName,
1601
+ createdByRole: c.createdByRole,
1602
+ };
1603
+ }
1604
+ } );
1605
+
1606
+ const taggingObj = {
1607
+ category: category,
1608
+ type: typeValue,
1609
+ comments: comms,
1610
+ };
1611
+
1612
+ // Add parent only for duplicate category
1613
+ if ( category === 'duplicate' && parent !== null ) {
1614
+ taggingObj.parent = parent;
1615
+ }
1616
+
1617
+ commentsDetails.push( taggingObj );
1618
+ } );
1619
+ } else if ( typeValue === 'review' || typeValue === 'approve' ) {
1620
+ // For review and approve, keep existing structure
1621
+ const comms = commentsResponse
1622
+ .filter( ( c ) => c.type === typeValue )
1623
+ .map( ( c ) => {
1624
+ if ( c.category === 'duplicate' ) {
1625
+ return {
1626
+ parent: c?.taggedImages[0]?._source?.parent,
1627
+ category: c.category,
1628
+ taggedImages: Array.isArray( c.taggedImages ) ?
1629
+ c.taggedImages.map( ( img ) => ( {
1630
+ id: img?._source?.id,
1631
+ tempId: img?._source?.tempId,
1632
+ timeRange: img?._source?.timeRange,
1633
+ entryTime: img?._source?.entryTime,
1634
+ exitTime: img?._source?.exitTime,
1635
+ filePath: img?._source?.filePath,
1636
+ isChecked: img?._source?.isChecked,
1637
+ } ) ) :
1638
+ [],
1639
+ createdByEmail: c.createdByEmail,
1640
+ createdByUserName: c.createdByUserName,
1641
+ createdByRole: c.createdByRole,
1642
+ status: c.status,
1643
+ message: c.message,
1644
+ };
1645
+ } else {
1646
+ return {
1647
+ category: c.category,
1648
+ taggedImages: Array.isArray( c.taggedImages ) ?
1649
+ c.taggedImages.map( ( img ) => ( {
1650
+ id: img?._source?.id,
1651
+ tempId: img?._source?.tempId,
1652
+ timeRange: img?._source?.timeRange,
1653
+ entryTime: img?._source?.entryTime,
1654
+ exitTime: img?._source?.exitTime,
1655
+ filePath: img?._source?.filePath,
1656
+ isChecked: img?._source?.isChecked,
1657
+ } ) ) :
1658
+ [],
1659
+ createdByEmail: c.createdByEmail,
1660
+ createdByUserName: c.createdByUserName,
1661
+ createdByRole: c.createdByRole,
1662
+ status: c.status,
1663
+ message: c.message,
1664
+ };
1665
+ }
1666
+ } );
1667
+
1668
+ // Only add if there are comments
1669
+ if ( comms.length > 0 ) {
1670
+ commentsDetails.push( {
1671
+ type: typeValue,
1672
+ comments: comms,
1673
+ } );
1674
+ } else {
1675
+ // Add empty comments array if no comments
1676
+ commentsDetails.push( {
1677
+ type: typeValue,
1678
+ comments: [],
1679
+ } );
1680
+ }
1681
+ }
1682
+ } );
1683
+
1684
+
1685
+ item._source.commentsDetails = commentsDetails;
1686
+
1687
+ if (
1688
+ Object.prototype.hasOwnProperty.call( mappingObj, 'revisedDetail' ) &&
1689
+ mappingObj.type !== 'tangoreview'
1690
+ ) {
1691
+ mappingObj.revisedDetail = processedRevopSources;
1692
+ }
1693
+ } );
1694
+ } else {
1695
+ item._source.mappingInfo.revisedDetail = processedRevopSources;
1696
+ }
1697
+ }
1698
+ }
1699
+ } else if (
1700
+ finalResponse &&
1701
+ finalResponse._source &&
1702
+ finalResponse._source.mappingInfo
1703
+ ) {
1704
+ finalResponse._source.mappingInfo.revisedDetail = processedRevopSources;
1705
+ }
1706
+
1707
+ return res.sendSuccess( { result: finalResponse } );
1441
1708
  } catch ( error ) {
1442
1709
  const err = error.message || 'Internal Server Error';
1443
1710
  logger.error( { error: error, messgage: req.query } );
@@ -1509,7 +1776,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1509
1776
  item.isChecked == true ? tempId.push( { tempId: item.tempId, timeRange: item.timeRange } ) : null;
1510
1777
  bulkBody.push(
1511
1778
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${item.timeRange}_${item.tempId}` } },
1512
- { doc: { isChecked: item.isChecked, status: item?.isChecked == true? 'approved':'rejected' } },
1779
+ { doc: { isChecked: item.isChecked, status: item?.isChecked == true ? 'approved' : 'rejected' } },
1513
1780
  );
1514
1781
  } );
1515
1782
  }
@@ -1524,11 +1791,11 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1524
1791
  updateData.employee = updatedEmployee;
1525
1792
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: updateData } );
1526
1793
  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
- );
1794
+ ( employee.isChecked == true ) ? tempId.push( { tempId: employee.tempId, timeRange: employee.timeRange } ) : null;
1795
+ bulkBody.push(
1796
+ { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
1797
+ { doc: { isChecked: employee.isChecked, status: employee?.isChecked == true ? 'approved' : 'rejected' } },
1798
+ );
1532
1799
  }
1533
1800
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { employeeACCount: tempId?.length || 0 } } );
1534
1801
  }
@@ -1547,7 +1814,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1547
1814
  houseKeeping.isChecked == true ? tempId.push( { tempId: houseKeeping.tempId, timeRange: houseKeeping.timeRange } ) : null;
1548
1815
  bulkBody.push(
1549
1816
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${houseKeeping.timeRange}_${houseKeeping.tempId}` } },
1550
- { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true? 'approved':'rejected' } },
1817
+ { doc: { isChecked: houseKeeping.isChecked, status: houseKeeping?.isChecked == true ? 'approved' : 'rejected' } },
1551
1818
  );
1552
1819
  }
1553
1820
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { houseKeepingACCount: tempId?.length || 0 } } );
@@ -1568,7 +1835,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1568
1835
  junk.isChecked == true ? tempId.push( { tempId: junk.tempId, timeRange: junk.timeRange } ) : null;
1569
1836
  bulkBody.push(
1570
1837
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${junk.timeRange}_${junk.tempId}` } },
1571
- { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true? 'approved':'rejected' } },
1838
+ { doc: { isChecked: junk.isChecked, status: junk?.isChecked == true ? 'approved' : 'rejected' } },
1572
1839
  );
1573
1840
  }
1574
1841
  await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { junkACCount: tempId?.length || 0 } } );
@@ -1593,7 +1860,7 @@ export async function updateTicketStatus( data, openSearch, temp, user ) {
1593
1860
  let getUpdateExistingOne = await getOpenSearchById( openSearch.footfallDirectory, _id );
1594
1861
  const tempIdList = await extractCheckedTempIds( getUpdateExistingOne?.body );
1595
1862
  const isSendMessge = await sendSqsMessage( data, tempIdList, getStoreType, storeId );
1596
- if ( isSendMessge ==true ) {
1863
+ if ( isSendMessge == true ) {
1597
1864
  return true; // res.sendSuccess( 'Ticket has been updated successfully' );
1598
1865
  } else {
1599
1866
  return false; // res.sendError( 'No SQS message sent', 500 );
@@ -1656,7 +1923,7 @@ function updateEmployeeCheckFlags( existingEmployees, inputEmployees, status ) {
1656
1923
  // Step 2: Loop through all existing and update isChecked accordingly
1657
1924
  const updatedEmployees = existingEmployees.map( ( emp ) => ( {
1658
1925
  ...emp,
1659
- isChecked: status === 'rejected'? !checkedTempIds.has( emp.tempId ):checkedTempIds.has( emp.tempId ),
1926
+ isChecked: status === 'rejected' ? !checkedTempIds.has( emp.tempId ) : checkedTempIds.has( emp.tempId ),
1660
1927
  } ) );
1661
1928
 
1662
1929
  return updatedEmployees;
@@ -1706,7 +1973,7 @@ function mergeDuplicateImagesWithUncheck( existingData, inputData, status ) {
1706
1973
  export async function sendSqsMessage( inputData, tempId, getStoreType, storeId ) {
1707
1974
  const sqs = JSON.parse( process.env.SQS );
1708
1975
  const openSearch = JSON.parse( process.env.OPENSEARCH );
1709
- const sqsName = getStoreType > 0? sqs.revopTrackTicket: sqs.revopTicket;
1976
+ const sqsName = getStoreType > 0 ? sqs.revopTrackTicket : sqs.revopTicket;
1710
1977
  const sqsProduceQueue = getStoreType > 0 ? {
1711
1978
  QueueUrl: `${sqs.url}${sqsName}`,
1712
1979
  MessageBody: JSON.stringify( {
@@ -1734,12 +2001,11 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1734
2001
 
1735
2002
  } ),
1736
2003
  };
1737
- const sqsQueue = getStoreType > 0? await sendMessageToFIFOQueue( sqsProduceQueue ):
1738
- await sendMessageToQueue(
1739
- sqsProduceQueue.QueueUrl,
1740
- sqsProduceQueue.MessageBody,
1741
-
1742
- );
2004
+ const sqsQueue = getStoreType > 0 ? await sendMessageToFIFOQueue( sqsProduceQueue ) :
2005
+ await sendMessageToQueue(
2006
+ sqsProduceQueue.QueueUrl,
2007
+ sqsProduceQueue.MessageBody,
2008
+ );
1743
2009
  if ( sqsQueue.statusCode ) {
1744
2010
  logger.error( {
1745
2011
  error: `${sqsQueue}`,
@@ -1747,7 +2013,7 @@ export async function sendSqsMessage( inputData, tempId, getStoreType, storeId )
1747
2013
  } );
1748
2014
  return false;
1749
2015
  } else {
1750
- const id =`${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0? 'live':'reduction'}_${Date.now()}`;
2016
+ const id = `${storeId}_${inputData.dateString.split( '-' ).reverse().join( '-' )}_${getStoreType > 0 ? 'live' : 'reduction'}_${Date.now()}`;
1751
2017
  const logs = {
1752
2018
  QueueUrl: sqsProduceQueue.QueueUrl,
1753
2019
  MessageBody: sqsProduceQueue.MessageBody,
@@ -1797,13 +2063,7 @@ export async function getTaggedStores( req, res ) {
1797
2063
  },
1798
2064
  },
1799
2065
  ];
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
- // }
2066
+
1807
2067
  filter.push(
1808
2068
  {
1809
2069
  terms: { 'storeId.keyword': req?.stores || [] },
@@ -1918,8 +2178,8 @@ export async function downloadTickets( req, res ) {
1918
2178
  {
1919
2179
  terms: {
1920
2180
  'storeId.keyword': Array.isArray( inputData.storeId ) ?
1921
- inputData.storeId :
1922
- inputData.storeId,
2181
+ inputData.storeId :
2182
+ inputData.storeId,
1923
2183
  },
1924
2184
  },
1925
2185
  );
@@ -1967,26 +2227,26 @@ export async function downloadTickets( req, res ) {
1967
2227
  },
1968
2228
 
1969
2229
  } ) :
1970
- inputData.revopsType === 'junk' ?
1971
- filter.push( {
1972
- range: {
1973
- junkCount: {
1974
- gt: 0,
2230
+ inputData.revopsType === 'junk' ?
2231
+ filter.push( {
2232
+ range: {
2233
+ junkCount: {
2234
+ gt: 0,
2235
+ },
1975
2236
  },
1976
- },
1977
- } ):
2237
+ } ) :
1978
2238
 
1979
- filter.push( {
1980
- range: {
1981
- duplicateCount: {
1982
- gt: 0,
2239
+ filter.push( {
2240
+ range: {
2241
+ duplicateCount: {
2242
+ gt: 0,
2243
+ },
1983
2244
  },
1984
- },
1985
- } );
2245
+ } );
1986
2246
  source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1987
2247
  inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] :
1988
2248
  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' ] : [];
2249
+ inputData.revopsType == 'junk' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'junkCount', 'comments', 'junk', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus', 'junkStatus' ] : [];
1990
2250
  }
1991
2251
 
1992
2252
  if ( inputData.action ) {
@@ -2063,7 +2323,7 @@ export async function downloadTickets( req, res ) {
2063
2323
  let temp = [];
2064
2324
  if ( inputData?.action ) {
2065
2325
  response.map( ( hit ) => {
2066
- const defaultData ={
2326
+ const defaultData = {
2067
2327
  storeId: hit._source.storeId,
2068
2328
  dateString: hit?._source?.dateString,
2069
2329
  ticketName: hit?._source?.ticketName,
@@ -2093,19 +2353,19 @@ export async function downloadTickets( req, res ) {
2093
2353
  // result.type = 'employee';
2094
2354
  result.matched = matched;
2095
2355
  }
2096
- if ( matched.includes( 'matched_housekeeping' )&& ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
2356
+ if ( matched.includes( 'matched_housekeeping' ) && ( !inputData.revopsType || inputData.revopsType == 'houseKeeping' ) ) {
2097
2357
  logger.info( { revop: inputData.revopsType } );
2098
2358
  result = defaultData;
2099
2359
  result.houseKeeping = hit?._source?.houseKeeping;
2100
2360
  result.matched = matched;
2101
2361
  }
2102
- if ( matched.includes( 'matched_duplicate' )&& ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
2362
+ if ( matched.includes( 'matched_duplicate' ) && ( !inputData.revopsType || inputData.revopsType == 'duplicateImages' ) ) {
2103
2363
  logger.info( { revop: inputData.revopsType } );
2104
2364
  result = defaultData;
2105
2365
  result.duplicateImages = hit?._source?.duplicateImages;
2106
2366
  result.matched = matched;
2107
2367
  }
2108
- if ( matched.includes( 'matched_junk' )&& ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
2368
+ if ( matched.includes( 'matched_junk' ) && ( !inputData.revopsType || inputData.revopsType == 'junk' ) ) {
2109
2369
  result = defaultData;
2110
2370
  result.junk = hit?._source?.junk;
2111
2371
  result.matched = matched;
@@ -2149,7 +2409,7 @@ export async function downloadTickets( req, res ) {
2149
2409
  revopsType: inputData?.revopsType,
2150
2410
  type: 'get-tickets',
2151
2411
  };
2152
- const record={
2412
+ const record = {
2153
2413
  stores: inputData?.storeId,
2154
2414
  fromDate: inputData?.fromDate,
2155
2415
  toDate: inputData?.toDate,
@@ -2167,7 +2427,7 @@ export async function downloadTickets( req, res ) {
2167
2427
  const sqs = JSON.parse( process.env.SQS );
2168
2428
  const sqsName = sqs.revopDownload;
2169
2429
 
2170
- const sqsProduceQueue ={
2430
+ const sqsProduceQueue = {
2171
2431
  QueueUrl: `${sqs.url}${sqsName}`,
2172
2432
  MessageBody: JSON.stringify( {
2173
2433
  _id: getId?._id,
@@ -2196,7 +2456,6 @@ export async function downloadTickets( req, res ) {
2196
2456
  }
2197
2457
 
2198
2458
  async function extractTempIds( document ) {
2199
- logger.info( { document: document } );
2200
2459
  const source = document?._source || {};
2201
2460
  const result = [];
2202
2461
 
@@ -2246,7 +2505,7 @@ export async function reviewerList( req, res ) {
2246
2505
  featureName: 'FootfallDirectory',
2247
2506
  modules: {
2248
2507
  $elemMatch: {
2249
- name: 'Reviewer',
2508
+ name: inputData?.type === 'review' ? 'reviewer' : 'approver',
2250
2509
  $or: [ { isAdd: true }, { isEdit: true } ],
2251
2510
  },
2252
2511
  },
@@ -2255,7 +2514,7 @@ export async function reviewerList( req, res ) {
2255
2514
  };
2256
2515
 
2257
2516
  const getUserlist = await findUser( reviewerRoleQuery, { userName: 1, email: 1, role: 1 } );
2258
- return res.sendSuccess( getUserlist|| [] );
2517
+ return res.sendSuccess( getUserlist || [] );
2259
2518
  } catch ( error ) {
2260
2519
  const err = error.message || 'Internal Server Error';
2261
2520
  return res.sendError( err, 500 );
@@ -2277,6 +2536,16 @@ export async function openTicketList( req, res ) {
2277
2536
  clientId: Array.isArray( clientId ) ? clientId : [ clientId ],
2278
2537
  },
2279
2538
  },
2539
+ {
2540
+ term: {
2541
+ 'mappingInfo.type': inputData.type,
2542
+ },
2543
+ },
2544
+ {
2545
+ term: {
2546
+ 'mappingInfo.status.keyword': 'Open',
2547
+ },
2548
+ },
2280
2549
  {
2281
2550
  range: {
2282
2551
  dateString: {
@@ -2295,14 +2564,19 @@ export async function openTicketList( req, res ) {
2295
2564
  filter: filter,
2296
2565
  },
2297
2566
  },
2298
- _source: [ 'ticketId', 'storeName', 'revicedFootfall', 'footfallCount', 'revicedPerc' ],
2567
+ _source: [ 'ticketId', 'storeName', 'storeId', 'dateString', 'revicedFootfall', 'footfallCount', 'revicedPerc', 'type' ],
2299
2568
  };
2569
+ // INSERT_YOUR_CODE
2570
+ // Add sorting by revicedPerc descending (highest revised accuracy first)
2571
+ openSearchQuery.sort = [
2572
+ { 'revicedPerc.keyword': { order: inputData?.sortOrder === 1 ? 'asc' : 'desc' } },
2573
+ ];
2300
2574
 
2301
2575
 
2302
2576
  // Assuming getOpenSearchData and openSearch.footfallDirectoryTagging are available
2303
2577
  const result = await getOpenSearchData( openSearch.footfallDirectory, openSearchQuery );
2304
2578
  const getUserlist = result?.body?.hits?.hits?.map( ( hit ) => hit._source ) || [];
2305
- return res.sendSuccess( getUserlist|| [] );
2579
+ return res.sendSuccess( getUserlist || [] );
2306
2580
  } catch ( error ) {
2307
2581
  const err = error.message || 'Internal Server Error';
2308
2582
  logger.error( { error: error, function: 'openTicketList' } );
@@ -2315,73 +2589,32 @@ export async function assignTicket( req, res ) {
2315
2589
  const inputData = req.body;
2316
2590
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2317
2591
 
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
-
2592
+ const { email, userName, role, storeId, dateString } = inputData;
2593
+ const _id = `${storeId}_${dateString}_footfall-directory-tagging`;
2381
2594
 
2382
- logger.info( { response } );
2595
+ const getTicket = await getOpenSearchById( openSearch.footfallDirectory, _id );
2596
+ if ( !getTicket ) {
2597
+ return res.sendError( 'Ticket is not found', 400 );
2598
+ }
2599
+ const source = getTicket?.body?._source;
2600
+ const mappingInfo = Array.isArray( source.mappingInfo ) ? source.mappingInfo : [];
2601
+ if ( mappingInfo.length === 0 ) {
2602
+ return res.sendError( 'Ticket is not found', 400 );
2603
+ }
2604
+ const lastIndex = mappingInfo.length - 1;
2605
+ const lastEntry = mappingInfo[lastIndex];
2606
+ if ( String( lastEntry.status ) !== 'In-Progress' ) {
2607
+ return res.sendError( 'Ticket is not in progress', 400 );
2608
+ }
2383
2609
 
2384
- return res.sendSuccess( { updated: response?.body?.updated ?? 0 } );
2610
+ const currentTime = new Date();
2611
+ const updatedMappingInfo = [ ...mappingInfo ];
2612
+ updatedMappingInfo[lastIndex] = { ...lastEntry, createdByEmail: email, updatedAt: currentTime, createdByUserName: userName, createdByRole: role };
2613
+ const updateResult = await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: { mappingInfo: updatedMappingInfo } } );
2614
+ if ( !updateResult ) {
2615
+ return res.sendError( 'Failed to update ticket', 400 );
2616
+ }
2617
+ return res.sendSuccess( { message: 'Ticket assigned successfully' } );
2385
2618
  } catch ( error ) {
2386
2619
  const err = error.message || 'Internal Server Error';
2387
2620
  logger.error( { error: error, function: 'assignTicket' } );
@@ -2392,11 +2625,13 @@ export async function assignTicket( req, res ) {
2392
2625
  export async function updateTempStatus( req, res ) {
2393
2626
  try {
2394
2627
  const openSearch = JSON.parse( process.env.OPENSEARCH );
2395
- const { id, status } = req.body;
2628
+ const inputData = req.body;
2629
+ const { id, type, status, comments } = inputData;
2396
2630
 
2397
2631
  // Use bulk update API via bucketing (batch update) -- fetch docs, then bulk-update
2398
2632
  // 1. Search for all documents matching the ticket IDs
2399
2633
  const searchBody = {
2634
+ size: 10000,
2400
2635
  query: {
2401
2636
  bool: {
2402
2637
  must: [
@@ -2415,16 +2650,14 @@ export async function updateTempStatus( req, res ) {
2415
2650
  openSearch.revop,
2416
2651
  searchBody,
2417
2652
  );
2418
- logger.info( { searchResp: searchResp } );
2653
+
2419
2654
  // Extract bulk IDs to update
2420
2655
  const hits = searchResp?.body?.hits?.hits ?? [];
2421
- logger.info( { hits: hits } );
2656
+
2422
2657
  if ( !hits.length ) {
2423
2658
  return res.sendError( 'no data', 204 );
2424
2659
  }
2425
2660
 
2426
- // 2. Build bulk update commands
2427
- // Each doc: { update: { _id: ..., _index: ... } }, { doc: { status: status } }
2428
2661
 
2429
2662
  // 1. Get all IDs from hits
2430
2663
  const docIdToIndex = {};
@@ -2432,7 +2665,6 @@ export async function updateTempStatus( req, res ) {
2432
2665
  docIdToIndex[doc._id] = doc._index;
2433
2666
  } );
2434
2667
  const docIds = hits.map( ( doc ) => doc._id );
2435
- logger.info( { docIds } );
2436
2668
  // 2. Fetch all docs by ID to get 'actions' (in chunks if large)
2437
2669
  const getBody = [];
2438
2670
  for ( const doc of hits ) {
@@ -2440,71 +2672,76 @@ export async function updateTempStatus( req, res ) {
2440
2672
  }
2441
2673
 
2442
2674
  let mgetResp;
2443
- try {
2444
- mgetResp = await getOpenSearchData(
2445
- openSearch.revop,
2446
- {
2447
- query: {
2448
- ids: {
2449
- values: docIds,
2450
- },
2675
+
2676
+ mgetResp = await getOpenSearchData(
2677
+ openSearch.revop,
2678
+ {
2679
+ size: 10000,
2680
+ query: {
2681
+ ids: {
2682
+ values: docIds,
2451
2683
  },
2452
- _source: true,
2453
2684
  },
2454
- );
2455
- } catch ( err ) {
2456
- logger.error( { error: err } );
2457
- mgetResp = undefined;
2458
- }
2459
- logger.info( { mgetResp } );
2685
+ _source: true,
2686
+ },
2687
+ );
2460
2688
  // (If you have a utility for multi-get, you may want to use that. Else, you might need to fetch each by ID.)
2461
2689
  // 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
- }
2690
+ let fullDocs = mgetResp?.body?.hits?.hits || searchResp?.body?.hits?.hits || [];
2469
2691
 
2470
2692
  // 3. Prepare the new actions array for each doc, and set up bulk update payloads
2471
2693
  const reviewActions = [ 'approved', 'rejected' ];
2472
2694
  const docsToUpdate = [];
2473
- logger.info( { fullDocs: fullDocs } );
2474
2695
  for ( const doc of fullDocs ) {
2475
2696
  const source = doc._source || doc.fields || {}; // support mget and search hits
2476
2697
  let actions = Array.isArray( source.actions ) ? [ ...source.actions ] : [];
2477
2698
  if ( reviewActions.includes( status ) ) {
2478
2699
  // for review: update or push 'review'
2479
2700
  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' } );
2701
+ switch ( type ) {
2702
+ case 'review':
2703
+ actions = actions.map( ( item ) => {
2704
+ if ( item.actionType === 'review' ) {
2705
+ found = true;
2706
+ return { ...item, action: status };
2707
+ }
2708
+ return item;
2709
+ } );
2710
+ if ( !found ) {
2711
+ actions.push( { actionType: 'review', action: status } );
2712
+ }
2713
+ break;
2714
+ case 'approve':
2715
+ actions = actions.map( ( item ) => {
2716
+ if ( item.actionType === 'approve' ) {
2717
+ found = true;
2718
+ return { ...item, action: status };
2719
+ }
2720
+ return item;
2721
+ } );
2722
+ if ( !found ) {
2723
+ actions.push( { actionType: 'approve', action: status } );
2724
+ }
2725
+ break;
2726
+ default:
2727
+ return res.sendError( 'wrong vaue', 400 );
2502
2728
  }
2503
2729
  }
2730
+ let isChecked = true;
2731
+ switch ( type ) {
2732
+ case 'review':
2733
+ isChecked = status === 'approved' ? true : false;
2734
+ break;
2735
+ case 'approve':
2736
+ isChecked = status === 'approved' ? doc?._source?.isChecked : !doc?._source?.isChecked;
2737
+ break;
2738
+ }
2504
2739
  docsToUpdate.push( {
2505
2740
  _index: doc._index || docIdToIndex[doc._id],
2506
2741
  _id: doc._id,
2507
2742
  actions,
2743
+ isChecked: isChecked,
2744
+ comments: comments || '',
2508
2745
  } );
2509
2746
  }
2510
2747
  const bulkPayload = [];
@@ -2514,11 +2751,9 @@ export async function updateTempStatus( req, res ) {
2514
2751
  update: { _index: doc._index, _id: doc._id },
2515
2752
  } );
2516
2753
  bulkPayload.push( {
2517
- doc: { actions: doc.actions },
2754
+ doc: { actions: doc.actions, isChecked: doc?.isChecked, comments: doc?.comments || '' },
2518
2755
  } );
2519
2756
  }
2520
- logger.info( { bulkPayload: bulkPayload } );
2521
-
2522
2757
 
2523
2758
  // 3. Execute bulk update
2524
2759
  const bulkResp = await bulkUpdate( bulkPayload );
@@ -2526,8 +2761,50 @@ export async function updateTempStatus( req, res ) {
2526
2761
  // Count successes
2527
2762
  const updatedCount = bulkResp?.body?.items?.filter( ( item ) => item?.update?.result === 'updated' || item?.update?.result === 'noop' ).length ?? 0;
2528
2763
 
2529
- logger.info( { updated: updatedCount, by: 'updateTempStatus', ids: id } );
2764
+ if ( inputData?.comments && inputData?.comments !== '' ) {
2765
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
2766
+ const searchBody1 = {
2767
+ size: 10000,
2768
+ query: {
2769
+ bool: {
2770
+ must: [
2771
+ {
2772
+ terms: {
2773
+ '_id': docIds,
2774
+ },
2775
+ },
2776
+ {
2777
+ term: {
2778
+ isParent: false,
2779
+ },
2780
+ },
2781
+ ],
2782
+ },
2783
+ },
2784
+ };
2785
+
2786
+ const getSearchResp = await getOpenSearchData(
2787
+ openSearch.revop,
2788
+ searchBody1,
2789
+ );
2790
+
2530
2791
 
2792
+ const taggedImages = getSearchResp?.body?.hits?.hits?.length > 0 ? getSearchResp?.body?.hits?.hits : [];
2793
+ const logs = {
2794
+ type: inputData.type,
2795
+ storeId: taggedImages?.[0]?._source?.storeId,
2796
+ dateString: taggedImages?.[0]?._source?.dateString,
2797
+ category: taggedImages?.[0]?._source?.revopsType || '',
2798
+ taggedImages: taggedImages,
2799
+ status: inputData?.status,
2800
+ createdByEmail: req?.user?.email,
2801
+ createdByUserName: req?.user?.userName,
2802
+ createdByRole: req?.user?.role,
2803
+ message: inputData.comments || '',
2804
+ createdAt: new Date(),
2805
+ };
2806
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
2807
+ }
2531
2808
  return res.sendSuccess( { updated: updatedCount } );
2532
2809
  } catch ( error ) {
2533
2810
  const err = error.message;
@@ -2536,3 +2813,249 @@ export async function updateTempStatus( req, res ) {
2536
2813
  }
2537
2814
  }
2538
2815
 
2816
+ export async function updateUserTicketStatus( req, res ) {
2817
+ try {
2818
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
2819
+ const { storeId, dateString } = req.body || {};
2820
+
2821
+ if ( !storeId || !dateString ) {
2822
+ return res.sendError( 'storeId and dateString are required', 400 );
2823
+ }
2824
+
2825
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
2826
+
2827
+ // Fetch existing ticket so we can validate mappingInfo state
2828
+ const existingDoc = await getOpenSearchById( openSearch.footfallDirectory, docId );
2829
+ const ticketSource = existingDoc?.body?._source;
2830
+
2831
+ if ( !ticketSource ) {
2832
+ return res.sendError( 'Ticket not found', 404 );
2833
+ }
2834
+
2835
+ const mappingInfo = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [];
2836
+ if ( mappingInfo.length === 0 ) {
2837
+ return res.sendError( 'mappingInfo is missing for this ticket', 400 );
2838
+ }
2839
+
2840
+ const lastIndex = mappingInfo.length - 1;
2841
+ const lastEntry = mappingInfo[lastIndex];
2842
+
2843
+ if ( !lastEntry ) {
2844
+ return res.sendError( 'Unable to determine current ticket status', 400 );
2845
+ }
2846
+
2847
+ if ( String( lastEntry.status ).toLowerCase() !== 'open' ) {
2848
+ return res.sendError( 'Ticket is already picked by another user', 409 );
2849
+ }
2850
+
2851
+ const currentTime = new Date();
2852
+ const updatedMappingInfo = [ ...mappingInfo ];
2853
+ updatedMappingInfo[lastIndex] = {
2854
+ ...lastEntry,
2855
+ status: 'In-Progress',
2856
+ updatedAt: currentTime,
2857
+ createdByRole: req?.user?.role || '',
2858
+ createdByEmail: req?.user?.email || '',
2859
+ createdByUserName: req?.user?.userName || '',
2860
+ };
2861
+
2862
+ const updatePayload = {
2863
+ doc: {
2864
+ status: 'In-Progress',
2865
+ mappingInfo: updatedMappingInfo,
2866
+ updatedAt: currentTime,
2867
+ },
2868
+ };
2869
+
2870
+ const updateResult = await updateOpenSearchData(
2871
+ openSearch.footfallDirectory,
2872
+ docId,
2873
+ updatePayload,
2874
+ );
2875
+
2876
+
2877
+ if ( !updateResult || !( updateResult.statusCode === 200 || updateResult.statusCode === 201 ) ) {
2878
+ return res.sendError( 'Failed to update ticket status', 500 );
2879
+ }
2880
+ return res.sendSuccess( 'Ticket status updated successfully' );
2881
+ } catch ( error ) {
2882
+ const err = error.message;
2883
+ logger.info( { error: err, function: 'updateUserTicketStatus' } );
2884
+ return res.sendError( err, 500 );
2885
+ }
2886
+ }
2887
+
2888
+ export async function multiCloseTicket( req, res ) {
2889
+ try {
2890
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
2891
+ const inputData = req.body;
2892
+
2893
+ // inputData structure should include an array of items to close
2894
+ // Accept both array or single ticket update
2895
+ const tickets = Array.isArray( inputData.ticketList ) ? inputData.ticketList : [ inputData.ticketList ];
2896
+ // const mode = inputData.mode || '';
2897
+
2898
+ if ( !tickets.length ) {
2899
+ return res.sendError( 'No tickets provided', 400 );
2900
+ }
2901
+
2902
+ const results = [];
2903
+ for ( const ticket of tickets ) {
2904
+ const { storeId, dateString } = ticket || {};
2905
+ if ( !storeId || !dateString ) {
2906
+ results.push( { storeId, dateString, success: false, error: 'Missing storeId or dateString' } );
2907
+ continue;
2908
+ }
2909
+ // 1. Update the ticket document in footfallDirectory index
2910
+ const docId = `${storeId}_${dateString}_footfall-directory-tagging`;
2911
+
2912
+ // Fetch existing doc to update mappingInfo
2913
+
2914
+ const doc = await getOpenSearchById( openSearch.footfallDirectory, docId );
2915
+
2916
+
2917
+ const ticketSource = doc?.body?._source;
2918
+ if ( !ticketSource || !ticketSource.mappingInfo ) {
2919
+ results.push( { storeId, dateString, success: false, error: 'Ticket or mappingInfo missing' } );
2920
+ continue;
2921
+ }
2922
+
2923
+ let mappingInfoArray = Array.isArray( ticketSource.mappingInfo ) ? ticketSource.mappingInfo : [ ticketSource.mappingInfo ];
2924
+ // Find all mappingInfo items matching type 'approve'
2925
+ let updated = false;
2926
+ let newMappingInfoArray = mappingInfoArray.map( ( mi, i ) => {
2927
+ if ( mi?.type === 'approve' && mi?.status === 'Open' ) {
2928
+ updated = true;
2929
+ return {
2930
+ ...mi,
2931
+ status: 'Approver-Closed',
2932
+ status: 'Closed', // following the user's instruction to set sttaus property (assumed typo, but explicitly used)
2933
+ updatedAt: new Date(),
2934
+ };
2935
+ }
2936
+ return mi;
2937
+ } );
2938
+
2939
+ if ( !updated ) {
2940
+ // None found to update
2941
+ results.push( { storeId, dateString, success: false, error: `coudn't approve this store` } );
2942
+ continue;
2943
+ }
2944
+
2945
+ // Write update to footfallDirectory
2946
+ const ticketUpdatePayload = {
2947
+ doc: {
2948
+ mappingInfo: newMappingInfoArray,
2949
+ status: 'Approver-Closed', // status updated at top level as well
2950
+ updatedAt: new Date(),
2951
+ },
2952
+ };
2953
+ let ticketUpdateResult;
2954
+ try {
2955
+ ticketUpdateResult = await updateOpenSearchData( openSearch.footfallDirectory, docId, ticketUpdatePayload );
2956
+ if ( !( ticketUpdateResult && ( ticketUpdateResult.statusCode === 200 || ticketUpdateResult.statusCode === 201 ) ) ) {
2957
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
2958
+ continue;
2959
+ }
2960
+ } catch ( err ) {
2961
+ results.push( { storeId, dateString, success: false, error: 'Failed to update ticket' } );
2962
+ continue;
2963
+ }
2964
+
2965
+
2966
+ // For each ticket, update actions array for all matching image docs (storeId & dateString)
2967
+
2968
+ // Query to find all matching docs in revopTagging index with storeId and dateString
2969
+ const revopImageQuery = {
2970
+ size: 10000, // assume there won't be more than 1000 images per ticket
2971
+ query: {
2972
+ bool: {
2973
+ must: [
2974
+ { term: { 'storeId.keyword': storeId } },
2975
+ { term: { dateString: dateString } },
2976
+ ],
2977
+ },
2978
+ },
2979
+ };
2980
+
2981
+ // Fetch matching docs
2982
+ const searchRes = await getOpenSearchData( openSearch.revop, revopImageQuery );
2983
+ const revopHits = searchRes?.body?.hits?.hits || [];
2984
+
2985
+ // Optimized: batch and parallelize image-doc updates to avoid long sequential waits
2986
+ const BATCH_SIZE = 100;
2987
+ const now = new Date();
2988
+
2989
+ for ( let i = 0; i < revopHits.length; i += BATCH_SIZE ) {
2990
+ const batch = revopHits.slice( i, i + BATCH_SIZE );
2991
+
2992
+ const updatePromises = batch.map( async ( hit ) => {
2993
+ const imageDocId = hit._id;
2994
+ const imageSource = hit._source || {};
2995
+ const imageActionsArray = Array.isArray( imageSource.actions ) ? [ ...imageSource.actions ] : [];
2996
+
2997
+ imageActionsArray.push( {
2998
+ actionType: 'approve',
2999
+ action: 'approved',
3000
+ } );
3001
+
3002
+ const imageUpdatePayload = {
3003
+ doc: {
3004
+ actions: imageActionsArray,
3005
+ updatedAt: now,
3006
+ },
3007
+ };
3008
+
3009
+ return updateOpenSearchData( openSearch.revop, imageDocId, imageUpdatePayload );
3010
+ } );
3011
+
3012
+ // Wait for this batch to finish before starting the next one
3013
+ await Promise.all( updatePromises );
3014
+ }
3015
+ }
3016
+ if ( results && results?.length > 0 ) {
3017
+ return res.sendError( results, 500 );
3018
+ }
3019
+ // Return batch summary
3020
+ } catch ( error ) {
3021
+ const err = error.message;
3022
+ logger.info( { error: err, function: 'multiCloseTicket' } );
3023
+ return res.sendError( err, 500 );
3024
+ }
3025
+ }
3026
+
3027
+
3028
+ export async function checkTicketExists( req, res ) {
3029
+ try {
3030
+ let inputData = req.body;
3031
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
3032
+ let findQuery = {
3033
+ size: 10000,
3034
+ query: {
3035
+ bool: {
3036
+ must: [
3037
+ {
3038
+ term: {
3039
+ 'storeId.keyword': inputData.storeId,
3040
+ },
3041
+ },
3042
+ {
3043
+ term: {
3044
+ 'dateString': inputData.dateString,
3045
+ },
3046
+ },
3047
+ ],
3048
+ },
3049
+ },
3050
+ };
3051
+ let findTicket = await getOpenSearchData( openSearch.footfallDirectory, findQuery );
3052
+ let Ticket = findTicket.body?.hits?.hits;
3053
+
3054
+
3055
+ res.sendSuccess( Ticket );
3056
+ } catch ( error ) {
3057
+ const err = error.message;
3058
+ logger.info( { error: err, function: 'checkTicketExists' } );
3059
+ return res.sendError( err, 500 );
3060
+ }
3061
+ }