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

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, 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,109 @@ 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
+ }
140
243
  export async function revoptaggingcount( req, res ) {
141
244
  try {
142
245
  const openSearch = JSON.parse( process.env.OPENSEARCH );
@@ -221,6 +324,9 @@ export async function storeProcessedData( req, res ) {
221
324
  dayjs.extend( isSameOrBefore );
222
325
 
223
326
  let start = dayjs( fromDate );
327
+ // get start value from the before day one
328
+ // Move start one day back so we can access "day before" when looping
329
+ start = start.subtract( 1, 'day' );
224
330
  let end = dayjs( toDate );
225
331
 
226
332
  if ( !start.isValid() || !end.isValid() ) {
@@ -267,10 +373,12 @@ export async function storeProcessedData( req, res ) {
267
373
 
268
374
  const responseArray = [];
269
375
 
270
- for ( let i = 0; i < orderedDates.length; i++ ) {
376
+ for ( let i = 1; i < orderedDates.length; i++ ) {
271
377
  const currentDate = orderedDates[i];
272
378
  const currentId = `${storeId}_${currentDate}`;
379
+ logger.info( { currentId, currentDate } );
273
380
  const processedData = hitsMap.get( currentId );
381
+ logger.info( { processedData } );
274
382
  if ( !processedData ) {
275
383
  responseArray.push( {
276
384
  date: currentDate,
@@ -283,13 +391,17 @@ export async function storeProcessedData( req, res ) {
283
391
 
284
392
  const prevDate = dayjs( currentDate ).subtract( 1, 'day' ).format( 'YYYY-MM-DD' );
285
393
  const prevId = `${storeId}_${prevDate}`;
394
+ logger.info( { prevId, prevDate } );
286
395
  const previousData = hitsMap.get( prevId );
287
396
 
288
397
  let footfallCountTrend = 0;
398
+ logger.info( { previousData, previoucubr1: previousData?.footfall_count } );
289
399
  if ( previousData && previousData.footfall_count ) {
400
+ logger.info( { previousData, previoucubr: previousData?.footfall_count } );
290
401
  footfallCountTrend = Math.round(
291
- ( ( processedData.footfall_count - previousData.footfall_count ) / previousData.footfall_count ) * 100,
402
+ ( ( processedData.footfall_count - previousData?.footfall_count ) / previousData.footfall_count ) * 100,
292
403
  );
404
+ logger.info( { footfallCountTrend } );
293
405
  }
294
406
  // Add ticket status from openSearch.footfallDirectory (_source.status)
295
407
  let ticketStatus = null;
@@ -302,6 +414,11 @@ export async function storeProcessedData( req, res ) {
302
414
  { term: { 'storeId.keyword': storeId } },
303
415
  { term: { 'dateString': currentDate } },
304
416
  { term: { 'ticketName.keyword': 'footfall-directory' } },
417
+ {
418
+ 'term': {
419
+ 'type.keyword': 'store',
420
+ },
421
+ },
305
422
  ],
306
423
  },
307
424
  },
@@ -403,43 +520,177 @@ export async function footFallImages( req, res ) {
403
520
  'ticketName.keyword': 'footfall-directory',
404
521
  },
405
522
  },
523
+ {
524
+ 'term': {
525
+ 'type.keyword': 'store',
526
+ },
527
+ },
406
528
  ],
407
529
  },
408
530
  },
409
- // '_source': [ 'dateString', 'storeId', 'duplicateCount', 'footfallCount', 'employeeCount', 'houseKeepingCount', 'junkCount', 'status', 'ticketId', 'comments', 'userName', 'role', 'createdAt', 'email', 'houseKeepingACCount', 'duplicateACCount', 'employeeACCount', 'junkACCount', 'approverEmail', 'approverRole', 'approverUserName' ],
531
+ '_source': [ 'dateString', 'storeId', 'mappingInfo', 'revicedFootfall', 'revicedPerc', 'reviced', 'createdAt', 'updatedAt', 'footfallCount' ],
410
532
 
411
533
  };
412
534
 
413
535
  const getData = await getOpenSearchData( opensearch.footfallDirectory, query );
414
536
  const ticketDetails = getData?.body?.hits?.hits[0];
415
537
  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
- }
538
+ const footfallValue = ticketDetails?._source?.footfallCount ?? 0;
539
+ const mappingInfoArray = ticketDetails?._source?.mappingInfo ?? [];
540
+ // Helper to get mappingInfo for an actionType
541
+ function getMappingForType( type ) {
542
+ return mappingInfoArray.find( ( m ) => m.type === type ) || {};
543
+ }
544
+
545
+ // List of actionTypes to process in sequence
546
+
547
+ if ( req.user.userType !== 'tango' && req.user.role !== 'superadmin' ) {
548
+ switch ( req.user.role ) {
549
+ case 'user':
550
+ const actionTypesUser = [ 'tagging', 'finalRevision' ];
551
+
552
+ temp = [];
553
+ actionTypesUser.forEach( ( type ) => {
554
+ const mapping = getMappingForType( type );
555
+ if ( type === 'tagging' ) {
556
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
557
+ const revisedPerc =
558
+ footfallValue > 0 ?
559
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
560
+ '0';
561
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
562
+ temp.push( {
563
+ actionType: type,
564
+ footfall: footfallValue,
565
+ revicedFootfall: revisedFootfall,
566
+ revicedPerc: revisedPerc,
567
+ count: countObj,
568
+ createdAt: mapping.createdAt ?? '',
569
+ createdByEmail: mapping.createdByEmail ?? '',
570
+ createdByUserName: mapping.createdByUserName ?? '',
571
+ createdByRole: mapping.createdByRole ?? '',
572
+ isUp: false,
573
+ } );
574
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
575
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
576
+ const revisedPerc =
577
+ footfallValue > 0 ?
578
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
579
+ '0';
580
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
581
+ temp.push( {
582
+ actionType: type,
583
+ footfall: footfallValue,
584
+ revicedFootfall: revisedFootfall,
585
+ revicedPerc: revisedPerc,
586
+ count: countObj,
587
+ createdAt: mapping.createdAt ?? '',
588
+ createdByEmail: mapping.createdByEmail ?? '',
589
+ createdByUserName: mapping.createdByUserName ?? '',
590
+ createdByRole: mapping.createdByRole ?? '',
591
+ } );
592
+ }
593
+ } );
594
+ break;
595
+ case 'admin':
596
+ const actionTypesAdmin = [ 'tagging', 'review', 'finalRevision' ];
597
+ temp = [];
598
+ actionTypesAdmin.forEach( ( type ) => {
599
+ const mapping = getMappingForType( type );
600
+ if ( type === 'tagging' ) {
601
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
602
+ const revisedPerc =
603
+ footfallValue > 0 ?
604
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
605
+ '0';
606
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
607
+ temp.push( {
608
+ actionType: type,
609
+ footfall: footfallValue,
610
+ revicedFootfall: revisedFootfall,
611
+ revicedPerc: revisedPerc,
612
+ count: countObj,
613
+ createdAt: mapping.createdAt ?? '',
614
+ createdByEmail: mapping.createdByEmail ?? '',
615
+ createdByUserName: mapping.createdByUserName ?? '',
616
+ createdByRole: mapping.createdByRole ?? '',
617
+ isUp: false,
618
+ } );
619
+ } else if ( type !== 'tagging' && mapping.status === 'Closed' ) {
620
+ const revisedFootfall = mapping.revicedFootfall ?? 0;
621
+ const revisedPerc =
622
+ footfallValue > 0 ?
623
+ `${Math.round( ( revisedFootfall / footfallValue ) * 100 )}` :
624
+ '0';
625
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
626
+ temp.push( {
627
+ actionType: type,
628
+ footfall: footfallValue,
629
+ revicedFootfall: revisedFootfall,
630
+ revicedPerc: revisedPerc,
631
+ count: countObj,
632
+ createdAt: mapping.createdAt ?? '',
633
+ createdByEmail: mapping.createdByEmail ?? '',
634
+ createdByUserName: mapping.createdByUserName ?? '',
635
+ createdByRole: mapping.createdByRole ?? '',
636
+ } );
637
+ }
638
+ } );
639
+ }
640
+ } else {
641
+ const actionTypes = [ 'tagging', 'review', 'approve', 'tangoreview', 'finalRevision' ];
642
+
643
+ // Dynamically add to temp only if actionType matches and status is 'closed',
644
+ // except for 'tagging' where status must be 'raised'
645
+ temp = [];
646
+ actionTypes.forEach( ( type ) => {
647
+ const mapping = getMappingForType( type );
648
+ if ( type === 'tagging' ) {
649
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
650
+ temp.push( {
651
+ actionType: type,
652
+ footfall: footfallValue,
653
+ revicedFootfall: mapping.revicedFootfall ?? 0,
654
+ revicedPerc: mapping.reviced ?? '--',
655
+ count: countObj,
656
+ createdAt: mapping.createdAt ?? '',
657
+ createdByEmail: mapping.createdByEmail ?? '',
658
+ createdByUserName: mapping.createdByUserName ?? '',
659
+ createdByRole: mapping.createdByRole ?? '',
660
+ isUp: false,
661
+ } );
662
+ }
663
+ if ( type !== 'tagging' && mapping.status === 'Closed' ) {
664
+ const countObj = mapping.count ? [ ...mapping.count ] : [];
665
+ temp.push( {
666
+ actionType: type,
667
+ footfall: footfallValue,
668
+ revicedFootfall: mapping.revicedFootfall ?? 0,
669
+ revicedPerc: mapping.reviced ?? '--',
670
+ count: countObj,
671
+ createdAt: mapping.createdAt ?? '',
672
+ createdByEmail: mapping.createdByEmail ?? '',
673
+ createdByUserName: mapping.createdByUserName ?? '',
674
+ createdByRole: mapping.createdByRole ?? '',
675
+ } );
676
+ }
677
+ } );
678
+ }
679
+
680
+
681
+ const LamdaURL = revop.getImages;
682
+ let resultData = await LamdaServiceCall( LamdaURL, inputData );
683
+ if ( resultData ) {
684
+ // temp.length? temp[0].status = 'open': null;
685
+
686
+ if ( resultData.status_code == '200' ) {
687
+ return res.sendSuccess( { ...resultData, ticketStatus: temp?.length > 0 && ticketDetails? temp : null, config: req?.store?.footfallDirectoryConfigs } );
688
+ } else {
689
+ return res.sendError( 'No Content', 204 );
690
+ }
691
+ } else {
692
+ return res.sendError( 'No Content', 204 );
693
+ }
443
694
  } catch ( error ) {
444
695
  logger.error( { message: error, data: req.query, function: 'storeProcessedData' } );
445
696
  const err = error.message || 'Internal Server Error';
@@ -464,9 +715,21 @@ export async function tagTempId( req, res ) {
464
715
  exitTime: inputData.exitTime,
465
716
  filePath: inputData.filePath,
466
717
  status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
467
- description: '',
468
- isChecked: inputData.isChecked,
469
- duplicateImage: inputData?.duplicateImage?.length>0? inputData?.duplicateImage :[],
718
+ description: inputData.comments || '',
719
+ isChecked: null,
720
+ // Add id to each object in duplicateImage if it exists and is an array
721
+ duplicateImage: Array.isArray( inputData?.duplicateImage ) ?
722
+ inputData.duplicateImage.map( ( img ) => ( {
723
+ ...img,
724
+ id: `${inputData?.storeId}_${inputData?.dateString}_${img?.tempId}`,
725
+ actions: [
726
+ {
727
+ actionType: 'tagging',
728
+ action: 'submitted',
729
+ },
730
+ ],
731
+ } ) ) :
732
+ [],
470
733
  type: 'tagging-reflect',
471
734
  ticketStatus: 'submitted',
472
735
  isParent: inputData?.revopsType === 'duplicate'? true : false,
@@ -476,6 +739,7 @@ export async function tagTempId( req, res ) {
476
739
  action: 'submitted',
477
740
  },
478
741
  ],
742
+ comments: inputData.comments || '',
479
743
  createdAt: new Date(),
480
744
  updatedAt: new Date(),
481
745
 
@@ -491,8 +755,10 @@ export async function tagTempId( req, res ) {
491
755
  ctx._source.description = params.description;
492
756
  ctx._source.isChecked = params.isChecked;
493
757
  ctx._source.duplicateImage = params.duplicateImage;
758
+ ctx._source.isParent = params.isParent;
494
759
  ctx._source.updatedAt = params.updatedAt;
495
760
  ctx._source.timeRange = params.timeRange;
761
+ ctx._source.comments = params.comments;
496
762
  if (ctx._source.createdAt == null) {
497
763
  ctx._source.createdAt = params.createdAt;
498
764
  ctx._source.clientId = params.clientId;
@@ -507,6 +773,32 @@ export async function tagTempId( req, res ) {
507
773
  };
508
774
  const id = `${inputData.storeId}_${inputData.dateString}_${inputData.timeRange}_${inputData.tempId}`;
509
775
  await upsertWithScript( openSearch.revop, id, { script, upsert: upsertRecord } );
776
+ if ( inputData?.comments && inputData?.comments !== '' ) {
777
+ const id = `${inputData.storeId}_${inputData.dateString}_${Date.now()}`;
778
+ const logs = {
779
+ type: 'tagging',
780
+ parent: inputData?.revopsType == 'duplicate'? inputData.tempId : null,
781
+ id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
782
+ tempId: inputData.tempId,
783
+ timeRange: inputData.timeRange,
784
+ storeId: inputData.storeId,
785
+ dateString: inputData.dateString,
786
+ processType: inputData.processType,
787
+ category: inputData.revopsType,
788
+ entryTime: inputData.entryTime,
789
+ exitTime: inputData.exitTime,
790
+ filePath: inputData.filePath,
791
+ status: inputData?.revopsType == 'non-tagging' ?'':'submitted',
792
+ description: inputData.comments || '',
793
+ isChecked: null,
794
+ createdByEmail: req?.user?.email,
795
+ createdByUserName: req?.user?.userName,
796
+ createdByRole: req?.user?.role,
797
+ message: inputData.comments || '',
798
+ createdAt: new Date(),
799
+ };
800
+ await insertWithId( openSearch.vmsCommentsLog, id, logs );
801
+ }
510
802
  if ( inputData?.duplicateImage?.length> 0 ) {
511
803
  let bulkBody = [];
512
804
  for ( let item of inputData?.duplicateImage ) {
@@ -514,10 +806,10 @@ export async function tagTempId( req, res ) {
514
806
  clientId: inputData.storeId.split( '-' )[0],
515
807
  storeId: inputData.storeId,
516
808
  tempId: item.tempId,
517
- id: `${inputData?.storeId}_${inputData?.dateString}_${inputData?.tempId}`,
809
+ id: `${inputData?.storeId}_${inputData?.dateString}_${item?.tempId}`,
518
810
  dateString: inputData.dateString,
519
811
  timeRange: item.timeRange,
520
- isChecked: item.isChecked,
812
+ isChecked: null,
521
813
  processType: inputData.processType,
522
814
  revopsType: item.revopsType,
523
815
  entryTime: item.entryTime,
@@ -529,6 +821,7 @@ export async function tagTempId( req, res ) {
529
821
  isParent: false,
530
822
  type: 'tagging-reflect',
531
823
  ticketStatus: 'submitted',
824
+ comments: inputData.comments || '',
532
825
  actions: [
533
826
  {
534
827
  actionType: 'tagging',
@@ -556,12 +849,20 @@ export async function tagTempId( req, res ) {
556
849
  return { success: false, errors: res1.items };
557
850
  } else {
558
851
  logger.info( { msg: 'res1' } );
559
- return res.sendSuccess( `ID tagged as duplicates` );
852
+ return res.sendSuccess( `ID tagged as Duplicates` );
560
853
  // return { success: true };
561
854
  }
562
855
  }
563
856
  } 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';
857
+ // Convert camelCase revopsType to space-separated and capitalize first letter of each word
858
+ function camelCaseToTitle( str ) {
859
+ if ( !str ) return '';
860
+ return str.replace( /([A-Z])/g, ' $1' ).replace( /^./, ( s ) => s.toUpperCase() );
861
+ }
862
+ const titleRevopsType = camelCaseToTitle( inputData?.revopsType );
863
+ const message = inputData?.revopsType == 'non-tagging' ?
864
+ 'ID removed from tagging' :
865
+ `ID tagged as ${titleRevopsType}`;
565
866
  return res.sendSuccess( message );
566
867
  }
567
868
  } catch ( error ) {
@@ -588,3 +889,343 @@ export async function getCategorizedImages( req, res ) {
588
889
  return res.sendError( err, 500 );
589
890
  }
590
891
  }
892
+
893
+ export async function vmsDataMigration( req, res ) {
894
+ try {
895
+ const openSearch = JSON.parse( process.env.OPENSEARCH );
896
+ const inputData = req.body;
897
+ const { storeId, dateString, limit = 100 } = inputData;
898
+
899
+ // Build query to fetch old structure documents
900
+ const query = {
901
+ query: {
902
+ bool: {
903
+ must: [
904
+ {
905
+ term: {
906
+ 'ticketName.keyword': 'footfall-directory',
907
+ },
908
+ },
909
+ ],
910
+ },
911
+ },
912
+ size: parseInt( limit ),
913
+ };
914
+
915
+ // Add storeId filter if provided
916
+ if ( storeId ) {
917
+ query.query.bool.must.push( {
918
+ term: {
919
+ 'storeId.keyword': storeId,
920
+ },
921
+ } );
922
+ }
923
+
924
+ // Add dateString filter if provided
925
+ if ( dateString ) {
926
+ query.query.bool.must.push( {
927
+ terms: {
928
+ dateString: dateString?.split( ',' ),
929
+ },
930
+ } );
931
+ }
932
+
933
+ // Exclude documents that already have the new structure (have mappingInfo or type: 'store')
934
+ query.query.bool.must_not = [
935
+ {
936
+ exists: {
937
+ field: 'mappingInfo',
938
+ },
939
+ },
940
+ {
941
+ term: {
942
+ 'type.keyword': 'store',
943
+ },
944
+ },
945
+ ];
946
+
947
+ const getData = await getOpenSearchData( openSearch.oldFootfallDirectory, query );
948
+ logger.info( { getData } );
949
+ const hits = getData?.body?.hits?.hits || [];
950
+
951
+ if ( hits.length === 0 ) {
952
+ return res.sendSuccess( { message: 'No documents found to migrate', migrated: 0 } );
953
+ }
954
+
955
+ let migratedCount = 0;
956
+ const errors = [];
957
+
958
+ for ( const hit of hits ) {
959
+ try {
960
+ const oldSource = hit._source;
961
+ const documentId = hit._id;
962
+
963
+ // Calculate revicedFootfall (sum of AC counts)
964
+ const revicedFootfall = ( oldSource.duplicateACCount || 0 ) +
965
+ ( oldSource.employeeACCount || 0 ) +
966
+ ( oldSource.houseKeepingACCount || 0 ) +
967
+ ( oldSource.junkACCount || 0 );
968
+
969
+ // Calculate revicedPerc
970
+ const footfallCount = oldSource.footfallCount || 0;
971
+ const revicedPerc = footfallCount > 0 ?
972
+ Math.round( ( revicedFootfall / footfallCount ) * 100 ) :
973
+ 0;
974
+
975
+ // Calculate reviced
976
+ const reviced = parseInt( revicedPerc );
977
+
978
+ // Transform arrays to revisedDetail format
979
+ const revisedDetail = [];
980
+
981
+ // Transform duplicateImages
982
+ if ( Array.isArray( oldSource.duplicateImages ) ) {
983
+ for ( const duplicate of oldSource.duplicateImages ) {
984
+ const parentId = `${oldSource.storeId}_${oldSource.dateString}_${duplicate.tempId}`;
985
+ const parentDetail = {
986
+ id: parentId,
987
+ clientId: oldSource.clientId,
988
+ storeId: oldSource.storeId,
989
+ tempId: duplicate.tempId,
990
+ dateString: oldSource.dateString,
991
+ timeRange: duplicate.timeRange,
992
+ processType: 'footfall',
993
+ revopsType: 'duplicate',
994
+ entryTime: duplicate.entryTime,
995
+ exitTime: duplicate.exitTime,
996
+ filePath: duplicate.filePath,
997
+ status: oldSource.duplicateStatus || 'submitted',
998
+ description: '',
999
+ isChecked: duplicate.isChecked !== undefined ? duplicate.isChecked : false,
1000
+ type: 'tagging-reflect',
1001
+ parent: null,
1002
+ isParent: true,
1003
+ createdAt: oldSource.createdAt || new Date(),
1004
+ updatedAt: oldSource.updatedAt || new Date(),
1005
+ duplicateImage: [],
1006
+ };
1007
+
1008
+ // Add child duplicate images
1009
+ if ( Array.isArray( duplicate.data ) ) {
1010
+ parentDetail.duplicateImage = duplicate.data.map( ( child ) => ( {
1011
+ id: `${oldSource.storeId}_${oldSource.dateString}_${child.tempId}`,
1012
+ tempId: child.tempId,
1013
+ timeRange: child.timeRange,
1014
+ entryTime: child.entryTime,
1015
+ exitTime: child.exitTime,
1016
+ filePath: child.filePath,
1017
+ isChecked: child.isChecked !== undefined ? child.isChecked : true,
1018
+ } ) );
1019
+
1020
+ // Add child details to revisedDetail
1021
+ for ( const child of duplicate.data ) {
1022
+ revisedDetail.push( {
1023
+ id: `${oldSource.storeId}_${oldSource.dateString}_${child.tempId}`,
1024
+ clientId: oldSource.clientId,
1025
+ storeId: oldSource.storeId,
1026
+ tempId: child.tempId,
1027
+ dateString: oldSource.dateString,
1028
+ timeRange: child.timeRange,
1029
+ processType: 'footfall',
1030
+ revopsType: 'duplicate',
1031
+ entryTime: child.entryTime,
1032
+ exitTime: child.exitTime,
1033
+ filePath: child.filePath,
1034
+ status: oldSource.duplicateStatus || 'submitted',
1035
+ description: '',
1036
+ isChecked: child.isChecked !== undefined ? child.isChecked : false,
1037
+ type: 'tagging-reflect',
1038
+ parent: duplicate.tempId,
1039
+ isParent: false,
1040
+ createdAt: oldSource.createdAt || new Date(),
1041
+ updatedAt: oldSource.updatedAt || new Date(),
1042
+ duplicateImage: [],
1043
+ } );
1044
+ }
1045
+ }
1046
+
1047
+ revisedDetail.push( parentDetail );
1048
+ }
1049
+ }
1050
+
1051
+ // Transform houseKeeping
1052
+ if ( Array.isArray( oldSource.houseKeeping ) ) {
1053
+ for ( const item of oldSource.houseKeeping ) {
1054
+ revisedDetail.push( {
1055
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1056
+ clientId: oldSource.clientId,
1057
+ storeId: oldSource.storeId,
1058
+ tempId: item.tempId,
1059
+ dateString: oldSource.dateString,
1060
+ timeRange: item.timeRange,
1061
+ processType: 'footfall',
1062
+ revopsType: 'houseKeeping',
1063
+ entryTime: item.entryTime,
1064
+ exitTime: item.exitTime,
1065
+ filePath: item.filePath,
1066
+ status: oldSource.houseKeepingStatus || 'submitted',
1067
+ description: '',
1068
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1069
+ type: 'tagging-reflect',
1070
+ parent: null,
1071
+ isParent: false,
1072
+ createdAt: oldSource.createdAt || new Date(),
1073
+ updatedAt: oldSource.updatedAt || new Date(),
1074
+ duplicateImage: [],
1075
+ } );
1076
+ }
1077
+ }
1078
+
1079
+ // Transform employee
1080
+ if ( Array.isArray( oldSource.employee ) ) {
1081
+ for ( const item of oldSource.employee ) {
1082
+ revisedDetail.push( {
1083
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1084
+ clientId: oldSource.clientId,
1085
+ storeId: oldSource.storeId,
1086
+ tempId: item.tempId,
1087
+ dateString: oldSource.dateString,
1088
+ timeRange: item.timeRange,
1089
+ processType: 'footfall',
1090
+ revopsType: 'employee',
1091
+ entryTime: item.entryTime,
1092
+ exitTime: item.exitTime,
1093
+ filePath: item.filePath,
1094
+ status: oldSource.employeeStatus || 'submitted',
1095
+ description: '',
1096
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1097
+ type: 'tagging-reflect',
1098
+ parent: null,
1099
+ isParent: false,
1100
+ createdAt: oldSource.createdAt || new Date(),
1101
+ updatedAt: oldSource.updatedAt || new Date(),
1102
+ duplicateImage: [],
1103
+ } );
1104
+ }
1105
+ }
1106
+
1107
+ // Transform junk
1108
+ if ( Array.isArray( oldSource.junk ) ) {
1109
+ for ( const item of oldSource.junk ) {
1110
+ revisedDetail.push( {
1111
+ id: `${oldSource.storeId}_${oldSource.dateString}_${item.tempId}`,
1112
+ clientId: oldSource.clientId,
1113
+ storeId: oldSource.storeId,
1114
+ tempId: item.tempId,
1115
+ dateString: oldSource.dateString,
1116
+ timeRange: item.timeRange,
1117
+ processType: 'footfall',
1118
+ revopsType: 'junk',
1119
+ entryTime: item.entryTime,
1120
+ exitTime: item.exitTime,
1121
+ filePath: item.filePath,
1122
+ status: 'submitted',
1123
+ description: '',
1124
+ isChecked: item.isChecked !== undefined ? item.isChecked : false,
1125
+ type: 'tagging-reflect',
1126
+ parent: null,
1127
+ isParent: false,
1128
+ createdAt: oldSource.createdAt || new Date(),
1129
+ updatedAt: oldSource.updatedAt || new Date(),
1130
+ duplicateImage: [],
1131
+ } );
1132
+ }
1133
+ }
1134
+
1135
+ // Create count array
1136
+ const count = [
1137
+ {
1138
+ name: 'Duplicate',
1139
+ value: oldSource.duplicateCount || 0,
1140
+ key: 'duplicateCount',
1141
+ type: 'duplicate',
1142
+ },
1143
+ {
1144
+ name: 'Employee',
1145
+ value: oldSource.employeeCount || 0,
1146
+ key: 'employeeCount',
1147
+ type: 'employee',
1148
+ },
1149
+ {
1150
+ name: 'House keeping',
1151
+ value: oldSource.houseKeepingCount || 0,
1152
+ key: 'houseKeepingCount',
1153
+ type: 'houseKeeping',
1154
+ },
1155
+ {
1156
+ name: 'Junk',
1157
+ value: oldSource.junkCount || 0,
1158
+ key: 'junkCount',
1159
+ type: 'junk',
1160
+ },
1161
+ ];
1162
+
1163
+ // Create mappingInfo array
1164
+ const mappingInfo = [
1165
+ {
1166
+ type: 'tagging',
1167
+ mode: 'mobile',
1168
+ revicedFootfall,
1169
+ revicedPerc: `${revicedPerc}%`,
1170
+ reviced,
1171
+ count,
1172
+ revisedDetail,
1173
+ status: oldSource.status === 'open' ? 'Raised' : oldSource.status || 'Raised',
1174
+ createdByEmail: oldSource.email || '',
1175
+ createdByUserName: oldSource.userName || '',
1176
+ createdByRole: oldSource.role || 'user',
1177
+ createdAt: oldSource.createdAt || new Date(),
1178
+ },
1179
+ {
1180
+ type: 'review',
1181
+ count,
1182
+ revisedDetail,
1183
+ status: oldSource.status === 'open' ? 'Open' : oldSource.status || 'Open',
1184
+ dueDate: oldSource.updatedAt ? new Date( new Date( oldSource.updatedAt ).getTime() + 3 * 24 * 60 * 60 * 1000 ) : new Date( Date.now() + 3 * 24 * 60 * 60 * 1000 ),
1185
+ },
1186
+ ];
1187
+
1188
+ // Create new structure
1189
+ const newSource = {
1190
+ storeId: oldSource.storeId,
1191
+ type: 'store',
1192
+ dateString: oldSource.dateString,
1193
+ storeName: oldSource.storeName,
1194
+ ticketName: oldSource.ticketName,
1195
+ footfallCount: oldSource.footfallCount,
1196
+ clientId: oldSource.clientId,
1197
+ ticketId: oldSource.ticketId,
1198
+ createdAt: oldSource.createdAt,
1199
+ updatedAt: oldSource.updatedAt,
1200
+ status: oldSource.status === 'open' ? 'Raised' : oldSource.status || 'Raised',
1201
+ comments: oldSource.comments || '',
1202
+ revicedFootfall,
1203
+ revicedPerc: `${revicedPerc}%`,
1204
+ reviced,
1205
+ mappingInfo,
1206
+ };
1207
+
1208
+ // Update document in OpenSearch
1209
+ // await updateOpenSearchData( openSearch.footfallDirectory, documentId, { doc: newSource } );
1210
+ // migratedCount++;
1211
+
1212
+ logger.info( { message: 'Document migrated successfully', newSource, documentId, storeId: oldSource.storeId, dateString: oldSource.dateString } );
1213
+ } catch ( error ) {
1214
+ const errorMsg = `Error migrating document ${hit._id}: ${error.message}`;
1215
+ errors.push( errorMsg );
1216
+ logger.error( { error: error, documentId: hit._id, function: 'vmsDataMigration' } );
1217
+ }
1218
+ }
1219
+
1220
+ return res.sendSuccess( {
1221
+ message: `Migration completed. ${migratedCount} document(s) migrated.`,
1222
+ migrated: migratedCount,
1223
+ total: hits.length,
1224
+ errors: errors.length > 0 ? errors : undefined,
1225
+ } );
1226
+ } catch ( error ) {
1227
+ logger.error( { error: error, message: req.query, function: 'vmsDataMigration' } );
1228
+ const err = error.message || 'Internal Server Error';
1229
+ return res.sendError( err, 500 );
1230
+ }
1231
+ }