tango-app-api-infra 3.8.1-beta.0 → 3.8.1-beta.10

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.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "tango-app-api-infra",
3
- "version": "3.8.1-beta.0",
3
+ "version": "3.8.1-beta.10",
4
4
  "description": "infra",
5
5
  "main": "index.js",
6
6
  "type": "module",
@@ -1,4 +1,4 @@
1
- import { download, logger } from 'tango-app-api-middleware';
1
+ import { download, logger, sendMessageToQueue } from 'tango-app-api-middleware';
2
2
  import { bulkUpdate, 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 dayjs from 'dayjs';
@@ -10,11 +10,12 @@ export async function createTicket( req, res ) {
10
10
  const getStoreName = await findOneStore( { storeId: inputData.storeId }, { storeName: 1, _id: 0 } );
11
11
  inputData.ticketId = 'TE_FDT_' + new Date().valueOf();
12
12
  inputData.clientId = inputData?.storeId?.split( '-' )[0];
13
- inputData.storeName =getStoreName?.storeName;
13
+ inputData.storeName = getStoreName?.storeName;
14
14
  inputData.createdAt = new Date();
15
15
  inputData.updatedAt = new Date();
16
- inputData.userName = req.user.userName;
17
- inputData.role = req.user.role;
16
+ inputData.userName = req?.user?.userName;
17
+ inputData.email = req?.user?.email;
18
+ inputData.role = req?.user?.role;
18
19
  inputData.status = 'open';
19
20
  if ( inputData.houseKeepingCount > 0 ) {
20
21
  inputData.houseKeepingStatus = 'pending';
@@ -67,12 +68,19 @@ async function bulkUpdateStatusToPending( indexName, inputData ) {
67
68
 
68
69
  // 3. Duplicate Images > data[]
69
70
  if ( inputData.duplicateCount > 0 ) {
71
+ let updatedDuplicateImages = [];
70
72
  for ( const dup of inputData.duplicateImages || [] ) {
71
73
  const id = `${inputData.storeId}_${inputData.dateString}_${dup.timeRange}_${dup.tempId}`;
72
- const updatedDuplicateImages = ( dup.data || [] ).map( ( item ) => ( {
73
- ...item,
74
- status: 'pending',
75
- } ) );
74
+ dup?.data?.map( ( item ) => {
75
+ updatedDuplicateImages.push( {
76
+ ...item,
77
+ status: 'pending',
78
+ } );
79
+ bulkBody.push(
80
+ { update: { _index: indexName, _id: `${inputData.storeId}_${inputData.dateString}_${item.timeRange}_${item.tempId}` } },
81
+ { doc: { status: 'pending' } },
82
+ );
83
+ } );
76
84
  bulkBody.push(
77
85
  { update: { _index: indexName, _id: id } },
78
86
  { doc: { status: 'pending', duplicateImage: updatedDuplicateImages } },
@@ -174,120 +182,201 @@ export async function ticketSummary( req, res ) {
174
182
 
175
183
  export async function ticketList( req, res ) {
176
184
  try {
177
- // let count = 0;
178
- // const array =[
179
- // { A: '3c88e8eb6d499a26d37ca075c390bd2d', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/101' },
180
- // { A: 'c3dd981f485ccd25221c7da704f35d24', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/101' },
181
- // { A: 'ee887e5546ab0e96ca0adbf8c2453c8d', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/1001' },
182
- // { A: 'a59ddeb30d158602d6c672994af2f79a', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/1001' },
183
- // { A: '1f3318280025f5456262b0322b806940', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/201' },
184
- // { A: '925a70751a0988793057148b20158186', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/201',
185
- // }, { A: 'd6d4ab301c322212fb612a7d9661b6be', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/301',
186
- // }, { A: 'cb09aeedfa5ade043922634eb1c35e46', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/301',
187
- // }, { A: '58f81e33f66ef7af15424c1dc44e7de1', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/401',
188
- // }, { A: '7ac8f73f02347d29f6f2bff3c52275f5', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/401',
189
- // }, { A: '9599acd19a48eb28957ad18e40a0a5b1', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/401',
190
- // }, { A: '36584a3ce215a5fbd1c0e6535201020c', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/501',
191
- // }, { A: 'cb7e91870e0a72d4eb691e2618aa5979', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/501',
192
- // }, { A: 'cd072627e6924df745cba004616ec3c9', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/601',
193
- // }, { A: 'a5891519a50f68a9d73a774539fe8496', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/601',
194
- // }, { A: '2008517125b03b1b9a66348e6d017523', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/701',
195
- // }, { A: '79fc93551b59f9ed07eab48d190ba7f4', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/701',
196
- // }, { A: '89365abcfc8dc663b26471d81a7f68eb', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/801',
197
- // }, { A: 'e9fb3b989a245994bd48b97287aad096', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/801',
198
- // }, { A: '4493fb6dc07611bab03630b154ca2483', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/801',
199
- // }, { A: 'e31caf56bb86f6e12377db006ac61756', B: 'rtsp://admin:Bli$$%40145@192.168.29.86/streaming/channels/901',
200
- // }, { A: 'fe66c637bc8da296485494e371f67b99', B: 'rtsp://admin:Bli$$%40145@192.168.29.58/streaming/channels/901' },
201
- // ];
202
-
203
- // for ( const item of array ) {
204
- // const a = await updateOneCamera( { streamName: item.A, clientId: '370' }, { RTSP: item.B } );
205
- // count++;
206
- // logger.info( { aaa: a, count: count } );
207
- // }
208
-
209
- // process.exit();
210
185
  const openSearch = JSON.parse( process.env.OPENSEARCH );
211
186
  const inputData = req.query;
212
187
  const limit = inputData.limit || 10;
213
- const skip= inputData.offset == 0? 0:( inputData.offset - 1 ) *limit || 0;
188
+ const offset = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
189
+ const order = inputData?.sortOrder || -1;
190
+
214
191
  inputData.clientId = inputData.clientId.split( ',' ); // convert strig to array
215
- logger.info( { inputData: inputData, limit: limit, skip: skip } );
216
- const getCount= {
217
- query: {
218
- bool: {
219
- filter: [
220
- { terms: { 'clientId.keyword': Array.isArray( inputData.clientId ) ?
221
- inputData.clientId :
222
- inputData.clientId } },
223
- {
224
- range: {
225
- dateString: {
226
- gte: inputData.fromDate,
227
- lte: inputData.toDate,
228
- format: 'yyyy-MM-dd',
229
- },
230
- },
231
- },
232
- ],
192
+
193
+
194
+ let filter = [
195
+ {
196
+ 'range': {
197
+ 'dateString': {
198
+ 'gte': inputData.fromDate,
199
+ 'lte': inputData.toDate,
200
+ 'format': 'yyyy-MM-dd',
201
+ },
233
202
  },
234
203
  },
235
- };
204
+ {
205
+ terms: {
206
+ 'clientId.keyword': Array.isArray( inputData.clientId ) ?
207
+ inputData.clientId :
208
+ [ inputData.clientId ],
209
+ },
210
+ },
211
+ ];
236
212
 
237
- const geteDataCount = await getOpenSearchCount( openSearch.footfallDirectory, getCount );
238
- if ( !geteDataCount || geteDataCount?.body?.count == 0 ) {
239
- return res.sendError( 'No data found', 204 );
213
+ if ( inputData?.storeId ) {
214
+ filter.push(
215
+ {
216
+ terms: {
217
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
218
+ inputData.storeId :
219
+ [ inputData.storeId ],
220
+ },
221
+ },
222
+ );
240
223
  }
241
- const getQuery = {
242
- size: limit,
243
- from: skip,
244
- query: {
245
- bool: {
246
- filter: [
247
- { terms: { 'clientId.keyword': Array.isArray( inputData.clientId ) ?
248
- inputData.clientId :
249
- inputData.clientId } },
250
- {
251
- range: {
252
- dateString: {
253
- gte: inputData.fromDate,
254
- lte: inputData.toDate,
255
- format: 'yyyy-MM-dd',
256
- },
224
+
225
+ let search = {
226
+ 'must': filter,
227
+ };
228
+
229
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
230
+ search = {
231
+ 'must': filter,
232
+ 'should': [
233
+ {
234
+ 'wildcard': {
235
+ 'storeName.keyword': {
236
+ 'value': `*${inputData.searchValue}*`,
257
237
  },
258
238
  },
259
- ],
260
- },
239
+ },
240
+ {
241
+ 'wildcard': {
242
+ 'storeId.keyword': {
243
+ 'value': `*${inputData.searchValue}*`,
244
+ },
245
+ },
246
+ },
247
+ {
248
+ 'wildcard': {
249
+ 'ticketId.keyword': {
250
+ 'value': `*${inputData.searchValue}*`,
251
+ },
252
+ },
253
+ },
254
+ {
255
+ 'wildcard': {
256
+ 'status.keyword': {
257
+ 'value': `*${inputData.searchValue}*`,
258
+ },
259
+ },
260
+ },
261
+
262
+ ],
263
+ 'minimum_should_match': 1,
264
+ };
265
+ }
266
+
267
+ let searchQuery = {
268
+ '_source': [
269
+ 'storeName',
270
+ 'storeId',
271
+ 'ticketId',
272
+ 'createdAt',
273
+ 'updatedAt',
274
+ 'footfallCount',
275
+ 'duplicateCount',
276
+ 'employeeCount',
277
+ 'houseKeepingCount',
278
+ 'status',
279
+ 'dateString',
280
+ ],
281
+ 'from': offset,
282
+ 'size': limit,
283
+ 'query': {
284
+ 'bool': search,
261
285
  },
262
- _source: [ 'storeName', 'storeId', 'ticketId', 'createdAt', 'footfallCount', 'duplicateCount', 'employeeCount', 'houseKeepingCount', 'status', 'dateString' ],
286
+ 'sort': [
287
+ { dateString: { order: 'desc' } },
288
+ ],
263
289
  };
264
290
 
265
- const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
266
- const response = getData?.body?.hits?.hits;
267
- logger.info( { response: response, body: getData?.body, getData: getData, geteDataCount: geteDataCount } );
291
+ if ( inputData.sortBy && inputData.sortBy !== '' ) {
292
+ let sortByValue = '';
268
293
 
294
+ if ( [ 'storeName', 'storeId', 'ticketId', 'status' ].includes( inputData?.sortBy ) ) {
295
+ sortByValue = `${inputData.sortBy}.keyword`;
296
+ } else {
297
+ sortByValue = inputData.sortBy;
298
+ }
269
299
 
270
- if ( inputData.isExport=== true ) {
271
- const temp = [];
272
- for ( const item of response ) {
273
- temp.push( {
274
- 'Store Name': item.storeName,
275
- 'Store ID': item.storeId,
276
- 'Ticket ID': item.ticketId,
277
- 'Ticket raised on': dayjs( item.createdAt ).format( 'dd MMM, yyyy' ),
278
- 'Total Footfalls': item.footfallCount,
279
- 'Duplicates': item.duplicateCount,
280
- 'Employee/Staff': item.employeeCount,
281
- 'HouseKeeping': item.houseKeepingCount,
282
- 'Revised Footfalls': item.footfallCount-( item.duplicateCount+item.employeeCount+item.houseKeepingCount ),
283
- 'Status': item.status,
300
+
301
+ searchQuery = {
302
+ '_source': [
303
+ 'storeName',
304
+ 'storeId',
305
+ 'ticketId',
306
+ 'createdAt',
307
+ 'updatedAt',
308
+ 'footfallCount',
309
+ 'duplicateCount',
310
+ 'employeeCount',
311
+ 'houseKeepingCount',
312
+ 'status',
313
+ 'dateString',
314
+ ],
315
+ 'from': offset,
316
+ 'size': limit,
317
+ 'query': {
318
+ 'bool': search,
319
+ },
320
+ 'sort': [
321
+ { [sortByValue]: { order: order === -1 ? 'desc' : 'asc' } },
322
+ ],
323
+ };
324
+ }
325
+
326
+ if ( inputData.isExport == true ) {
327
+ searchQuery = {
328
+ '_source': [
329
+ 'storeName',
330
+ 'storeId',
331
+ 'ticketId',
332
+ 'createdAt',
333
+ 'updatedAt',
334
+ 'footfallCount',
335
+ 'duplicateCount',
336
+ 'employeeCount',
337
+ 'houseKeepingCount',
338
+ 'status',
339
+ 'dateString',
340
+ ],
341
+ 'from': 0,
342
+ 'size': 10000,
343
+ 'query': {
344
+ 'bool': search,
345
+ },
346
+ 'sort': [
347
+ { 'storeName.keyword': { order: 'desc' } },
348
+ ],
349
+ };
350
+ }
351
+ const getData = await getOpenSearchData( openSearch.footfallDirectory, searchQuery );
352
+ const count = getData?.body?.hits?.total?.value;
353
+ if ( !count || count == 0 ) {
354
+ return res.sendError( 'No data found', 204 );
355
+ }
356
+ const searchValue = getData?.body?.hits?.hits;
357
+ if ( !searchValue || searchValue?.length == 0 ) {
358
+ return res.sendError( 'No data found', 204 );
359
+ }
360
+
361
+ if ( inputData.isExport == true ) {
362
+ const exportData = [];
363
+ for ( const item of searchValue ) {
364
+ exportData.push( {
365
+ 'Store Name': item._source.storeName || '--',
366
+ 'Store ID': item._source.storeId,
367
+ 'Ticket ID': item._source.ticketId,
368
+ 'Ticket raised on': dayjs( item._source.createdAt ).format( 'DD MMM, YYYY' ),
369
+ 'Total Footfalls': item._source.footfallCount,
370
+ 'Duplicates': item._source.duplicateCount,
371
+ 'Employee/Staff': item._source.employeeCount,
372
+ 'HouseKeeping': item._source.houseKeepingCount,
373
+ 'Revised Footfalls': item._source.footfallCount - ( item._source.duplicateCount + item._source.employeeCount + item._source.houseKeepingCount ),
374
+ 'Status': item._source.status,
284
375
  } );
285
376
  }
286
- await download( temp, res );
287
- return;
377
+ return await download( exportData, res );
288
378
  }
289
-
290
- return res.sendSuccess( { result: response, count: geteDataCount?.body?.count } );
379
+ return res.sendSuccess( { result: searchValue, count: count } );
291
380
  } catch ( error ) {
292
381
  const err = error.message || 'Internal Server Error';
293
382
  logger.error( { error: error, messgage: req.query } );
@@ -299,65 +388,161 @@ export async function getTickets( req, res ) {
299
388
  try {
300
389
  const openSearch = JSON.parse( process.env.OPENSEARCH );
301
390
  const inputData = req.query;
302
- const limit = inputData.limit || 10;
303
- const skip= inputData.offset == 0? 0:( inputData.offset - 1 ) *limit || 0;
391
+ const limit = inputData.limit;
392
+ const skip = inputData.offset == 0 ? 0 : ( inputData.offset - 1 ) * limit || 0;
304
393
  inputData.storeId = inputData.storeId.split( ',' ); // convert strig to array
305
394
  logger.info( { inputData: inputData, limit: limit, skip: skip } );
306
- const getCount= {
307
- query: {
395
+ let source = [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'duplicateCount', 'comments', 'employee', 'houseKeeping', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus' ];
396
+ let filter = [
397
+ {
398
+ terms: {
399
+ 'storeId.keyword': Array.isArray( inputData.storeId ) ?
400
+ inputData.storeId :
401
+ inputData.storeId,
402
+ },
403
+ },
404
+ {
405
+ range: {
406
+ dateString: {
407
+ gte: inputData.fromDate,
408
+ lte: inputData.toDate,
409
+ format: 'yyyy-MM-dd',
410
+ },
411
+ },
412
+ },
413
+ ];
414
+ if ( inputData.status ) {
415
+ filter.push(
416
+ {
417
+ term: {
418
+ 'status.keyword': inputData.status,
419
+ },
420
+ },
421
+ );
422
+ }
423
+ if (
424
+ inputData.revopsType
425
+ ) {
426
+ filter.push( {
427
+ exists: {
428
+ field: inputData.revopsType,
429
+ },
430
+ } );
431
+ inputData.revopsType === 'employee' ?
432
+ filter.push( {
433
+ range: {
434
+ employeeCount: {
435
+ gt: 0,
436
+ },
437
+ },
438
+ } ) :
439
+ inputData.revopsType === 'housekeeping' ?
440
+ filter.push( {
441
+ range: {
442
+ housekeepingCount: {
443
+ gt: 0,
444
+ },
445
+ },
446
+ } ) :
447
+ filter.push( {
448
+ range: {
449
+ duplicateCount: {
450
+ gt: 0,
451
+ },
452
+ },
453
+ } );
454
+ source = inputData.revopsType == 'employee' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'employeeCount', 'comments', 'employee', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus' ] :
455
+ inputData.revopsType == 'houseKeeping' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'houseKeepingCount', 'comments', 'houseKeeping', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus' ] :
456
+ inputData.revopsType == 'duplicateImages' ? [ 'storeId', 'dateString', 'ticketName', 'footfallCount', 'duplicateCount', 'comments', 'duplicateImages', 'ticketId', 'clientId', 'storeName', 'createdAt', 'updatedAt', 'userName', 'role', 'status', 'employeeStatus', 'houseKeepingStatus', 'duplicateStatus' ] : [];
457
+ }
458
+
459
+ if ( inputData.action ) {
460
+ filter.push( {
308
461
  bool: {
309
- filter: [
310
- { terms: { 'storeId.keyword': Array.isArray( inputData.storeId ) ?
311
- inputData.storeId :
312
- inputData.storeId } },
462
+ should: [
313
463
  {
314
- range: {
315
- dateString: {
316
- gte: inputData.fromDate,
317
- lte: inputData.toDate,
318
- format: 'yyyy-MM-dd',
319
- },
464
+ term: {
465
+ 'houseKeepingStatus.keyword': inputData.action,
466
+ },
467
+ },
468
+ {
469
+ term: {
470
+ 'employeeStatus.keyword': inputData.action,
471
+ },
472
+ },
473
+ {
474
+ term: {
475
+ 'duplicateStatus.keyword': inputData.action,
320
476
  },
321
477
  },
322
478
  ],
479
+ minimum_should_match: 1, // Ensures at least one should condition must match
480
+ },
481
+ } );
482
+ }
483
+ let getRevCount = {};
484
+ if ( inputData.revopsType ) {
485
+ getRevCount = {
486
+ size: 0,
487
+ query: {
488
+ bool: {
489
+ filter: filter,
490
+ },
491
+ },
492
+ aggs: {
493
+ totalCount: {
494
+ sum: {
495
+ field: inputData.revopsType == 'employee' ? 'employeeCount' : inputData.revopsType == 'houseKeeping' ? 'houseKeepingCount' : 'duplicateCount',
496
+ },
497
+ },
498
+ },
499
+ };
500
+ }
501
+ const getCount = {
502
+ query: {
503
+ bool: {
504
+ filter: filter,
323
505
  },
324
506
  },
325
507
  };
326
508
 
509
+
327
510
  const geteDataCount = await getOpenSearchCount( openSearch.footfallDirectory, getCount );
328
- if ( !geteDataCount || geteDataCount?.body?.count == 0 ) {
511
+ const geteRevDataCount = inputData?.revopsType ? await getOpenSearchData( openSearch.footfallDirectory, getRevCount ) : null;
512
+ const revCount = inputData?.revopsType ? geteRevDataCount?.body?.aggregations?.totalCount?.value : 0;
513
+ const count = geteDataCount?.body?.count;
514
+ if ( !geteDataCount || count == 0 ) {
329
515
  return res.sendError( 'No data found', 204 );
330
516
  }
517
+
331
518
  const getQuery = {
332
519
  size: limit,
333
520
  from: skip,
334
521
  query: {
335
522
  bool: {
336
- filter: [
337
- { terms: { 'storeId.keyword': Array.isArray( inputData.storeId ) ?
338
- inputData.storeId :
339
- inputData.storeId } },
340
- {
341
- range: {
342
- dateString: {
343
- gte: inputData.fromDate,
344
- lte: inputData.toDate,
345
- format: 'yyyy-MM-dd',
346
- },
347
- },
348
- },
349
- ],
523
+ filter: filter,
350
524
  },
351
525
  },
352
- // _source: [ 'storeName', 'storeId', 'ticketId', 'createdAt', 'footfallCount', 'duplicateCount', 'employeeCount', 'houseKeepingCount', 'status', 'dateString' ],
526
+ _source: source,
353
527
  };
354
528
 
355
529
  const getData = await getOpenSearchData( openSearch.footfallDirectory, getQuery );
356
530
  const response = getData?.body?.hits?.hits;
531
+ // let temp=[ response[0] ];
532
+ // logger.info( { temp: temp, response: response } );
533
+ // if ( inputData.revopsType ) {
534
+ // const key =inputData.revopsType == 'employee'? 'employeeCount':inputData.revopsType == 'houseKeeping'?'houseKeepingCount': 'duplicateCount';
535
+
536
+ // const type = inputData.revopsType;
537
+ // temp[0]._source[key] = revCount;
538
+ // for ( const doc of response.slice( 1 ) ) {
539
+ // inputData.revopsType == 'duplicateImages'? temp[0]._source[type].push( ...doc._source[type] ):temp[0]._source[type].push( doc._source[type] );
540
+ // }
541
+ // }
357
542
  logger.info( { response: response, body: getData?.body, getData: getData, geteDataCount: geteDataCount } );
358
543
 
359
544
 
360
- if ( inputData.isExport=== true ) {
545
+ if ( inputData.isExport === true ) {
361
546
  const temp = [];
362
547
  for ( const item of response ) {
363
548
  temp.push( {
@@ -369,7 +554,7 @@ export async function getTickets( req, res ) {
369
554
  'Duplicates': item.duplicateCount,
370
555
  'Employee/Staff': item.employeeCount,
371
556
  'HouseKeeping': item.houseKeepingCount,
372
- 'Revised Footfalls': item.footfallCount-( item.duplicateCount+item.employeeCount+item.houseKeepingCount ),
557
+ 'Revised Footfalls': item.footfallCount - ( item.duplicateCount + item.employeeCount + item.houseKeepingCount ),
373
558
  'Status': item.status,
374
559
  } );
375
560
  }
@@ -377,7 +562,7 @@ export async function getTickets( req, res ) {
377
562
  return;
378
563
  }
379
564
 
380
- return res.sendSuccess( { result: response, count: geteDataCount?.body?.count } );
565
+ return res.sendSuccess( { result: inputData.revopsType ? response : response, count: count, revopCount: revCount } );
381
566
  } catch ( error ) {
382
567
  const err = error.message || 'Internal Server Error';
383
568
  logger.error( { error: error, messgage: req.query } );
@@ -387,45 +572,187 @@ export async function getTickets( req, res ) {
387
572
 
388
573
  export async function updateStatus( req, res ) {
389
574
  try {
575
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
390
576
  const inputData = req.body;
391
- const id = req.query._id;
392
- const splitId = id.split( '_' );
393
- const storeId = splitId[0];
394
- const dateString = splitId[1];
395
- if ( inputData.duplicateStatus === 'approved' && inputData.employeeStatus === 'approved' && inputData.houseKeepingStatus === 'approved' ) {
396
- inputData.status = 'closed';
577
+ let temp = [];
578
+ for ( let data of inputData?.data ) {
579
+ await updateTicketStatus( data, openSearch, temp );
397
580
  }
398
- await updateOpenSearchData( openSearch.footfallDirectory, id, { _doc: inputData } );
399
- let bulkBody = [];
400
- for ( let duplicate of inputData.duplicateImages ) {
401
- bulkBody.push(
402
- { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${duplicate.timeRange}_${duplicate.tempId}` } },
403
- { doc: { duplicateImage: duplicate.duplicateImage } },
404
- );
405
- duplicate.map( ( item ) => {
581
+
582
+ if ( temp.length == inputData?.data?.length ) {
583
+ return res.sendSuccess( 'Ticket has been updated successfully' );
584
+ }
585
+ } catch ( error ) {
586
+ const err = error.messgae || 'Internal Server Error';
587
+ logger.error( { error: error, data: req.body, function: 'infra-footfallDirectory-updateStatus' } );
588
+ return res.sendError( err, 500 );
589
+ }
590
+ }
591
+ export async function updateTicketStatus( data, openSearch, temp ) {
592
+ const id = data._id;
593
+ const splitId = id.split( '_' );
594
+ const storeId = splitId[0];
595
+ const dateString = splitId[1];
596
+ if ( data.duplicateStatus !== 'pending' && data.employeeStatus !== 'pending' && data.houseKeepingStatus !== 'pending' ) {
597
+ data.status = 'closed';
598
+ }
599
+ let tempId = [];
600
+ const { _id, ...updateData } = data;
601
+ await updateOpenSearchData( openSearch.footfallDirectory, _id, { doc: updateData } );
602
+ let bulkBody = [];
603
+ for ( let duplicate of data.duplicateImages ) {
604
+ bulkBody.push(
605
+ { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${duplicate.timeRange}_${duplicate.tempId}` } },
606
+ { doc: { duplicateImage: duplicate.duplicateImage } },
607
+ );
608
+ duplicate?.data?.map( ( item ) => {
609
+ item.isChecked == true ? tempId.push( item.tempId ) : null;
406
610
  bulkBody.push(
407
611
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${item.timeRange}_${item.tempId}` } },
408
612
  { doc: { isChecked: item.isChecked } },
409
613
  );
410
- } );
411
- }
412
- for ( let employee of inputData.employee ) {
614
+ } );
615
+ }
616
+ for ( let employee of data.employee ) {
617
+ employee.isChecked == true ? tempId.push( employee.tempId ) : null;
413
618
  bulkBody.push(
414
619
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${employee.timeRange}_${employee.tempId}` } },
415
620
  { doc: { isChecked: employee.isChecked } },
416
621
  );
417
- }
418
- for ( let houseKeeping of inputData.houseKeeping ) {
622
+ }
623
+ for ( let houseKeeping of data.houseKeeping ) {
624
+ houseKeeping.isChecked == true ? tempId.push( houseKeeping.tempId ) : null;
419
625
  bulkBody.push(
420
626
  { update: { _index: openSearch.revop, _id: `${storeId}_${dateString}_${houseKeeping.timeRange}_${houseKeeping.tempId}` } },
421
627
  { doc: { isChecked: houseKeeping.isChecked } },
422
628
  );
629
+ }
630
+ temp.push( storeId );
631
+ // if ( inputData.status == 'closed' ) {
632
+ // const isSendMessge = await sendSqsMessage( inputData, tempId );
633
+ // if ( isSendMessge ==true ) {
634
+ // return res.sendSuccess( 'Ticket has been updated successfully' );
635
+ // } else {
636
+ // return res.sendError( 'No SQS message sent', 500 );
637
+ // }
638
+ // }
639
+ // if ( data.status == 'closed' ) {
640
+ // await sendSqsMessage( data, tempId );
641
+ // }
642
+ }
643
+
644
+ export async function sendSqsMessage( inputData, tempId ) {
645
+ const sqs = JSON.parse( process.env.sqs );
646
+ const sqsProduceQueue = {
647
+ QueueUrl: `${sqs.url}${sqs.revopticket}`,
648
+ MessageBody: JSON.stringify( {
649
+ store_id: inputData.storeId,
650
+ store_date: inputData.dateString.split( '-' ).reverse().join( '-' ),
651
+ bucket_name: '',
652
+ zone_id: 'traffic_zone',
653
+ process_type: 'reduction',
654
+ revop_type: 'footfall',
655
+ temp_id: tempId,
656
+ } ),
657
+ };
658
+
659
+ const sqsQueue = await sendMessageToQueue(
660
+ sqsProduceQueue.QueueUrl,
661
+ sqsProduceQueue.MessageBody,
662
+ );
663
+
664
+ if ( sqsQueue.statusCode ) {
665
+ logger.error( {
666
+ error: `${sqsQueue}`,
667
+ type: 'UPLOAD_ERROR',
668
+ } );
669
+ return res.sendError( 'SQS not sent', 500 );
670
+ }
671
+ }
672
+
673
+ export async function getTaggedStores( req, res ) {
674
+ try {
675
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
676
+ const inputData = req.query;
677
+ logger.info( { inputData: inputData } );
678
+ let filter = [
679
+ {
680
+ term: { 'clientId.keyword': inputData.clientId },
681
+ },
682
+ {
683
+ range: {
684
+ dateString: {
685
+ gte: inputData.fromDate,
686
+ lte: inputData.toDate,
687
+ format: 'yyyy-MM-dd',
688
+ },
689
+ },
690
+ },
691
+
692
+ {
693
+ exists: {
694
+ field: 'storeId',
695
+ },
696
+ },
697
+ {
698
+ exists: {
699
+ field: 'storeName',
700
+ },
701
+ },
702
+ ];
703
+
704
+ if ( inputData.searchValue && inputData.searchValue !== '' ) {
705
+ filter.push( {
706
+ regexp: {
707
+ 'storeName': {
708
+ value: `.*${inputData.searchValue.toLowerCase()}.*`,
709
+ flags: 'ALL',
710
+ },
711
+ },
712
+ } );
423
713
  }
424
- return res.sendSuccess( 'ticket closed successfully' );
714
+ const getCount = {
715
+ size: 0,
716
+ query: {
717
+ bool: {
718
+ filter: filter,
719
+ },
720
+ },
721
+ aggs: {
722
+ unique_stores: {
723
+ composite: {
724
+ size: 1000,
725
+ sources: [
726
+ {
727
+ storeId: {
728
+ terms: { field: 'storeId.keyword' },
729
+ },
730
+ },
731
+ {
732
+ storeName: {
733
+ terms: { field: 'storeName.keyword' },
734
+ },
735
+ },
736
+ ],
737
+ },
738
+ },
739
+ },
740
+ };
741
+ let temp = [];
742
+
743
+ const geteDataCount = await getOpenSearchData( openSearch.footfallDirectory, getCount );
744
+ if ( !geteDataCount && geteDataCount?.body?.aggregations?.unique_stores?.buckets?.length > 0 ) {
745
+ return res.sendError( 'No data found', 204 );
746
+ }
747
+
748
+ const response = geteDataCount?.body?.aggregations?.unique_stores?.buckets;
749
+ logger.info( { response: response } );
750
+ response?.map( ( item ) => temp.push( { storeId: item.key.storeId, storeName: item.key.storeName } ) );
751
+ return res.sendSuccess( { result: temp, count: response?.length } );
425
752
  } catch ( error ) {
426
- const err = error.messgae || 'Internal Server Error';
427
- logger.error( { error: error, data: req.body, function: 'infra-footfallDirectory-updateStatus' } );
428
- return res.sendError( err, 500 );
753
+ const err = error.message || 'Internal Server Error';
754
+ logger.error( { error: error, messgage: req.query } );
755
+ return res.sendSuccess( err, 500 );
429
756
  }
430
757
  }
431
758
 
@@ -1,5 +1,5 @@
1
1
  import j2s from 'joi-to-swagger';
2
- import { createTicketSchema, getTicketsSchema, ticketListSchema, ticketSummarySchema, updateStatusQuerySchema, updateStatusSchemea } from '../dtos/footfallDirectory.dtos.js';
2
+ import { createTicketSchema, getTaggedStoresSchema, getTicketsSchema, ticketListSchema, ticketSummarySchema, updateStatusSchemea } from '../dtos/footfallDirectory.dtos.js';
3
3
 
4
4
  export const footfallDirectoryDocs = {
5
5
 
@@ -59,7 +59,6 @@ export const footfallDirectoryDocs = {
59
59
  },
60
60
  },
61
61
  },
62
-
63
62
  '/v3/footfall-directory-tagging/ticket-list': {
64
63
  get: {
65
64
  tags: [ 'Footfall Directory Ticket' ],
@@ -72,6 +71,12 @@ export const footfallDirectoryDocs = {
72
71
  scema: j2s( ticketListSchema ).swagger,
73
72
  required: true,
74
73
  },
74
+ {
75
+ in: 'query',
76
+ name: 'storeId',
77
+ scema: j2s( ticketListSchema ).swagger,
78
+ required: false,
79
+ },
75
80
  {
76
81
  in: 'query',
77
82
  name: 'fromDate',
@@ -86,7 +91,7 @@ export const footfallDirectoryDocs = {
86
91
  },
87
92
  {
88
93
  in: 'query',
89
- name: 'searchvalue',
94
+ name: 'searchValue',
90
95
  scema: j2s( ticketListSchema ).swagger,
91
96
  required: false,
92
97
  },
@@ -108,6 +113,12 @@ export const footfallDirectoryDocs = {
108
113
  scema: j2s( ticketListSchema ).swagger,
109
114
  required: false,
110
115
  },
116
+ {
117
+ in: 'query',
118
+ name: 'sortBy',
119
+ scema: j2s( ticketListSchema ).swagger,
120
+ required: false,
121
+ },
111
122
  ],
112
123
  responses: {
113
124
  200: { description: 'Successful' },
@@ -143,6 +154,36 @@ export const footfallDirectoryDocs = {
143
154
  scema: j2s( getTicketsSchema ).swagger,
144
155
  required: true,
145
156
  },
157
+ {
158
+ in: 'query',
159
+ name: 'revopsType',
160
+ scema: j2s( getTicketsSchema ).swagger,
161
+ required: false,
162
+ },
163
+ {
164
+ in: 'query',
165
+ name: 'status',
166
+ scema: j2s( getTicketsSchema ).swagger,
167
+ required: false,
168
+ },
169
+ {
170
+ in: 'query',
171
+ name: 'action',
172
+ scema: j2s( getTicketsSchema ).swagger,
173
+ required: false,
174
+ },
175
+ {
176
+ in: 'query',
177
+ name: 'limit',
178
+ scema: j2s( getTicketsSchema ).swagger,
179
+ required: true,
180
+ },
181
+ {
182
+ in: 'query',
183
+ name: 'offset',
184
+ scema: j2s( getTicketsSchema ).swagger,
185
+ required: false,
186
+ },
146
187
  ],
147
188
  responses: {
148
189
  200: { description: 'Successful' },
@@ -159,14 +200,7 @@ export const footfallDirectoryDocs = {
159
200
  tags: [ 'Footfall Directory Ticket' ],
160
201
  description: 'approve or reject the ticket status',
161
202
  operationId: 'update-status',
162
- parameters: [
163
- {
164
- in: 'query',
165
- name: 'storeId',
166
- scema: j2s( updateStatusQuerySchema ).swagger,
167
- required: true,
168
- },
169
- ],
203
+ parameters: [],
170
204
  requestBody: {
171
205
  content: {
172
206
  'application/json': {
@@ -183,4 +217,46 @@ export const footfallDirectoryDocs = {
183
217
  },
184
218
  },
185
219
  },
220
+
221
+ '/v3/footfall-directory-tagging/get-tagged-stores': {
222
+ get: {
223
+ tags: [ 'Footfall Directory Ticket' ],
224
+ description: 'To get list of tagged stores',
225
+ operationId: 'get-tagged-stores',
226
+ parameters: [
227
+ {
228
+ in: 'query',
229
+ name: 'clientId',
230
+ scema: j2s( getTaggedStoresSchema ).swagger,
231
+ required: true,
232
+ },
233
+ {
234
+ in: 'query',
235
+ name: 'fromDate',
236
+ scema: j2s( getTaggedStoresSchema ).swagger,
237
+ required: true,
238
+ },
239
+ {
240
+ in: 'query',
241
+ name: 'toDate',
242
+ scema: j2s( getTaggedStoresSchema ).swagger,
243
+ required: true,
244
+ },
245
+ {
246
+ in: 'query',
247
+ name: 'searchValue',
248
+ scema: j2s( getTaggedStoresSchema ).swagger,
249
+ required: false,
250
+ },
251
+ ],
252
+ responses: {
253
+ 200: { description: 'Successful' },
254
+ 401: { description: 'Unauthorized User' },
255
+ 422: { description: 'Field Error' },
256
+ 500: { description: 'Server Error' },
257
+ 204: { description: 'Not Found' },
258
+ },
259
+ },
260
+ },
261
+
186
262
  };
@@ -121,10 +121,12 @@ export const ticketSummaryValid = {
121
121
 
122
122
  export const ticketListSchema = Joi.object().keys( {
123
123
  clientId: Joi.string().required(),
124
+ storeId: Joi.string().optional(),
124
125
  searchValue: Joi.string().optional().allow( '' ),
125
126
  limit: Joi.number().optional(),
126
127
  offset: Joi.number().optional(),
127
128
  isExport: Joi.boolean().optional(),
129
+ sortBy: Joi.string().optional().allow( '' ),
128
130
  fromDate: Joi.string()
129
131
  .pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
130
132
  .required()
@@ -185,7 +187,6 @@ export const ticketListValid = {
185
187
 
186
188
  export const getTicketsSchema = Joi.object().keys( {
187
189
  storeId: Joi.string().required(),
188
-
189
190
  fromDate: Joi.string()
190
191
  .pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
191
192
  .required()
@@ -221,6 +222,12 @@ export const getTicketsSchema = Joi.object().keys( {
221
222
 
222
223
  return value;
223
224
  } ),
225
+ status: Joi.string().optional(),
226
+ action: Joi.string().optional(),
227
+ revopsType: Joi.string().optional(),
228
+ limit: Joi.number().required(),
229
+ offset: Joi.number().optional(),
230
+
224
231
  } ).custom( ( value, helpers ) => {
225
232
  const from = dayjs( value.fromDate );
226
233
  const to = dayjs( value.toDate );
@@ -244,26 +251,31 @@ export const getTicketsValid = {
244
251
  query: getTicketsSchema,
245
252
  };
246
253
 
247
- export const updateStatusQuerySchema =Joi.object().keys( {
248
-
249
- _id: Joi.string().required(),
250
- } );
251
-
252
254
  export const updateStatusSchemea =Joi.object().keys( {
253
-
254
- duplicateACCount: Joi.number().required(),
255
- employeeACCount: Joi.number().required(),
256
- houseKeepingACCount: Joi.number().required(),
257
- comments: Joi.string().optional().allow( '' ),
258
- duplicateImages: Joi.array().items(
255
+ data: Joi.array().items(
259
256
  Joi.object( {
260
- tempId: Joi.number().required(),
261
- filePath: Joi.string().required(),
262
- entryTime: Joi.string().required(),
263
- exitTime: Joi.string().required(),
264
- timeRange: Joi.string().required(),
265
- isChecked: Joi.boolean().required(),
266
- data: Joi.array().items(
257
+ _id: Joi.string().required(),
258
+ dateString: Joi.string().required().custom( ( value, helpers ) => {
259
+ const inputDate = dayjs( value, 'YYYY-MM-DD', true );
260
+ const today = dayjs();
261
+
262
+ if ( !inputDate.isValid() ) {
263
+ return helpers.error( 'any.invalid' );
264
+ }
265
+
266
+ const diff = today.diff( inputDate, 'day' );
267
+
268
+ if ( diff > 10 ) {
269
+ return helpers.message( 'Approval is not allowed for a period exceeding 10 days' );
270
+ }
271
+
272
+ return value;
273
+ } ),
274
+ duplicateACCount: Joi.number().optional(),
275
+ employeeACCount: Joi.number().optional(),
276
+ houseKeepingACCount: Joi.number().optional(),
277
+ comments: Joi.string().optional().allow( '' ),
278
+ duplicateImages: Joi.array().items(
267
279
  Joi.object( {
268
280
  tempId: Joi.number().required(),
269
281
  filePath: Joi.string().required(),
@@ -271,33 +283,104 @@ export const updateStatusSchemea =Joi.object().keys( {
271
283
  exitTime: Joi.string().required(),
272
284
  timeRange: Joi.string().required(),
273
285
  isChecked: Joi.boolean().required(),
274
- } ),
275
- ).optional(),
276
- } ) ).optional(),
277
-
278
- houseKeeping: Joi.array().items( Joi.object( {
279
- tempId: Joi.number().required(),
280
- filePath: Joi.string().required(),
281
- entryTime: Joi.string().required(),
282
- exitTime: Joi.string().required(),
283
- timeRange: Joi.string().required(),
284
- isChecked: Joi.boolean().required(),
285
- } ) ).optional(),
286
- employee: Joi.array().items( Joi.object( {
287
- tempId: Joi.number().required(),
288
- filePath: Joi.string().required(),
289
- entryTime: Joi.string().required(),
290
- exitTime: Joi.string().required(),
291
- timeRange: Joi.string().required(),
292
- isChecked: Joi.boolean().required(),
286
+ data: Joi.array().items(
287
+ Joi.object( {
288
+ tempId: Joi.number().required(),
289
+ filePath: Joi.string().required(),
290
+ entryTime: Joi.string().required(),
291
+ exitTime: Joi.string().required(),
292
+ timeRange: Joi.string().required(),
293
+ isChecked: Joi.boolean().required(),
294
+ } ),
295
+ ).optional(),
296
+ } ) ).optional(),
297
+ houseKeeping: Joi.array().items( Joi.object( {
298
+ tempId: Joi.number().required(),
299
+ filePath: Joi.string().required(),
300
+ entryTime: Joi.string().required(),
301
+ exitTime: Joi.string().required(),
302
+ timeRange: Joi.string().required(),
303
+ isChecked: Joi.boolean().required(),
304
+
305
+ } ) ).optional(),
306
+ employee: Joi.array().items( Joi.object( {
307
+ tempId: Joi.number().required(),
308
+ filePath: Joi.string().required(),
309
+ entryTime: Joi.string().required(),
310
+ exitTime: Joi.string().required(),
311
+ timeRange: Joi.string().required(),
312
+ isChecked: Joi.boolean().required(),
313
+
314
+ } ) ).optional(),
315
+ duplicateStatus: Joi.string().required(),
316
+ employeeStatus: Joi.string().required(),
317
+ houseKeepingStatus: Joi.string().required(),
318
+ } ) ).required(),
293
319
 
294
- } ) ).optional(),
295
- duplicateStatus: Joi.string().required(),
296
- employeeStatus: Joi.string().required(),
297
- houseKeepingStatus: Joi.string().required(),
298
320
  } );
299
321
 
300
322
  export const updateStatusValid = {
301
- query: updateStatusQuerySchema,
302
323
  body: updateStatusSchemea,
303
324
  };
325
+
326
+
327
+ export const getTaggedStoresSchema = Joi.object().keys( {
328
+ clientId: Joi.string().required(),
329
+ searchValue: Joi.string().optional().allow( '' ),
330
+ fromDate: Joi.string()
331
+ .pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
332
+ .required()
333
+ .messages( {
334
+ 'string.pattern.name': `'fromDate' must be in the format YYYY-MM-DD (e.g., 2025-07-19).`,
335
+ 'string.empty': `'fromDate' is required.`,
336
+ } )
337
+ .custom( ( value, helpers ) => {
338
+ const from = dayjs( value );
339
+ if ( !from.isValid() ) {
340
+ return helpers.error( 'any.invalid', { message: 'Invalid fromDate' } );
341
+ }
342
+ return value;
343
+ } ),
344
+
345
+ toDate: Joi.string()
346
+ .pattern( /^\d{4}-\d{2}-\d{2}$/, 'YYYY-MM-DD format' )
347
+ .required()
348
+ .messages( {
349
+ 'string.pattern.name': `'toDate' must be in the format YYYY-MM-DD (e.g., 2025-07-19).`,
350
+ 'string.empty': `'toDate' is required.`,
351
+ } )
352
+ .custom( ( value, helpers ) => {
353
+ const to = dayjs( value );
354
+ const today = dayjs();
355
+
356
+ if ( !to.isValid() ) {
357
+ return helpers.error( 'any.invalid', { message: 'Invalid toDate' } );
358
+ }
359
+ if ( to.isAfter( today, 'day' ) ) {
360
+ return helpers.error( 'any.invalid', { message: 'toDate cannot be in the future' } );
361
+ }
362
+
363
+ return value;
364
+ } ),
365
+ } ).custom( ( value, helpers ) => {
366
+ const from = dayjs( value.fromDate );
367
+ const to = dayjs( value.toDate );
368
+
369
+ if ( !from.isValid() || !to.isValid() ) {
370
+ return helpers.error( 'any.invalid', { message: 'Invalid dates' } );
371
+ }
372
+
373
+ if ( from.isAfter( to ) ) {
374
+ return helpers.error( 'any.invalid', { message: 'fromDate cannot be after toDate' } );
375
+ }
376
+
377
+ if ( to.diff( from, 'day' ) > 90 ) {
378
+ return helpers.error( 'any.invalid', { message: 'Date range cannot exceed 90 days' } );
379
+ }
380
+
381
+ return value;
382
+ } );
383
+
384
+ export const getTaggedStoresValid = {
385
+ query: getTaggedStoresSchema,
386
+ };
@@ -1,7 +1,7 @@
1
1
  import express from 'express';
2
2
  import { isExist } from '../validations/footfallDirectory.validation.js';
3
- import { createTicket, getTickets, ticketList, ticketSummary, updateStatus } from '../controllers/footfallDirectory.controllers.js';
4
- import { createTicketValid, getTicketsValid, ticketListValid, ticketSummaryValid, updateStatusValid } from '../dtos/footfallDirectory.dtos.js';
3
+ import { createTicket, getTaggedStores, getTickets, ticketList, ticketSummary, updateStatus } from '../controllers/footfallDirectory.controllers.js';
4
+ import { createTicketValid, getTaggedStoresValid, getTicketsValid, ticketListValid, ticketSummaryValid, updateStatusValid } from '../dtos/footfallDirectory.dtos.js';
5
5
  import { bulkValidate, isAllowedSessionHandler, validate } from 'tango-app-api-middleware';
6
6
 
7
7
  export const footfallDirectoryRouter = express.Router();
@@ -11,6 +11,7 @@ footfallDirectoryRouter.get( '/ticket-summary', isAllowedSessionHandler, bulkVal
11
11
 
12
12
  footfallDirectoryRouter.get( '/ticket-list', isAllowedSessionHandler, bulkValidate( ticketListValid ), ticketList );
13
13
  footfallDirectoryRouter.get( '/get-tickets', isAllowedSessionHandler, bulkValidate( getTicketsValid ), getTickets );
14
+ footfallDirectoryRouter.get( '/get-tagged-stores', bulkValidate( getTaggedStoresValid ), getTaggedStores );
14
15
 
15
16
  footfallDirectoryRouter.put( '/update-status', bulkValidate( updateStatusValid ), updateStatus );
16
17