tango-app-api-analysis-traffic 3.8.7-vms.3 → 3.8.7-vms.31

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,7 +1,7 @@
1
1
  import { logger, insertOpenSearchData, getOpenSearchData, updateOpenSearchData } from 'tango-app-api-middleware';
2
2
  import { findOnerevopConfig } from '../services/revopConfig.service.js';
3
3
  import * as clientService from '../services/clients.services.js';
4
- import { bulkUpdate, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
4
+ import { bulkUpdate, insertWithId, searchOpenSearchData, scrollResponse, upsertWithScript } from 'tango-app-api-middleware/src/utils/openSearch.js';
5
5
  import { findOneVmsStoreRequest } from '../services/vmsStoreRequest.service.js';
6
6
  // import dayjs from 'dayjs';
7
7
  // Lamda Service Call //
@@ -137,6 +137,217 @@ export async function getrevoptagging( req, res ) {
137
137
  return res.sendError( { error: error }, 500 );
138
138
  }
139
139
  }
140
+
141
+ export async function migrateRevopIndex( req, res ) {
142
+ try {
143
+ const { storeId, dateString, size = 500 } = req.body;
144
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
145
+
146
+ const query = {
147
+ size: size,
148
+ query: {
149
+ bool: {
150
+ must: [],
151
+ },
152
+ },
153
+ };
154
+
155
+ if ( storeId ) {
156
+ query.query.bool.must.push( {
157
+ term: {
158
+ 'storeId.keyword': storeId,
159
+ },
160
+ } );
161
+ }
162
+
163
+ if ( dateString ) {
164
+ query.query.bool.must.push( {
165
+ terms: {
166
+ dateString: Array.isArray( dateString ) ? dateString : `${dateString}`.split( ',' ),
167
+ },
168
+ } );
169
+ }
170
+
171
+ const response = await getOpenSearchData( openSearch.revop, query );
172
+ const hits = response?.body?.hits?.hits || [];
173
+
174
+ if ( hits.length === 0 ) {
175
+ return res.sendSuccess( { message: 'No records found for migration', updated: 0 } );
176
+ }
177
+
178
+ const bulkBody = [];
179
+
180
+ for ( const hit of hits ) {
181
+ const src = hit._source || {};
182
+ const statusValue = ( src.status || '' ).toLowerCase();
183
+ const parentValue = src.parent;
184
+ const idValue = `${src.storeId || ''}_${src.dateString || src.dteString || ''}_${src.tempId || ''}`;
185
+
186
+ let actions = [
187
+ {
188
+ actionType: 'tagging',
189
+ action: 'submitted',
190
+ },
191
+ ];
192
+
193
+ if ( statusValue === 'approved' ) {
194
+ actions = [
195
+ {
196
+ actionType: 'tagging',
197
+ action: 'submitted',
198
+ },
199
+ {
200
+ actionType: 'review',
201
+ action: 'approved',
202
+ },
203
+ ];
204
+ } else if ( statusValue === 'rejected' ) {
205
+ actions = [
206
+ {
207
+ actionType: 'tagging',
208
+ action: 'submitted',
209
+ },
210
+ {
211
+ actionType: 'review',
212
+ action: 'rejected',
213
+ },
214
+ ];
215
+ }
216
+
217
+ const doc = {
218
+ id: idValue,
219
+ isParent: parentValue === null || parentValue === undefined ? false : true,
220
+ actions,
221
+ ticketStatus: src.status,
222
+ // updatedAt: new Date(),
223
+ };
224
+
225
+ bulkBody.push(
226
+ { update: { _index: openSearch.newRevop, _id: hit._id } },
227
+ { doc: doc, doc_as_upsert: true },
228
+ );
229
+ }
230
+
231
+ const bulkRes = await bulkUpdate( bulkBody );
232
+ if ( bulkRes?.errors ) {
233
+ logger.error( 'Bulk migration errors:', bulkRes.items );
234
+ return res.sendError( 'Failed to migrate some records', 500 );
235
+ }
236
+
237
+ return res.sendSuccess( { message: 'Migration completed', updated: hits.length } );
238
+ } catch ( error ) {
239
+ logger.error( { error: error, message: req.body, function: 'migrateRevopIndex' } );
240
+ return res.sendError( { error: error }, 500 );
241
+ }
242
+ }
243
+
244
+ export async function expireReviewStatus( req, res ) {
245
+ try {
246
+ const {
247
+ thresholdDate = '2025-12-20',
248
+ batchSize = 500,
249
+ storeId,
250
+ dateString,
251
+ } = req.body;
252
+ logger.info( { inputData: req.body, msg: '........1' } );
253
+ const cutoffDate = new Date( thresholdDate );
254
+ if ( Number.isNaN( cutoffDate.getTime() ) ) {
255
+ return res.sendError( 'Invalid thresholdDate', 400 );
256
+ }
257
+
258
+
259
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
260
+ const query = {
261
+ size: batchSize,
262
+ query: {
263
+ bool: {
264
+ must: [
265
+ { term: { 'ticketName.keyword': 'footfall-directory' } },
266
+ { term: { 'type.keyword': 'store' } },
267
+ ],
268
+ must_not: [
269
+ { terms: { 'status.keyword': [ 'Closed' ] } },
270
+ ],
271
+ },
272
+ },
273
+ };
274
+
275
+
276
+ if ( storeId ) {
277
+ query.query.bool.must.push( { term: { 'storeId.keyword': storeId } } );
278
+ }
279
+
280
+
281
+ if ( dateString ) {
282
+ query.query.bool.must.push( {
283
+ terms: {
284
+ dateString: Array.isArray( dateString ) ? dateString : `${dateString}`.split( ',' ),
285
+ },
286
+ } );
287
+ }
288
+
289
+
290
+ let totalUpdated = 0;
291
+ let scrollId = null;
292
+
293
+ let firstResponse = await searchOpenSearchData( openSearch.footfallDirectory, query );
294
+ let hitsBatch = firstResponse?.body?.hits?.hits || [];
295
+ logger.info( { hitsBatch } );
296
+ scrollId = firstResponse?.body?._scroll_id;
297
+
298
+ while ( hitsBatch.length > 0 ) {
299
+ const bulkBody = [];
300
+
301
+ for ( const hit of hitsBatch ) {
302
+ const src = hit._source || {};
303
+ const mappingInfo = Array.isArray( src.mappingInfo ) ? src.mappingInfo : [];
304
+ let changed = false;
305
+ const updatedMapping = mappingInfo.map( ( item ) => {
306
+ if ( item?.type === 'review'&& item?.type !== 'Closed' && item?.dueDate ) {
307
+ const due = new Date( item.dueDate );
308
+ logger.info( { due, msg: '..........1', cutoffDate } );
309
+ if ( !Number.isNaN( due.getTime() ) && due < cutoffDate ) {
310
+ changed = true;
311
+ return { ...item, status: 'Expired' };
312
+ }
313
+ }
314
+ return item;
315
+ } );
316
+
317
+
318
+ if ( changed ) {
319
+ const doc = {
320
+ mappingInfo: updatedMapping,
321
+ status: 'Expired',
322
+ };
323
+
324
+ bulkBody.push(
325
+ { update: { _index: openSearch.footfallDirectory, _id: hit._id } },
326
+ { doc: doc, doc_as_upsert: true },
327
+ );
328
+ }
329
+ }
330
+
331
+ if ( bulkBody.length > 0 ) {
332
+ const bulkRes = await bulkUpdate( bulkBody );
333
+ if ( bulkRes?.errors ) {
334
+ logger.error( { message: 'Bulk expire errors', items: bulkRes.items } );
335
+ }
336
+ totalUpdated += bulkBody.length / 2;
337
+ }
338
+ logger.info( { totalUpdated, msg: '........9' } );
339
+ if ( !scrollId ) break;
340
+ const nextRes = await scrollResponse( scrollId );
341
+ hitsBatch = nextRes?.body?.hits?.hits || [];
342
+ scrollId = nextRes?.body?._scroll_id;
343
+ }
344
+
345
+ return res.sendSuccess( { message: 'Expired review status updated', updated: totalUpdated } );
346
+ } catch ( error ) {
347
+ logger.error( { error: error, message: req.body, function: 'expireReviewStatus' } );
348
+ return res.sendError( { error: error }, 500 );
349
+ }
350
+ }
140
351
  export async function revoptaggingcount( req, res ) {
141
352
  try {
142
353
  const openSearch = JSON.parse( process.env.OPENSEARCH );
@@ -221,6 +432,9 @@ export async function storeProcessedData( req, res ) {
221
432
  dayjs.extend( isSameOrBefore );
222
433
 
223
434
  let start = dayjs( fromDate );
435
+ // get start value from the before day one
436
+ // Move start one day back so we can access "day before" when looping
437
+ start = start.subtract( 1, 'day' );
224
438
  let end = dayjs( toDate );
225
439
 
226
440
  if ( !start.isValid() || !end.isValid() ) {
@@ -267,10 +481,12 @@ export async function storeProcessedData( req, res ) {
267
481
 
268
482
  const responseArray = [];
269
483
 
270
- for ( let i = 0; i < orderedDates.length; i++ ) {
484
+ for ( let i = 1; i < orderedDates.length; i++ ) {
271
485
  const currentDate = orderedDates[i];
272
486
  const currentId = `${storeId}_${currentDate}`;
487
+ logger.info( { currentId, currentDate } );
273
488
  const processedData = hitsMap.get( currentId );
489
+ logger.info( { processedData } );
274
490
  if ( !processedData ) {
275
491
  responseArray.push( {
276
492
  date: currentDate,
@@ -283,13 +499,17 @@ export async function storeProcessedData( req, res ) {
283
499
 
284
500
  const prevDate = dayjs( currentDate ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
285
501
  const prevId = `${storeId}_${prevDate}`;
502
+ logger.info( { prevId, prevDate } );
286
503
  const previousData = hitsMap.get( prevId );
287
504
 
288
505
  let footfallCountTrend = 0;
506
+ logger.info( { previousData, previoucubr1: previousData?.footfall_count } );
289
507
  if ( previousData && previousData.footfall_count ) {
508
+ logger.info( { previousData, previoucubr: previousData?.footfall_count } );
290
509
  footfallCountTrend = Math.round(
291
- ( ( processedData.footfall_count - previousData.footfall_count ) / previousData.footfall_count ) * 100,
510
+ ( ( processedData.footfall_count - previousData?.footfall_count ) / previousData.footfall_count ) * 100,
292
511
  );
512
+ logger.info( { footfallCountTrend } );
293
513
  }
294
514
  // Add ticket status from openSearch.footfallDirectory (_source.status)
295
515
  let ticketStatus = null;
@@ -302,6 +522,11 @@ export async function storeProcessedData( req, res ) {
302
522
  { term: { 'storeId.keyword': storeId } },
303
523
  { term: { 'dateString': currentDate } },
304
524
  { term: { 'ticketName.keyword': 'footfall-directory' } },
525
+ {
526
+ 'term': {
527
+ 'type.keyword': 'store',
528
+ },
529
+ },
305
530
  ],
306
531
  },
307
532
  },
@@ -403,43 +628,177 @@ export async function footFallImages( req, res ) {
403
628
  'ticketName.keyword': 'footfall-directory',
404
629
  },
405
630
  },
631
+ {
632
+ 'term': {
633
+ 'type.keyword': 'store',
634
+ },
635
+ },
406
636
  ],
407
637
  },
408
638
  },
409
- // '_source': [ 'dateString', 'storeId', 'duplicateCount', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'junkCount', 'status', 'ticketId', 'comments', 'userName', 'role', 'createdAt', 'email', 'houseKeepingACCount', 'duplicateACCount', 'employeeACCount', 'junkACCount', 'approverEmail', 'approverRole', 'approverUserName' ],
639
+ '_source': [ 'dateString', 'storeId', 'mappingInfo', 'revicedFootfall', 'revicedPerc', 'reviced', 'createdAt', 'updatedAt', 'footfallCount' ],
410
640
 
411
641
  };
412
642
 
413
643
  const getData = await getOpenSearchData( opensearch.footfallDirectory, query );
414
644
  const ticketDetails = getData?.body?.hits?.hits[0];
415
645
  let temp = [];
416
- ticketDetails?._source? temp.push( ticketDetails?._source ) :null;
417
- // temp[0].status = 'open';
418
- if ( ticketDetails?._source?.status == 'closed' ) {
419
- delete temp[0].status;
420
- temp.push( { ...ticketDetails?._source, status: 'closed' } );
421
-
422
- temp[1].userName = getData?.body?.hits?.hits?.[0]?._source?.approverUserName;
423
- temp[1].email = getData?.body?.hits?.hits?.[0]?._source?.approverEmail;
424
- temp[1].role = getData?.body?.hits?.hits?.[0]?._source?.approverRole;
425
- temp[1].employeeCount = getData?.body?.hits?.hits?.[0]?._source?.employeeACCount;
426
- temp[1].houseKeepingCount = getData?.body?.hits?.hits?.[0]?._source?.houseKeepingACCount;
427
- temp[1].duplicateCount = getData?.body?.hits?.hits?.[0]?._source?.duplicateACCount;
428
- temp[1].junkCount = getData?.body?.hits?.hits?.[0]?._source?.junkACCount;
429
- }
430
- const LamdaURL = revop.getImages;
431
- let resultData = await LamdaServiceCall( LamdaURL, inputData );
432
- logger.info( { resultData: resultData } );
433
- if ( resultData ) {
434
- temp.length? temp[0].status = 'open': null;
435
- if ( resultData.status_code == '200' ) {
436
- return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0? temp : null, config: req?.store?.footfallDirectoryConfigs } );
437
- } else {
438
- return res.sendError( 'No Content', 204 );
439
- }
440
- } else {
441
- return res.sendError( 'No Content', 204 );
442
- }
646
+ const footfallValue = ticketDetails?._source?.footfallCount ?? 0;
647
+ const mappingInfoArray = ticketDetails?._source?.mappingInfo ?? [];
648
+ // Helper to get mappingInfo for an actionType
649
+ function getMappingForType( type ) {
650
+ return mappingInfoArray.find( ( m ) => m.type === type ) || {};
651
+ }
652
+
653
+ // List of actionTypes to process in sequence
654
+
655
+ if ( req.user.userType !== 'tango' && req.user.role !== 'superadmin' ) {
656
+ switch ( req.user.role ) {
657
+ case 'user':
658
+ const actionTypesUser = [ 'tagging', 'finalRevision' ];
659
+
660
+ temp = [];
661
+ actionTypesUser.forEach( ( type ) => {
662
+ const mapping = getMappingForType( type );
663
+ if ( type === 'tagging' ) {
664
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
665
+ const revisedPerc =
666
+ footfallValue > 0 ?
667
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
668
+ '0';
669
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
670
+ temp.push( {
671
+ actionType: type,
672
+ footfall: footfallValue,
673
+ revicedFootfall: revisedFootfall,
674
+ revicedPerc: revisedPerc,
675
+ count: countObj,
676
+ createdAt: mapping.createdAt ?? '',
677
+ createdByEmail: mapping.createdByEmail ?? '',
678
+ createdByUserName: mapping.createdByUserName ?? '',
679
+ createdByRole: mapping.createdByRole ?? '',
680
+ isUp: false,
681
+ } );
682
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
683
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
684
+ const revisedPerc =
685
+ footfallValue > 0 ?
686
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
687
+ '0';
688
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
689
+ temp.push( {
690
+ actionType: type,
691
+ footfall: footfallValue,
692
+ revicedFootfall: revisedFootfall,
693
+ revicedPerc: revisedPerc,
694
+ count: countObj,
695
+ createdAt: mapping.createdAt ?? '',
696
+ createdByEmail: mapping.createdByEmail ?? '',
697
+ createdByUserName: mapping.createdByUserName ?? '',
698
+ createdByRole: mapping.createdByRole ?? '',
699
+ } );
700
+ }
701
+ } );
702
+ break;
703
+ case 'admin':
704
+ const actionTypesAdmin = [ 'tagging', 'review', 'finalRevision' ];
705
+ temp = [];
706
+ actionTypesAdmin.forEach( ( type ) => {
707
+ const mapping = getMappingForType( type );
708
+ if ( type === 'tagging' ) {
709
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
710
+ const revisedPerc =
711
+ footfallValue > 0 ?
712
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
713
+ '0';
714
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
715
+ temp.push( {
716
+ actionType: type,
717
+ footfall: footfallValue,
718
+ revicedFootfall: revisedFootfall,
719
+ revicedPerc: revisedPerc,
720
+ count: countObj,
721
+ createdAt: mapping.createdAt ?? '',
722
+ createdByEmail: mapping.createdByEmail ?? '',
723
+ createdByUserName: mapping.createdByUserName ?? '',
724
+ createdByRole: mapping.createdByRole ?? '',
725
+ isUp: false,
726
+ } );
727
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
728
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
729
+ const revisedPerc =
730
+ footfallValue > 0 ?
731
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
732
+ '0';
733
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
734
+ temp.push( {
735
+ actionType: type,
736
+ footfall: footfallValue,
737
+ revicedFootfall: revisedFootfall,
738
+ revicedPerc: revisedPerc,
739
+ count: countObj,
740
+ createdAt: mapping.createdAt ?? '',
741
+ createdByEmail: mapping.createdByEmail ?? '',
742
+ createdByUserName: mapping.createdByUserName ?? '',
743
+ createdByRole: mapping.createdByRole ?? '',
744
+ } );
745
+ }
746
+ } );
747
+ }
748
+ } else {
749
+ const actionTypes = [ 'tagging', 'review', 'approve', 'tangoreview', 'finalRevision' ];
750
+
751
+ // Dynamically add to temp only if actionType matches and status is 'closed',
752
+ // except for 'tagging' where status must be 'raised'
753
+ temp = [];
754
+ actionTypes.forEach( ( type ) => {
755
+ const mapping = getMappingForType( type );
756
+ if ( type === 'tagging' ) {
757
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
758
+ temp.push( {
759
+ actionType: type,
760
+ footfall: footfallValue,
761
+ revicedFootfall: mapping.revicedFootfall ?? 0,
762
+ revicedPerc: mapping.revicedPerc ?? '--',
763
+ count: countObj,
764
+ createdAt: mapping.createdAt ?? '',
765
+ createdByEmail: mapping.createdByEmail ?? '',
766
+ createdByUserName: mapping.createdByUserName ?? '',
767
+ createdByRole: mapping.createdByRole ?? '',
768
+ isUp: false,
769
+ } );
770
+ }
771
+ if ( type !== 'tagging' && mapping.status === 'Closed' ) {
772
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
773
+ temp.push( {
774
+ actionType: type,
775
+ footfall: footfallValue,
776
+ revicedFootfall: mapping.revicedFootfall ?? 0,
777
+ revicedPerc: mapping.revicedPerc ?? '--',
778
+ count: countObj,
779
+ createdAt: mapping.createdAt ?? '',
780
+ createdByEmail: mapping.createdByEmail ?? '',
781
+ createdByUserName: mapping.createdByUserName ?? '',
782
+ createdByRole: mapping.createdByRole ?? '',
783
+ } );
784
+ }
785
+ } );
786
+ }
787
+
788
+
789
+ const LamdaURL = revop.getImages;
790
+ let resultData = await LamdaServiceCall( LamdaURL, inputData );
791
+ if ( resultData ) {
792
+ // temp.length? temp[0].status = 'open': null;
793
+
794
+ if ( resultData.status_code == '200' ) {
795
+ return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0 && ticketDetails? temp : null, config: req?.store?.footfallDirectoryConfigs } );
796
+ } else {
797
+ return res.sendError( 'No Content', 204 );
798
+ }
799
+ } else {
800
+ return res.sendError( 'No Content', 204 );
801
+ }
443
802
  } catch ( error ) {
444
803
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
445
804
  const err = error.message || 'Internal Server Error';
@@ -464,9 +823,21 @@ export async function tagTempId( req, res ) {
464
823
  exitTime: inputData.exitTime,
465
824
  filePath: inputData.filePath,
466
825
  status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
467
- description: '',
468
- isChecked: inputData.isChecked,
469
- duplicateImage: inputData?.duplicateImage?.length>0? inputData?.duplicateImage :[],
826
+ description: inputData.comments || '',
827
+ isChecked: null,
828
+ // Add id to each object in duplicateImage if it exists and is an array
829
+ duplicateImage: Array.isArray( inputData?.duplicateImage ) ?
830
+ inputData.duplicateImage.map( ( img ) => ( {
831
+ ...img,
832
+ id: `${inputData?.storeId}_${inputData?.dateString}_${img?.tempId}`,
833
+ actions: [
834
+ {
835
+ actionType: 'tagging',
836
+ action: 'submitted',
837
+ },
838
+ ],
839
+ } ) ) :
840
+ [],
470
841
  type: 'tagging-reflect',
471
842
  ticketStatus: 'submitted',
472
843
  isParent: inputData?.revopsType === 'duplicate'? true : false,
@@ -476,6 +847,7 @@ export async function tagTempId( req, res ) {
476
847
  action: 'submitted',
477
848
  },
478
849
  ],
850
+ comments: inputData.comments || '',
479
851
  createdAt: new Date(),
480
852
  updatedAt: new Date(),
481
853
 
@@ -491,8 +863,10 @@ export async function tagTempId( req, res ) {
491
863
  ctx._source.description = params.description;
492
864
  ctx._source.isChecked = params.isChecked;
493
865
  ctx._source.duplicateImage = params.duplicateImage;
866
+ ctx._source.isParent = params.isParent;
494
867
  ctx._source.updatedAt = params.updatedAt;
495
868
  ctx._source.timeRange = params.timeRange;
869
+ ctx._source.comments = params.comments;
496
870
  if (ctx._source.createdAt == null) {
497
871
  ctx._source.createdAt = params.createdAt;
498
872
  ctx._source.clientId = params.clientId;
@@ -507,6 +881,32 @@ export async function tagTempId( req, res ) {
507
881
  };
508
882
  const id = `${inputData.storeId}_${inputData.dateString}_${inputData.timeRange}_${inputData.tempId}`;
509
883
  await upsertWithScript( openSearch.revop, id, { script, upsert: upsertRecord } );
884
+ if ( inputData?.comments && inputData?.comments !== '' ) {
885
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
886
+ const logs = {
887
+ type: 'tagging',
888
+ parent: inputData?.revopsType == 'duplicate'? inputData.tempId : null,
889
+ id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
890
+ tempId: inputData.tempId,
891
+ timeRange: inputData.timeRange,
892
+ storeId: inputData.storeId,
893
+ dateString: inputData.dateString,
894
+ processType: inputData.processType,
895
+ category: inputData.revopsType,
896
+ entryTime: inputData.entryTime,
897
+ exitTime: inputData.exitTime,
898
+ filePath: inputData.filePath,
899
+ status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
900
+ description: inputData.comments || '',
901
+ isChecked: null,
902
+ createdByEmail: req?.user?.email,
903
+ createdByUserName: req?.user?.userName,
904
+ createdByRole: req?.user?.role,
905
+ message: inputData.comments || '',
906
+ createdAt: new Date(),
907
+ };
908
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
909
+ }
510
910
  if ( inputData?.duplicateImage?.length> 0 ) {
511
911
  let bulkBody = [];
512
912
  for ( let item of inputData?.duplicateImage ) {
@@ -514,10 +914,10 @@ export async function tagTempId( req, res ) {
514
914
  clientId: inputData.storeId.split( '-' )[0],
515
915
  storeId: inputData.storeId,
516
916
  tempId: item.tempId,
517
- id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
917
+ id: `${inputData?.storeId}_${inputData?.dateString}_${item?.tempId}`,
518
918
  dateString: inputData.dateString,
519
919
  timeRange: item.timeRange,
520
- isChecked: item.isChecked,
920
+ isChecked: null,
521
921
  processType: inputData.processType,
522
922
  revopsType: item.revopsType,
523
923
  entryTime: item.entryTime,
@@ -529,6 +929,7 @@ export async function tagTempId( req, res ) {
529
929
  isParent: false,
530
930
  type: 'tagging-reflect',
531
931
  ticketStatus: 'submitted',
932
+ comments: inputData.comments || '',
532
933
  actions: [
533
934
  {
534
935
  actionType: 'tagging',
@@ -556,12 +957,20 @@ export async function tagTempId( req, res ) {
556
957
  return { success: false, errors: res1.items };
557
958
  } else {
558
959
  logger.info( { msg: 'res1' } );
559
- return res.sendSuccess( `ID tagged as duplicates` );
960
+ return res.sendSuccess( `ID tagged as Duplicates` );
560
961
  // return { success: true };
561
962
  }
562
963
  }
563
964
  } else {
564
- const message = inputData?.revopsType == 'non-tagging' ? 'ID removed from tagging' :inputData?.revopsType == 'employee' ?'ID tagged as an employee/staff':inputData?.revopsType == 'junk'? 'ID tagged as a junk':'ID tagged as an house keeping';
965
+ // Convert camelCase revopsType to space-separated and capitalize first letter of each word
966
+ function camelCaseToTitle( str ) {
967
+ if ( !str ) return '';
968
+ return str.replace( /([A-Z])/g, ' $1' ).replace( /^./, ( s ) => s.toUpperCase() );
969
+ }
970
+ const titleRevopsType = camelCaseToTitle( inputData?.revopsType );
971
+ const message = inputData?.revopsType == 'non-tagging' ?
972
+ 'ID removed from tagging' :
973
+ `ID tagged as ${titleRevopsType}`;
565
974
  return res.sendSuccess( message );
566
975
  }
567
976
  } catch ( error ) {
@@ -588,3 +997,343 @@ export async function getCategorizedImages( req, res ) {
588
997
  return res.sendError( err, 500 );
589
998
  }
590
999
  }
1000
+
1001
+ export async function vmsDataMigration( req, res ) {
1002
+ try {
1003
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
1004
+ const inputData = req.body;
1005
+ const { storeId, dateString, limit = 100 } = inputData;
1006
+
1007
+ // Build query to fetch old structure documents
1008
+ const query = {
1009
+ query: {
1010
+ bool: {
1011
+ must: [
1012
+ {
1013
+ term: {
1014
+ 'ticketName.keyword': 'footfall-directory',
1015
+ },
1016
+ },
1017
+ ],
1018
+ },
1019
+ },
1020
+ size: parseInt( limit ),
1021
+ };
1022
+
1023
+ // Add storeId filter if provided
1024
+ if ( storeId ) {
1025
+ query.query.bool.must.push( {
1026
+ term: {
1027
+ 'storeId.keyword': storeId,
1028
+ },
1029
+ } );
1030
+ }
1031
+
1032
+ // Add dateString filter if provided
1033
+ if ( dateString ) {
1034
+ query.query.bool.must.push( {
1035
+ terms: {
1036
+ dateString: dateString?.split( ',' ),
1037
+ },
1038
+ } );
1039
+ }
1040
+
1041
+ // Exclude documents that already have the new structure (have mappingInfo or type: 'store')
1042
+ query.query.bool.must_not = [
1043
+ {
1044
+ exists: {
1045
+ field: 'mappingInfo',
1046
+ },
1047
+ },
1048
+ {
1049
+ term: {
1050
+ 'type.keyword': 'store',
1051
+ },
1052
+ },
1053
+ ];
1054
+
1055
+ const getData = await getOpenSearchData( openSearch.oldFootfallDirectory, query );
1056
+ logger.info( { getData } );
1057
+ const hits = getData?.body?.hits?.hits || [];
1058
+
1059
+ if ( hits.length === 0 ) {
1060
+ return res.sendSuccess( { message: 'No documents found to migrate', migrated: 0 } );
1061
+ }
1062
+
1063
+ let migratedCount = 0;
1064
+ const errors = [];
1065
+
1066
+ for ( const hit of hits ) {
1067
+ try {
1068
+ const oldSource = hit._source;
1069
+ const documentId = hit._id;
1070
+
1071
+ // Calculate revicedFootfall (sum of AC counts)
1072
+ const revicedFootfall = ( oldSource.duplicateACCount || 0 ) +
1073
+ ( oldSource.employeeACCount || 0 ) +
1074
+ ( oldSource.houseKeepingACCount || 0 ) +
1075
+ ( oldSource.junkACCount || 0 );
1076
+
1077
+ // Calculate revicedPerc
1078
+ const footfallCount = oldSource.footfallCount || 0;
1079
+ const revicedPerc = footfallCount > 0 ?
1080
+ Math.round( ( revicedFootfall / footfallCount ) * 100 ) :
1081
+ 0;
1082
+
1083
+ // Calculate reviced
1084
+ const reviced = parseInt( revicedPerc );
1085
+
1086
+ // Transform arrays to revisedDetail format
1087
+ const revisedDetail = [];
1088
+
1089
+ // Transform duplicateImages
1090
+ if ( Array.isArray( oldSource.duplicateImages ) ) {
1091
+ for ( const duplicate of oldSource.duplicateImages ) {
1092
+ const parentId = `${oldSource.storeId}_${oldSource.dateString}_${duplicate.tempId}`;
1093
+ const parentDetail = {
1094
+ id: parentId,
1095
+ clientId: oldSource.clientId,
1096
+ storeId: oldSource.storeId,
1097
+ tempId: duplicate.tempId,
1098
+ dateString: oldSource.dateString,
1099
+ timeRange: duplicate.timeRange,
1100
+ processType: 'footfall',
1101
+ revopsType: 'duplicate',
1102
+ entryTime: duplicate.entryTime,
1103
+ exitTime: duplicate.exitTime,
1104
+ filePath: duplicate.filePath,
1105
+ status: oldSource.duplicateStatus || 'submitted',
1106
+ description: '',
1107
+ isChecked: duplicate.isChecked !== undefined ? duplicate.isChecked : false,
1108
+ type: 'tagging-reflect',
1109
+ parent: null,
1110
+ isParent: true,
1111
+ createdAt: oldSource.createdAt || new Date(),
1112
+ updatedAt: oldSource.updatedAt || new Date(),
1113
+ duplicateImage: [],
1114
+ };
1115
+
1116
+ // Add child duplicate images
1117
+ if ( Array.isArray( duplicate.data ) ) {
1118
+ parentDetail.duplicateImage = duplicate.data.map( ( child ) => ( {
1119
+ id: `${oldSource.storeId}_${oldSource.dateString}_${child.tempId}`,
1120
+ tempId: child.tempId,
1121
+ timeRange: child.timeRange,
1122
+ entryTime: child.entryTime,
1123
+ exitTime: child.exitTime,
1124
+ filePath: child.filePath,
1125
+ isChecked: child.isChecked !== undefined ? child.isChecked : true,
1126
+ } ) );
1127
+
1128
+ // Add child details to revisedDetail
1129
+ for ( const child of duplicate.data ) {
1130
+ revisedDetail.push( {
1131
+ id: `${oldSource.storeId}_${oldSource.dateString}_${child.tempId}`,
1132
+ clientId: oldSource.clientId,
1133
+ storeId: oldSource.storeId,
1134
+ tempId: child.tempId,
1135
+ dateString: oldSource.dateString,
1136
+ timeRange: child.timeRange,
1137
+ processType: 'footfall',
1138
+ revopsType: 'duplicate',
1139
+ entryTime: child.entryTime,
1140
+ exitTime: child.exitTime,
1141
+ filePath: child.filePath,
1142
+ status: oldSource.duplicateStatus || 'submitted',
1143
+ description: '',
1144
+ isChecked: child.isChecked !== undefined ? child.isChecked : false,
1145
+ type: 'tagging-reflect',
1146
+ parent: duplicate.tempId,
1147
+ isParent: false,
1148
+ createdAt: oldSource.createdAt || new Date(),
1149
+ updatedAt: oldSource.updatedAt || new Date(),
1150
+ duplicateImage: [],
1151
+ } );
1152
+ }
1153
+ }
1154
+
1155
+ revisedDetail.push( parentDetail );
1156
+ }
1157
+ }
1158
+
1159
+ // Transform houseKeeping
1160
+ if ( Array.isArray( oldSource.houseKeeping ) ) {
1161
+ for ( const item of oldSource.houseKeeping ) {
1162
+ revisedDetail.push( {
1163
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1164
+ clientId: oldSource.clientId,
1165
+ storeId: oldSource.storeId,
1166
+ tempId: item.tempId,
1167
+ dateString: oldSource.dateString,
1168
+ timeRange: item.timeRange,
1169
+ processType: 'footfall',
1170
+ revopsType: 'houseKeeping',
1171
+ entryTime: item.entryTime,
1172
+ exitTime: item.exitTime,
1173
+ filePath: item.filePath,
1174
+ status: oldSource.houseKeepingStatus || 'submitted',
1175
+ description: '',
1176
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1177
+ type: 'tagging-reflect',
1178
+ parent: null,
1179
+ isParent: false,
1180
+ createdAt: oldSource.createdAt || new Date(),
1181
+ updatedAt: oldSource.updatedAt || new Date(),
1182
+ duplicateImage: [],
1183
+ } );
1184
+ }
1185
+ }
1186
+
1187
+ // Transform employee
1188
+ if ( Array.isArray( oldSource.employee ) ) {
1189
+ for ( const item of oldSource.employee ) {
1190
+ revisedDetail.push( {
1191
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1192
+ clientId: oldSource.clientId,
1193
+ storeId: oldSource.storeId,
1194
+ tempId: item.tempId,
1195
+ dateString: oldSource.dateString,
1196
+ timeRange: item.timeRange,
1197
+ processType: 'footfall',
1198
+ revopsType: 'employee',
1199
+ entryTime: item.entryTime,
1200
+ exitTime: item.exitTime,
1201
+ filePath: item.filePath,
1202
+ status: oldSource.employeeStatus || 'submitted',
1203
+ description: '',
1204
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1205
+ type: 'tagging-reflect',
1206
+ parent: null,
1207
+ isParent: false,
1208
+ createdAt: oldSource.createdAt || new Date(),
1209
+ updatedAt: oldSource.updatedAt || new Date(),
1210
+ duplicateImage: [],
1211
+ } );
1212
+ }
1213
+ }
1214
+
1215
+ // Transform junk
1216
+ if ( Array.isArray( oldSource.junk ) ) {
1217
+ for ( const item of oldSource.junk ) {
1218
+ revisedDetail.push( {
1219
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1220
+ clientId: oldSource.clientId,
1221
+ storeId: oldSource.storeId,
1222
+ tempId: item.tempId,
1223
+ dateString: oldSource.dateString,
1224
+ timeRange: item.timeRange,
1225
+ processType: 'footfall',
1226
+ revopsType: 'junk',
1227
+ entryTime: item.entryTime,
1228
+ exitTime: item.exitTime,
1229
+ filePath: item.filePath,
1230
+ status: 'submitted',
1231
+ description: '',
1232
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1233
+ type: 'tagging-reflect',
1234
+ parent: null,
1235
+ isParent: false,
1236
+ createdAt: oldSource.createdAt || new Date(),
1237
+ updatedAt: oldSource.updatedAt || new Date(),
1238
+ duplicateImage: [],
1239
+ } );
1240
+ }
1241
+ }
1242
+
1243
+ // Create count array
1244
+ const count = [
1245
+ {
1246
+ name: 'Duplicate',
1247
+ value: oldSource.duplicateCount || 0,
1248
+ key: 'duplicateCount',
1249
+ type: 'duplicate',
1250
+ },
1251
+ {
1252
+ name: 'Employee',
1253
+ value: oldSource.employeeCount || 0,
1254
+ key: 'employeeCount',
1255
+ type: 'employee',
1256
+ },
1257
+ {
1258
+ name: 'House keeping',
1259
+ value: oldSource.houseKeepingCount || 0,
1260
+ key: 'houseKeepingCount',
1261
+ type: 'houseKeeping',
1262
+ },
1263
+ {
1264
+ name: 'Junk',
1265
+ value: oldSource.junkCount || 0,
1266
+ key: 'junkCount',
1267
+ type: 'junk',
1268
+ },
1269
+ ];
1270
+
1271
+ // Create mappingInfo array
1272
+ const mappingInfo = [
1273
+ {
1274
+ type: 'tagging',
1275
+ mode: 'mobile',
1276
+ revicedFootfall,
1277
+ revicedPerc: `${revicedPerc}%`,
1278
+ reviced,
1279
+ count,
1280
+ revisedDetail,
1281
+ status: oldSource.status === 'open' ? 'Raised' : oldSource.status || 'Raised',
1282
+ createdByEmail: oldSource.email || '',
1283
+ createdByUserName: oldSource.userName || '',
1284
+ createdByRole: oldSource.role || 'user',
1285
+ createdAt: oldSource.createdAt || new Date(),
1286
+ },
1287
+ {
1288
+ type: 'review',
1289
+ count,
1290
+ revisedDetail,
1291
+ status: oldSource.status === 'open' ? 'Open' : oldSource.status || 'Open',
1292
+ dueDate: oldSource.updatedAt ? new Date( new Date( oldSource.updatedAt ).getTime() + 3 * 24 * 60 * 60 * 1000 ) : new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ),
1293
+ },
1294
+ ];
1295
+
1296
+ // Create new structure
1297
+ const newSource = {
1298
+ storeId: oldSource.storeId,
1299
+ type: 'store',
1300
+ dateString: oldSource.dateString,
1301
+ storeName: oldSource.storeName,
1302
+ ticketName: oldSource.ticketName,
1303
+ footfallCount: oldSource.footfallCount,
1304
+ clientId: oldSource.clientId,
1305
+ ticketId: oldSource.ticketId,
1306
+ createdAt: oldSource.createdAt,
1307
+ updatedAt: oldSource.updatedAt,
1308
+ status: oldSource.status === 'open' ? 'Raised' : oldSource.status || 'Raised',
1309
+ comments: oldSource.comments || '',
1310
+ revicedFootfall,
1311
+ revicedPerc: `${revicedPerc}%`,
1312
+ reviced,
1313
+ mappingInfo,
1314
+ };
1315
+
1316
+ // Update document in OpenSearch
1317
+ // await updateOpenSearchData( openSearch.footfallDirectory, documentId, { doc: newSource } );
1318
+ // migratedCount++;
1319
+
1320
+ logger.info( { message: 'Document migrated successfully', newSource, documentId, storeId: oldSource.storeId, dateString: oldSource.dateString } );
1321
+ } catch ( error ) {
1322
+ const errorMsg = `Error migrating document ${hit._id}: ${error.message}`;
1323
+ errors.push( errorMsg );
1324
+ logger.error( { error: error, documentId: hit._id, function: 'vmsDataMigration' } );
1325
+ }
1326
+ }
1327
+
1328
+ return res.sendSuccess( {
1329
+ message: `Migration completed. ${migratedCount} document(s) migrated.`,
1330
+ migrated: migratedCount,
1331
+ total: hits.length,
1332
+ errors: errors.length > 0 ? errors : undefined,
1333
+ } );
1334
+ } catch ( error ) {
1335
+ logger.error( { error: error, message: req.query, function: 'vmsDataMigration' } );
1336
+ const err = error.message || 'Internal Server Error';
1337
+ return res.sendError( err, 500 );
1338
+ }
1339
+ }