tango-app-api-store-zone 3.3.1-beta.9 → 3.3.2

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,17 +1,21 @@
1
1
  import { logger } from 'tango-app-api-middleware';
2
2
  import * as cameraService from '../services/camera.service.js';
3
3
  import * as taggingService from '../services/tagging.service.js';
4
+ import * as customZoneTagService from '../services/customzonetag.service.js';
5
+ import * as customzonegrouping from '../services/customzonegrouping.service.js';
4
6
  import * as storeService from '../services/store.service.js';
5
7
  import * as clientService from '../services/client.service.js';
6
8
  import * as externalService from '../services/external.service.js';
7
- import { signedUrl, listFileByPath, fileUpload, insertOpenSearchData } from 'tango-app-api-middleware';
8
- import * as processedchecklistconfigService from '../services/processedchecklistconfig.services.js';
9
-
9
+ import { signedUrl, listFileByPath, fileUpload, insertOpenSearchData, download } from 'tango-app-api-middleware';
10
+ import * as checklistconfigService from '../services/checklistconfig.services.js';
10
11
  import axios from 'axios';
12
+ import _ from 'lodash';
13
+
11
14
  export const addCustomTag = async ( req, res ) => {
12
15
  try {
13
16
  let inputData = req.body;
14
17
  let taggingDetails = await taggingService.findOne( { clientId: inputData.clientId, storeId: inputData.storeId, tagName: inputData.tagName, isDeleted: false } );
18
+ let findgroup = await customZoneTagService.findOne( { clientId: inputData.clientId, storeId: inputData.storeId, tagName: inputData.tagName } );
15
19
  if ( !taggingDetails ) {
16
20
  let data = {
17
21
  clientId: inputData.clientId,
@@ -20,6 +24,7 @@ export const addCustomTag = async ( req, res ) => {
20
24
  rgbColor: inputData.rgbColor,
21
25
  productName: inputData.productName,
22
26
  rgbBorderColor: inputData.rgbBorderColor,
27
+ groupName: findgroup.groupName,
23
28
  };
24
29
  await taggingService.deleteMany( { clientId: inputData.clientId, storeId: inputData.storeId, tagName: inputData.tagName, isDeleted: true } );
25
30
  await taggingService.create( data );
@@ -174,12 +179,12 @@ export const customTagListv2 = async ( req, res ) => {
174
179
  let customTagList = [
175
180
  { tagName: 'Front', productName: 'tangoTraffic' },
176
181
  { tagName: 'Back', productName: 'tangoTraffic' },
177
- { tagName: 'Tracker-in', productName: 'tangoTraker' },
178
- { tagName: 'Tracker-out', productName: 'tangoTraker' },
182
+ { tagName: 'Tracker-in', productName: 'tangoTracker' },
183
+ { tagName: 'Tracker-out', productName: 'tangoTracker' },
179
184
  ];
180
185
 
181
186
  // Step 2: Fetch store & client details in parallel
182
- const [ storeDetails, clientDetails ] = await Promise.all( [
187
+ const [ storeDetails, clientDetails, camList ] = await Promise.all( [
183
188
  storeService.findOne(
184
189
  { storeId: req.query.storeId },
185
190
  { product: 1 },
@@ -188,6 +193,10 @@ export const customTagListv2 = async ( req, res ) => {
188
193
  { clientId: req.query.clientId },
189
194
  { featureConfigs: 1 },
190
195
  ),
196
+ cameraService.find(
197
+ { storeId: req.query.storeId, isActivated: true, isUp: true },
198
+ { streamName: 1 },
199
+ ),
191
200
  ] );
192
201
 
193
202
  // Step 3: Add conditional tags
@@ -209,7 +218,7 @@ export const customTagListv2 = async ( req, res ) => {
209
218
  // Step 4: Fetch existing tags
210
219
  const tagInfo = await taggingService.find(
211
220
  { clientId: req.query.clientId, productName: req.query.selectedProduct },
212
- { tagName: 1, rgbColor: 1, rgbBorderColor: 1, productName: 1 },
221
+ { tagName: 1, rgbColor: 1, rgbBorderColor: 1, productName: 1, groupName: 1 },
213
222
  );
214
223
 
215
224
  // Merge all tags
@@ -220,7 +229,7 @@ export const customTagListv2 = async ( req, res ) => {
220
229
 
221
230
  // Step 6: Query counts per tag
222
231
  const tagNames = uniqueTags.map( ( t ) => t.tagName );
223
-
232
+ let activeCam = camList.map( ( data ) => data.streamName );
224
233
  const taggingDetails = await taggingService.aggregate( [
225
234
  {
226
235
  $match: {
@@ -229,6 +238,7 @@ export const customTagListv2 = async ( req, res ) => {
229
238
  storeId: req.query.storeId,
230
239
  coordinates: { $exists: true, $ne: [] },
231
240
  tagName: { $in: tagNames },
241
+ streamName: { $in: activeCam },
232
242
  },
233
243
  },
234
244
  { $group: { _id: '$tagName', count: { $sum: 1 } } },
@@ -248,28 +258,56 @@ export const customTagListv2 = async ( req, res ) => {
248
258
  'Tracker-out': { rgbColor: 'rgba(12, 195, 111, 0.5)', rgbBorderColor: 'rgb(22, 205, 125)' },
249
259
  };
250
260
 
261
+
251
262
  // Step 8: Final processing
252
263
  const finalTags = uniqueTags.map( ( tag ) => ( {
253
264
  tagName: tag.tagName,
254
265
  productName: tag.productName,
266
+ groupName: tag.groupName ? tag.groupName : '',
255
267
  count: countMap.get( tag.tagName ) || 0,
256
268
  rgbColor: tag.rgbColor || tagColors[tag.tagName]?.rgbColor,
257
269
  rgbBorderColor: tag.rgbBorderColor || tagColors[tag.tagName]?.rgbBorderColor,
258
270
  } ) );
271
+ const groupedData = finalTags.reduce( ( acc, item ) => {
272
+ if ( item.groupName === '' ) {
273
+ item.isGroup = false;
274
+ // If groupName is empty, push the item as a standalone object
275
+ acc.push( item );
276
+ } else {
277
+ // If groupName exists, find if we already started a group for it
278
+ let group = acc.find( ( i ) => i.isGroup && i.groupName === item.groupName );
279
+
280
+ if ( !group ) {
281
+ // Create a new group container
282
+ group = {
283
+ groupName: item.groupName,
284
+ isGroup: true, // flag to identify this is a group
285
+ children: [],
286
+ };
287
+ acc.push( group );
288
+ }
289
+
290
+ // Push the current item into the group's children
291
+ group.children.push( item );
292
+ }
293
+ return acc;
294
+ }, [] );
259
295
 
260
296
 
261
297
  // Step 9: Sort by count (desc)
262
- finalTags.sort( ( a, b ) => b.count - a.count );
298
+ groupedData.sort( ( a, b ) => b.count - a.count );
299
+
263
300
  if ( req.query.selectedProduct && req.query.selectedProduct === 'tangoTrax' ) {
264
301
  let Query = [ {
265
302
  $match: {
266
303
  client_id: req.query.clientId,
304
+ publish: true,
267
305
  checkListType: { $ne: 'custom' },
268
306
  },
269
307
  }, {
270
308
  $group: {
271
- _id: '$sourceCheckList_id',
272
- sourceCheckList_id: { $last: '$sourceCheckList_id' },
309
+ _id: '$_id',
310
+ sourceCheckList_id: { $last: '$_id' },
273
311
  tagName: { $last: '$checkListName' },
274
312
  },
275
313
  },
@@ -299,11 +337,25 @@ export const customTagListv2 = async ( req, res ) => {
299
337
  ], as: 'cameraList',
300
338
  },
301
339
  },
302
-
303
340
  {
304
341
  $project: {
305
342
  tagName: 1,
306
343
  type: 'checklist',
344
+ cameraList: {
345
+ $filter: {
346
+ input: '$cameraList',
347
+ as: 'item',
348
+ cond: {
349
+ $eq: [ '$$item.storeId', req.query.storeId ],
350
+ },
351
+ },
352
+ },
353
+ },
354
+ },
355
+ {
356
+ $project: {
357
+ tagName: 1,
358
+ type: 1,
307
359
  count: { $size: '$cameraList' },
308
360
  },
309
361
  },
@@ -314,8 +366,8 @@ export const customTagListv2 = async ( req, res ) => {
314
366
  },
315
367
 
316
368
  ];
317
- let getChecklistData = await processedchecklistconfigService.aggregate( Query );
318
- if ( finalTags&&finalTags.length>0 ) {
369
+ let getChecklistData = await checklistconfigService.aggregate( Query );
370
+ if ( finalTags && finalTags.length > 0 ) {
319
371
  let merged = getChecklistData.map( ( item ) => {
320
372
  let match = finalTags.find( ( a ) => a.tagName === item.tagName );
321
373
  if ( match ) {
@@ -323,7 +375,305 @@ export const customTagListv2 = async ( req, res ) => {
323
375
  }
324
376
  return item;
325
377
  } );
378
+ // also include any arr1 items not in arr2
379
+ finalTags.forEach( ( a ) => {
380
+ if ( !merged.find( ( m ) => m.tagName === a.tagName ) ) {
381
+ merged.push( a );
382
+ }
383
+ } );
384
+ merged.sort( ( a, b ) => b.count - a.count );
385
+ return res.sendSuccess( merged );
386
+ } else {
387
+ return res.sendSuccess( getChecklistData );
388
+ }
389
+ } else {
390
+ return res.sendSuccess( groupedData );
391
+ }
392
+ } catch ( e ) {
393
+ logger.error( { error: e, function: 'customTagList' } );
394
+ return res.sendError( e, 500 );
395
+ }
396
+ };
397
+ export const customTagListv3 = async ( req, res ) => {
398
+ try {
399
+ // Step 1: Base tag list
400
+ let customTagList = [
401
+ { tagName: 'Front', productName: 'tangoTraffic' },
402
+ { tagName: 'Back', productName: 'tangoTraffic' },
403
+ { tagName: 'Tracker-in', productName: 'tangoTracker' },
404
+ { tagName: 'Tracker-out', productName: 'tangoTracker' },
405
+ ];
406
+
407
+ // Step 2: Fetch store & client details in parallel
408
+ const [ storeDetails, clientDetails, camList ] = await Promise.all( [
409
+ storeService.findOne(
410
+ { storeId: req.query.storeId },
411
+ { product: 1 },
412
+ ),
413
+ clientService.findOne(
414
+ { clientId: req.query.clientId },
415
+ { featureConfigs: 1 },
416
+ ),
417
+ cameraService.find(
418
+ { storeId: req.query.storeId, isActivated: true, isUp: true },
419
+ { streamName: 1 },
420
+ ),
421
+ ] );
422
+
423
+ // Step 3: Add conditional tags
424
+ if ( clientDetails?.featureConfigs?.isExcludedArea ) {
425
+ customTagList.push( { tagName: 'Excluded Area', productName: 'tangoTraffic' } );
426
+ }
427
+ if ( clientDetails?.featureConfigs?.isPasserByData ) {
428
+ customTagList.push( { tagName: 'Passer By', productName: 'tangoTraffic' } );
429
+ }
430
+ if ( storeDetails?.product?.includes( 'tangoZone' ) ) {
431
+ customTagList.push(
432
+ { tagName: 'Entry/Exit', productName: 'tangoZone' },
433
+ { tagName: 'Billing', productName: 'tangoZone' },
434
+ );
435
+ }
436
+ if ( req.query.selectedProduct && req.query.selectedProduct != '' ) {
437
+ customTagList = customTagList.filter( ( ele ) => ele.productName === req.query.selectedProduct );
438
+ }
439
+ // Step 4: Fetch existing tags
440
+ const tagInfo = await taggingService.find(
441
+ { clientId: req.query.clientId, productName: req.query.selectedProduct },
442
+ { tagName: 1, rgbColor: 1, rgbBorderColor: 1, productName: 1, groupName: 1 },
443
+ );
444
+
445
+ // Merge all tags
446
+ const mergedTags = [ ...customTagList, ...tagInfo ];
447
+ const uniqueTags = [ ...new Map( mergedTags.map( ( tag ) => [ tag.tagName, tag ] ) ).values() ];
448
+
449
+ // Step 6: Query counts per tag
450
+ const tagNames = uniqueTags.map( ( t ) => t.tagName );
451
+ let activeCam = camList.map( ( data ) => data.streamName );
452
+ const taggingDetails = await taggingService.aggregate( [
453
+ {
454
+ $match: {
455
+ productName: req.query.selectedProduct,
456
+ clientId: req.query.clientId,
457
+ storeId: req.query.storeId,
458
+ coordinates: { $exists: true, $ne: [] },
459
+ tagName: { $in: tagNames },
460
+ streamName: { $in: activeCam },
461
+ },
462
+ },
463
+ { $group: { _id: '$tagName', count: { $sum: 1 } } },
464
+ ] );
465
+
466
+ const countMap = new Map( taggingDetails.map( ( t ) => [ t._id, t.count ] ) );
467
+
468
+ // Step 7: Tag colors config (instead of many ifs)
469
+ const tagColors = {
470
+ 'Front': { rgbColor: 'rgba(89, 80, 5, 0.5)', rgbBorderColor: 'rgb(99, 90, 15)' },
471
+ 'Back': { rgbColor: 'rgba(94, 60, 107, 0.5)', rgbBorderColor: 'rgb(104, 70, 117)' },
472
+ 'Excluded Area': { rgbColor: 'rgba(186, 60, 214, 0.5)', rgbBorderColor: 'rgb(196, 70, 224)' },
473
+ 'Passer By': { rgbColor: 'rgba(81, 153, 247, 0.5)', rgbBorderColor: 'rgb(91, 163, 257)' },
474
+ 'Entry/Exit': { rgbColor: 'rgba(224, 43, 170, 0.5)', rgbBorderColor: 'rgb(234, 53, 180)' },
475
+ 'Billing': { rgbColor: 'rgba(193, 214, 114, 0.5)', rgbBorderColor: 'rgb(203, 224, 124)' },
476
+ 'Tracker-in': { rgbColor: 'rgba(123, 95, 105, 0.5)', rgbBorderColor: 'rgb(133, 105, 115)' },
477
+ 'Tracker-out': { rgbColor: 'rgba(12, 195, 111, 0.5)', rgbBorderColor: 'rgb(22, 205, 125)' },
478
+ };
479
+
480
+
481
+ // Step 8: Final processing
482
+ const finalTags = uniqueTags.map( ( tag ) => ( {
483
+ tagName: tag.tagName,
484
+ productName: tag.productName,
485
+ groupName: tag.groupName ? tag.groupName : '',
486
+ count: countMap.get( tag.tagName ) || 0,
487
+ rgbColor: tag.rgbColor || tagColors[tag.tagName]?.rgbColor,
488
+ rgbBorderColor: tag.rgbBorderColor || tagColors[tag.tagName]?.rgbBorderColor,
489
+ } ) );
490
+
491
+
492
+ console.log( '🚀 ~ customTagListv3 ~ mergedTags:', mergedTags );
493
+
494
+ if ( req.query.selectedProduct === 'tangoZone' ) {
495
+ const zonefinalTags = await customZoneTagService.aggregate( [
496
+ {
497
+ $match: {
498
+ clientId: req.query.clientId,
499
+ productName: req.query.selectedProduct,
500
+ },
501
+ },
502
+
503
+ // Count lookup
504
+ {
505
+ $lookup: {
506
+ from: 'taggings',
507
+ let: { tagName: '$tagName', clientId: '$clientId' },
508
+ pipeline: [
509
+ {
510
+ $match: {
511
+ $expr: {
512
+ $and: [
513
+ { $eq: [ '$tagName', '$$tagName' ] },
514
+ { $eq: [ '$clientId', '$$clientId' ] },
515
+ { $in: [ '$streamName', activeCam ] },
516
+ { $ne: [ '$coordinates', [] ] },
517
+ ],
518
+ },
519
+ },
520
+ },
521
+ { $count: 'count' },
522
+ ],
523
+ as: 'tagsCount',
524
+ },
525
+ },
526
+
527
+ // Add count
528
+ {
529
+ $addFields: {
530
+ count: {
531
+ $ifNull: [ { $arrayElemAt: [ '$tagsCount.count', 0 ] }, 0 ],
532
+ },
533
+ },
534
+ },
535
+
536
+ {
537
+ $project: {
538
+ _id: 0,
539
+ tagName: 1,
540
+ productName: 1,
541
+ groupName: 1,
542
+ rgbColor: 1,
543
+ rgbBorderColor: 1,
544
+ count: 1,
545
+ },
546
+ },
547
+ {
548
+ $sort: {
549
+ count: -1,
550
+ },
551
+ },
552
+
553
+ // Split grouped vs ungrouped
554
+ {
555
+ $facet: {
556
+ grouped: [
557
+ { $match: { groupName: { $nin: [ null, '' ] } } },
558
+ {
559
+ $group: {
560
+ _id: '$groupName',
561
+ children: { $push: '$$ROOT' },
562
+ },
563
+ },
564
+ {
565
+ $project: {
566
+ _id: 0,
567
+ groupName: '$_id',
568
+ isGroup: { $literal: true },
569
+ children: 1,
570
+ },
571
+ },
572
+ ],
573
+
574
+ ungrouped: [
575
+ { $match: { groupName: { $in: [ null, '' ] } } },
576
+ {
577
+ $addFields: { isGroup: { $literal: false } },
578
+ },
579
+ ],
580
+ },
581
+ },
582
+
583
+ // Merge results
584
+ {
585
+ $project: {
586
+ result: { $concatArrays: [ '$ungrouped', '$grouped' ] },
587
+ },
588
+ },
589
+
590
+ { $unwind: '$result' },
591
+ { $replaceRoot: { newRoot: '$result' } },
592
+ ] );
593
+
594
+
595
+ return res.sendSuccess( zonefinalTags );
596
+ }
597
+
598
+
599
+ if ( req.query.selectedProduct && req.query.selectedProduct === 'tangoTrax' ) {
600
+ let Query = [ {
601
+ $match: {
602
+ client_id: req.query.clientId,
603
+ publish: true,
604
+ checkListType: { $ne: 'custom' },
605
+ },
606
+ }, {
607
+ $group: {
608
+ _id: '$_id',
609
+ sourceCheckList_id: { $last: '$_id' },
610
+ tagName: { $last: '$checkListName' },
611
+ },
612
+ },
613
+ {
614
+ $lookup: {
615
+ from: 'cameras',
616
+ let: { checkListName: '$tagName' },
617
+ pipeline: [
618
+ {
619
+ $match: {
620
+ $expr: {
621
+ $anyElementTrue: {
622
+ $map: {
623
+ input: { $ifNull: [ '$taggedChecklist', [] ] }, // ✅ default to empty array
624
+ as: 'tc',
625
+ in: { $eq: [ '$$tc.checkListName', '$$checkListName' ] },
626
+ },
627
+ },
628
+ },
629
+ },
630
+ },
631
+ {
632
+ $project: {
633
+ storeId: 1,
634
+ },
635
+ },
636
+ ], as: 'cameraList',
637
+ },
638
+ },
639
+ {
640
+ $project: {
641
+ tagName: 1,
642
+ type: 'checklist',
643
+ cameraList: {
644
+ $filter: {
645
+ input: '$cameraList',
646
+ as: 'item',
647
+ cond: {
648
+ $eq: [ '$$item.storeId', req.query.storeId ],
649
+ },
650
+ },
651
+ },
652
+ },
653
+ },
654
+ {
655
+ $project: {
656
+ tagName: 1,
657
+ type: 1,
658
+ count: { $size: '$cameraList' },
659
+ },
660
+ },
661
+ {
662
+ $sort: {
663
+ count: -1,
664
+ },
665
+ },
326
666
 
667
+ ];
668
+ let getChecklistData = await checklistconfigService.aggregate( Query );
669
+ if ( finalTags && finalTags.length > 0 ) {
670
+ let merged = getChecklistData.map( ( item ) => {
671
+ let match = finalTags.find( ( a ) => a.tagName === item.tagName );
672
+ if ( match ) {
673
+ return { ...item, ...match, count: item.count + match.count }; // add counts
674
+ }
675
+ return item;
676
+ } );
327
677
  // also include any arr1 items not in arr2
328
678
  finalTags.forEach( ( a ) => {
329
679
  if ( !merged.find( ( m ) => m.tagName === a.tagName ) ) {
@@ -402,12 +752,12 @@ export const tagging = async ( req, res ) => {
402
752
  ...taggingDetails.toObject(),
403
753
  },
404
754
  oldData: {
405
- tagName: taggingDetails.tagName,
406
- streamName: taggingDetails.streamName,
755
+ TagName: taggingDetails.tagName,
756
+ StreamName: taggingDetails.streamName,
407
757
  },
408
758
  newData: {
409
- tagName: taggingDetails.tagName,
410
- streamName: taggingDetails.streamName,
759
+ TagName: taggingDetails.tagName,
760
+ StreamName: taggingDetails.streamName,
411
761
  },
412
762
  showTo: [ 'tango', 'client' ],
413
763
  };
@@ -485,7 +835,7 @@ export const tagging = async ( req, res ) => {
485
835
  'reidParameterType': 'default',
486
836
  'reidParameterValue': [ 0.6, 1, 0.6 ],
487
837
  'reidSuggesteParameter': false,
488
- 'reidSuggestedReduction': false,
838
+ 'reidSuggestedReduction': true,
489
839
  'reidThresholdHoliday': 0,
490
840
  'reidThresholdWeekDay': 0,
491
841
  'stage': '',
@@ -497,11 +847,25 @@ export const tagging = async ( req, res ) => {
497
847
  'walkInTimeSpent': 3,
498
848
  'isWalkInOneImg': false,
499
849
  'isAudit': true,
500
- 'isForceCombine': false,
850
+ 'isForceCombine': true,
501
851
  };
502
852
 
503
853
  await externalService.create( data );
504
854
  }
855
+
856
+ let cameraDetails = await cameraService.findOne( { _id: InputData.cameraId, streamName: InputData.streamName }, { productModule: 1 } );
857
+ if ( cameraDetails ) {
858
+ if ( !cameraDetails?.productModule.includes( 'tangoZone' ) ) {
859
+ cameraDetails.productModule.push( { productName: 'tangoZone', checked: true } );
860
+ cameraDetails.save();
861
+ } else {
862
+ let camIndex = cameraDetails.productModule.findIndex( ( ele ) => ele.productName == 'tangoZone' );
863
+ if ( !cameraDetails?.productModule?.[camIndex]?.checked ) {
864
+ cameraDetails.productModule[camIndex].checked = true;
865
+ cameraDetails.save();
866
+ }
867
+ }
868
+ }
505
869
  await updatezoneTagging( req, res );
506
870
  } catch ( e ) {
507
871
  logger.error( { error: e, function: 'tagging' } );
@@ -592,17 +956,17 @@ export const getCameraList = async ( req, res ) => {
592
956
  };
593
957
  export const getCameraListv2 = async ( req, res ) => {
594
958
  try {
595
- let cameraDetails = await cameraService.find( { clientId: req.query.clientId, storeId: req.query.storeId, isActivated: true, isUp: true }, { cameraNumber: 1, streamName: 1, isActivated: 1, isUp: 1, cameraName: 1, taggedChecklist: 1 } );
959
+ let cameraDetails = await cameraService.find( { clientId: req.body.clientId, storeId: req.body.storeId, isActivated: true, isUp: true }, { cameraNumber: 1, streamName: 1, isActivated: 1, isUp: 1, cameraName: 1, taggedChecklist: 1 } );
596
960
  if ( !cameraDetails.length ) {
597
961
  return res.sendError( 'no data found', 204 );
598
962
  }
599
963
  const folderPath = {
600
- file_path: `${req.query.storeId}/zone_base_images/`,
964
+ file_path: `${req.body.storeId}/zone_base_images/`,
601
965
  Bucket: JSON.parse( process.env.BUCKET ).zoneBaseImage, MaxKeys: 1000,
602
966
  };
603
967
  let fileList = await listFileByPath( folderPath );
604
968
  const TaggedfolderPath = {
605
- file_path: `${req.query.storeId}/zone_tagged_image/`,
969
+ file_path: `${req.body.storeId}/zone_tagged_image/`,
606
970
  Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage, MaxKeys: 1000,
607
971
  };
608
972
  let tagFileList = await listFileByPath( TaggedfolderPath );
@@ -615,7 +979,7 @@ export const getCameraListv2 = async ( req, res ) => {
615
979
  baseImg: '',
616
980
  tagImg: '',
617
981
  };
618
- let taggingDetails = await taggingService.find( { cameraId: camera._id, streamName: camera.streamName, clientId: req.query.clientId, isDeleted: false }, { tagName: 1, coordinates: 1 } );
982
+ let taggingDetails = await taggingService.find( { cameraId: camera._id, streamName: camera.streamName, clientId: req.body.clientId, isDeleted: false }, { tagName: 1, coordinates: 1 } );
619
983
  if ( taggingDetails.length ) {
620
984
  tagList = taggingDetails.filter( ( item ) => item.coordinates.length ).map( ( item ) => {
621
985
  if ( item.coordinates.length ) {
@@ -623,14 +987,13 @@ export const getCameraListv2 = async ( req, res ) => {
623
987
  }
624
988
  } );
625
989
  }
626
-
627
990
  if ( tagFileList.data.length ) {
628
991
  tagFileList.data.forEach( ( item ) => {
629
992
  if ( item.Key.length > 1 ) {
630
993
  let splitStream = item.Key.split( '/' );
631
994
  let getStream = splitStream[splitStream.length - 1].split( '.' );
632
995
 
633
- if ( getStream && getStream[0] == `${req.query.storeId}_${camera.streamName}` ) {
996
+ if ( getStream && getStream[0] == `${req.body.storeId}_${camera.streamName}` ) {
634
997
  tagPath = item.Key;
635
998
  }
636
999
  }
@@ -640,7 +1003,7 @@ export const getCameraListv2 = async ( req, res ) => {
640
1003
  fileList.data.forEach( ( ele ) => {
641
1004
  let splitStream = ele.Key.split( '/' );
642
1005
  let getStream = splitStream[splitStream.length - 1].split( '.' );
643
- if ( getStream && getStream[0] == `${req.query.storeId}_${camera.streamName}` ) {
1006
+ if ( getStream && getStream[0] == `${req.body.storeId}_${camera.streamName}` ) {
644
1007
  imgPath = ele.Key;
645
1008
  }
646
1009
  } );
@@ -662,6 +1025,7 @@ export const getCameraListv2 = async ( req, res ) => {
662
1025
  camera.baseImg = cameraBaseImage;
663
1026
  }
664
1027
  camera.tagging = tagList;
1028
+ camera.taggedCount = tagList.filter( ( ele ) => ele.tagName === req.body.tagName );
665
1029
  cameraDetails[index] = camera;
666
1030
  }
667
1031
  cameraDetails.sort( ( a, b ) => a.tagging?.length > b.tagging?.length ? -1 : 1 );
@@ -717,10 +1081,10 @@ export const updateTag = async ( req, res ) => {
717
1081
  tagName: req.body.tagName,
718
1082
  },
719
1083
  oldData: {
720
- tagName: req.body.existTag,
1084
+ TagName: req.body.existTag,
721
1085
  },
722
1086
  newData: {
723
- tagName: req.body.tagName,
1087
+ TagName: req.body.tagName,
724
1088
  },
725
1089
  showTo: [ 'tango', 'client' ],
726
1090
  };
@@ -746,19 +1110,24 @@ export const deleteTag = async ( req, res ) => {
746
1110
  if ( !taggingDetails.length ) {
747
1111
  return res.sendError( 'no data found', 204 );
748
1112
  }
749
- let enableDelete = false;
750
- if ( req.user.permission ) {
751
- let permissions = req.user.permission;
752
- permissions.forEach( ( permission ) => {
753
- if ( permission.featureName == 'analytics' ) {
754
- let product = permission.product.find( ( item ) => item.name == 'tangoZone' );
755
- if ( product ) {
756
- enableDelete = product.isDelete;
757
- }
758
- }
759
- } );
1113
+
1114
+ let tagNameDetails = await taggingService.findOne( { clientId: req.body.clientId, tagName: req.body.tagName, $expr: { $ne: [ { $size: '$coordinates' }, 0 ] } } );
1115
+ if ( tagNameDetails ) {
1116
+ return res.sendError( 'This zone tag is already mapped to other stores.', 400 );
760
1117
  }
761
- if ( req.user?.role == 'superadmin' || ( req.user?.role == 'admin' && enableDelete ) ) {
1118
+ // let enableDelete = false;
1119
+ // if ( req.user.permission ) {
1120
+ // let permissions = req.user.permission;
1121
+ // permissions.forEach( ( permission ) => {
1122
+ // if ( permission.featureName == 'analytics' ) {
1123
+ // let product = permission.product.find( ( item ) => item.name == 'tangoZone' );
1124
+ // if ( product ) {
1125
+ // enableDelete = product.isDelete;
1126
+ // }
1127
+ // }
1128
+ // } );
1129
+ // }
1130
+ if ( req.user?.role == 'superadmin' || ( req.user?.role == 'admin' ) ) {
762
1131
  await taggingService.deleteMany( { clientId: req.body.clientId, tagName: req.body.tagName } );
763
1132
  await externalService.deleteMany( { zoneName: req.body.tagName, clientId: req.body.clientId } );
764
1133
  } else {
@@ -946,44 +1315,51 @@ export const updatezoneTagging = async ( req, res ) => {
946
1315
 
947
1316
  export const updateOldData = async ( req, res ) => {
948
1317
  try {
949
- let tagDetails = await taggingService.find( {}, { storeId: 1 } );
1318
+ let tagDetails = await taggingService.find( { clientId: req.body.clientId, tagName: req.body.tagName }, { storeId: 1 } );
1319
+
1320
+
950
1321
  if ( tagDetails.length ) {
951
1322
  for ( let [ index, item ] of tagDetails.entries() ) {
952
1323
  req.body.storeId = item.storeId;
953
1324
  let camDetails = await getCamTaggingDetails( req, res );
954
- if ( !camDetails ) {
955
- logger.error( { message: 'no data', store: item.storeId } );
956
- }
957
- const response = await axios.post( JSON.parse( process.env.URL ).zoneTaggingLamdaUrl, camDetails );
958
- if ( response?.data?.status && response?.data?.status == 'success' ) {
959
- let fileData = {
960
- Key: `${req.body.storeId}/zonetagging/`,
961
- fileName: `${req.body.storeId}_zonetagging.json`,
962
- Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage,
963
- body: JSON.stringify( camDetails ),
964
- ContentType: 'application/json',
965
- };
966
- let upload = await fileUpload( fileData );
967
- if ( !upload.Key ) {
968
- logger.error( { message: `JSON Upload Error`, store: item.storeId } );
969
- }
970
- if ( index == 0 ) {
971
- res.sendSuccess( 'Zone Updated Successfully' );
1325
+ console.log( '🚀 ~ updateOldData ~ camDetails:', camDetails );
1326
+ if ( camDetails ) {
1327
+ const response = await axios.post( JSON.parse( process.env.URL ).zoneTaggingLamdaUrl, camDetails );
1328
+
1329
+ if ( response?.data?.status && response?.data?.status == 'success' ) {
1330
+ let fileData = {
1331
+ Key: `${req.body.storeId}/zonetagging/`,
1332
+ fileName: `${req.body.storeId}_zonetagging.json`,
1333
+ Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage,
1334
+ body: JSON.stringify( camDetails ),
1335
+ ContentType: 'application/json',
1336
+ };
1337
+
1338
+ let upload = await fileUpload( fileData );
1339
+ if ( !upload.Key ) {
1340
+ logger.error( { message: `JSON Upload Error`, store: item.storeId } );
1341
+ }
1342
+ } else {
1343
+ logger.error( { message: 'no data', store: item.storeId } );
1344
+ return res.sendError( 'something went wrong', 500 );
972
1345
  }
973
- } else {
974
- logger.error( { message: 'no data', store: item.storeId } );
975
- // return res.sendError( 'something went wrong', 500 );
1346
+ }
1347
+ if ( index == tagDetails.length-1 ) {
1348
+ res.sendSuccess( 'Zone Updated Successfully' );
976
1349
  }
977
1350
  }
1351
+ } else {
1352
+ res.sendSuccess( 'Zone Updated Successfully' );
978
1353
  }
979
1354
  } catch ( e ) {
1355
+ console.log( '🚀 ~ updateOldData ~ e:', e );
980
1356
  logger.error( { error: e, function: 'updateOldData' } );
981
1357
  return res.sendError( e, 500 );
982
1358
  }
983
1359
  };
984
1360
  export async function updateCamera( req, res ) {
985
1361
  try {
986
- let findoneCheckList = await processedchecklistconfigService.findOne( {
1362
+ let findoneCheckList = await checklistconfigService.findOne( {
987
1363
  client_id: req.body.clientId, checkListName: req.body.selectedZone,
988
1364
  } );
989
1365
 
@@ -1041,3 +1417,1344 @@ async function updateJsonFile( req, res ) {
1041
1417
  }
1042
1418
  }
1043
1419
  };
1420
+
1421
+ export const getCameraStreamList = async ( req, res ) => {
1422
+ try {
1423
+ let cameraDetails = await cameraService.find( { clientId: req.body.clientId, storeId: req.body.storeId, streamName: req.body.streamName }, { cameraNumber: 1, streamName: 1, isActivated: 1, isUp: 1, cameraName: 1 } );
1424
+ if ( !cameraDetails.length ) {
1425
+ return res.sendError( 'no data found', 204 );
1426
+ }
1427
+ const folderPath = {
1428
+ file_path: `${req.body.storeId}/zone_base_images/`,
1429
+ Bucket: JSON.parse( process.env.BUCKET ).zoneBaseImage, MaxKeys: 1000,
1430
+ };
1431
+ let fileList = await listFileByPath( folderPath );
1432
+ const TaggedfolderPath = {
1433
+ file_path: `${req.body.storeId}/zone_tagged_image/`,
1434
+ Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage, MaxKeys: 1000,
1435
+ };
1436
+ let tagFileList = await listFileByPath( TaggedfolderPath );
1437
+ for ( let [ index, camera ] of cameraDetails.entries() ) {
1438
+ let tagList = [];
1439
+ let tagPath;
1440
+ let imgPath;
1441
+ camera = {
1442
+ ...camera._doc,
1443
+ baseImg: '',
1444
+ tagImg: '',
1445
+ };
1446
+ let taggingDetails = await taggingService.find( { cameraId: camera._id, streamName: camera.streamName, clientId: req.body.clientId, isDeleted: false }, { tagName: 1, coordinates: 1 } );
1447
+ if ( taggingDetails.length ) {
1448
+ tagList = taggingDetails.filter( ( item ) => item.coordinates.length ).map( ( item ) => {
1449
+ if ( item.coordinates.length ) {
1450
+ return { tagName: item.tagName, color: item.coordinates[0].color };
1451
+ }
1452
+ } );
1453
+ }
1454
+
1455
+ if ( tagFileList.data.length ) {
1456
+ tagFileList.data.forEach( ( item ) => {
1457
+ if ( item.Key.length > 1 ) {
1458
+ let splitStream = item.Key.split( '/' );
1459
+ let getStream = splitStream[splitStream.length - 1].split( '.' );
1460
+
1461
+ if ( getStream && getStream[0] == `${req.body.storeId}_${camera.streamName}` ) {
1462
+ tagPath = item.Key;
1463
+ }
1464
+ }
1465
+ } );
1466
+ }
1467
+ if ( fileList?.data.length ) {
1468
+ fileList.data.forEach( ( ele ) => {
1469
+ let splitStream = ele.Key.split( '/' );
1470
+ let getStream = splitStream[splitStream.length - 1].split( '.' );
1471
+ if ( getStream && getStream[0] == `${req.body.storeId}_${camera.streamName}` ) {
1472
+ imgPath = ele.Key;
1473
+ }
1474
+ } );
1475
+ }
1476
+ if ( tagPath ) {
1477
+ const params = {
1478
+ file_path: tagPath,
1479
+ Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage,
1480
+ };
1481
+ const cameraTagImage = await signedUrl( params );
1482
+ camera.tagImg = cameraTagImage;
1483
+ }
1484
+ if ( imgPath ) {
1485
+ const baseParams = {
1486
+ file_path: imgPath,
1487
+ Bucket: JSON.parse( process.env.BUCKET ).zoneBaseImage,
1488
+ };
1489
+ const cameraBaseImage = await signedUrl( baseParams );
1490
+ camera.baseImg = cameraBaseImage;
1491
+ }
1492
+ camera.tagging = tagList;
1493
+ cameraDetails[index] = camera;
1494
+ }
1495
+ cameraDetails.sort( ( a, b ) => a.tagging?.length > b.tagging?.length ? -1 : 1 );
1496
+ return res.sendSuccess( cameraDetails );
1497
+ } catch ( e ) {
1498
+ logger.error( { error: e, function: 'getCameraList' } );
1499
+ return res.sendError( e, 500 );
1500
+ }
1501
+ };
1502
+
1503
+
1504
+ // setting config - zone - get tagging details
1505
+ export const getZoneTaggingDetails = async ( req, res ) => {
1506
+ try {
1507
+ const {
1508
+ clientId,
1509
+ searchValue,
1510
+ sortColumName,
1511
+ sortBy,
1512
+ limit,
1513
+ offset = 1,
1514
+ export: isExport,
1515
+ } = req.body;
1516
+
1517
+ if ( !clientId ) {
1518
+ return res.sendError( 'clientId is required', 400 );
1519
+ }
1520
+
1521
+ const matchStage = {
1522
+ clientId,
1523
+ };
1524
+ let customTags = await customZoneTagService.findOne( { clientId: clientId } );
1525
+ const alltotalGroupCount = await customzonegrouping.count( { clientId } );
1526
+ if ( !customTags ) {
1527
+ return res.sendSuccess( { result: [], totalGroupCount: alltotalGroupCount } );
1528
+ }
1529
+ const searchStage = searchValue ?
1530
+ {
1531
+ $or: [
1532
+ { tagName: { $regex: searchValue, $options: 'i' } },
1533
+ { groupName: { $regex: searchValue, $options: 'i' } },
1534
+ ],
1535
+ } :
1536
+ null;
1537
+
1538
+ const pipeline = [
1539
+ { $match: matchStage },
1540
+ {
1541
+ $lookup: {
1542
+ from: 'taggings',
1543
+ let: { tagName: '$tagName' },
1544
+ pipeline: [
1545
+ {
1546
+ $match: {
1547
+ $expr: {
1548
+ $and: [
1549
+ { $eq: [ '$tagName', '$$tagName' ] },
1550
+ { $eq: [ '$clientId', clientId ] },
1551
+ { $ne: [ '$coordinates', [] ] },
1552
+ ],
1553
+ },
1554
+ },
1555
+ },
1556
+ {
1557
+ $group: {
1558
+ _id: '$storeId',
1559
+ count: { $sum: 1 },
1560
+ },
1561
+ },
1562
+ ],
1563
+ as: 'tagsCount',
1564
+ },
1565
+ },
1566
+ {
1567
+ $project: {
1568
+ clientId: 1,
1569
+ tagName: 1,
1570
+ groupName: 1,
1571
+ storesTaggedCount: { $size: '$tagsCount' },
1572
+ },
1573
+ },
1574
+ {
1575
+ $facet: {
1576
+ totalCount: [
1577
+ ...( searchStage ? [ { $match: searchStage } ] : [] ),
1578
+ { $count: 'count' },
1579
+ ],
1580
+
1581
+ filteredData: [
1582
+ ...( searchStage ? [ { $match: searchStage } ] : [] ),
1583
+ ...( sortColumName && sortBy ?
1584
+ [ { $sort: { [sortColumName]: sortBy } } ] :
1585
+ [] ),
1586
+ ...( isExport ?
1587
+ [] :
1588
+ limit ? [
1589
+ { $skip: ( offset - 1 ) * limit },
1590
+ { $limit: Number( limit ) },
1591
+ ] : [] ),
1592
+ ],
1593
+ },
1594
+ },
1595
+ ];
1596
+
1597
+ const [ result ] = await customZoneTagService.aggregate( pipeline );
1598
+
1599
+ const totalCount = result.totalCount[0]?.count || 0;
1600
+ if ( totalCount === 0 ) {
1601
+ return res.sendError( 'No data', 204 );
1602
+ }
1603
+ const zoneList = result.filteredData;
1604
+
1605
+ // Get total count of groups for tabs (actual count, not filtered by search)
1606
+ const totalGroupCount = await customzonegrouping.count( { clientId } );
1607
+
1608
+ if ( isExport && zoneList.length ) {
1609
+ const exportdata = zoneList.map( ( z ) => ( {
1610
+ 'Zone Name': z.tagName || '--',
1611
+ 'Zone Groups': z.groupName || '--',
1612
+ 'Stores tagged Count': z.storesTaggedCount || 0,
1613
+ } ) );
1614
+ await download( exportdata, res );
1615
+ return;
1616
+ }
1617
+
1618
+ return res.sendSuccess( {
1619
+ result: zoneList,
1620
+ totalCount,
1621
+ totalGroupCount,
1622
+ } );
1623
+ } catch ( e ) {
1624
+ logger.error( { error: e, function: 'getZoneTaggingDetails' } );
1625
+ return res.sendError( e, 500 );
1626
+ }
1627
+ // try {
1628
+ // const inputData = req.body;
1629
+ // let Query = [
1630
+ // {
1631
+ // $match: {
1632
+ // clientId: inputData.clientId,
1633
+ // },
1634
+ // },
1635
+ // {
1636
+ // $lookup: {
1637
+ // from: 'taggings',
1638
+ // let: { tagName: '$tagName' },
1639
+ // pipeline: [
1640
+ // {
1641
+ // $match: {
1642
+ // $expr: {
1643
+ // $and: [
1644
+ // {
1645
+ // $eq: [ '$tagName', '$$tagName' ],
1646
+ // },
1647
+ // {
1648
+ // $eq: [ '$clientId', inputData.clientId ],
1649
+ // },
1650
+ // ],
1651
+ // },
1652
+ // },
1653
+ // },
1654
+ // ], as: 'tagsCount',
1655
+ // },
1656
+ // },
1657
+ // {
1658
+ // $project: {
1659
+ // clientId: 1,
1660
+ // tagName: 1,
1661
+ // groupName: 1,
1662
+ // storesTaggedCount: { $size: '$tagsCount' },
1663
+ // },
1664
+ // },
1665
+ // ];
1666
+ // if ( req.body.searchValue && req.body.searchValue !== '' ) {
1667
+ // Query.push( {
1668
+ // $match: {
1669
+ // $or: [
1670
+ // { tagName: { $regex: req.body.searchValue, $options: 'i' } },
1671
+ // { groupName: { $regex: req.body.searchValue, $options: 'i' } },
1672
+ // ],
1673
+ // },
1674
+ // } );
1675
+ // }
1676
+
1677
+ // if ( req.body.sortColumName && req.body.sortColumName !== '' && req.body.sortBy ) {
1678
+ // Query.push( {
1679
+ // $sort: { [req.body.sortColumName]: req.body.sortBy },
1680
+ // } );
1681
+ // }
1682
+
1683
+ // const totalCount = await customZoneTagService.aggregate( Query );
1684
+ // if ( req.body.limit && req.body.offset && !req.body.export ) {
1685
+ // Query.push(
1686
+ // { $skip: ( req.body.offset - 1 ) * req.body.limit },
1687
+ // { $limit: Number( req.body.limit ) },
1688
+ // );
1689
+ // }
1690
+ // const zoneList = await customZoneTagService.aggregate( Query );
1691
+
1692
+ // if ( req.body.export && zoneList.length > 0 ) {
1693
+ // const exportdata = [];
1694
+ // zoneList.forEach( ( element ) => {
1695
+ // const data = {
1696
+ // 'Zone Name': element.tagName || '--',
1697
+ // 'Zone Groups': element.groupName || '--',
1698
+ // 'Stores tagged Count': element.storesTaggedCount || 0,
1699
+ // };
1700
+ // exportdata.push( data );
1701
+ // } );
1702
+ // await download( exportdata, res );
1703
+ // return;
1704
+ // }
1705
+
1706
+ // return res.sendSuccess( {
1707
+ // result: zoneList,
1708
+ // count: totalCount.length,
1709
+ // } );
1710
+ // } catch ( e ) {
1711
+ // logger.error( { error: e, function: 'getZoneTaggingDetails' } );
1712
+ // console.error( 'getZoneTaggingDetails error:', e );
1713
+ // return res.sendError( e, 500 );
1714
+ // }
1715
+ };
1716
+
1717
+ export const addZoneCustomTag = async ( req, res ) => {
1718
+ try {
1719
+ const {
1720
+ clientId,
1721
+ tagName,
1722
+ groupName,
1723
+ productName,
1724
+ isExistingGroup,
1725
+ rgbColor,
1726
+ rgbBorderColor,
1727
+ _id,
1728
+ } = req.body;
1729
+
1730
+ if ( !clientId || !tagName ) {
1731
+ return res.sendError( 'clientId and zoneName are required', 400 );
1732
+ }
1733
+
1734
+ // Escape regex special characters in tagName to avoid invalid regex patterns
1735
+ const escapedTagName = tagName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
1736
+
1737
+ // Case-insensitive check for existing tag
1738
+ const existingTag = await customZoneTagService.findOne(
1739
+ { clientId, tagName: { $regex: `^${escapedTagName}$`, $options: 'i' }, _id: { $ne: _id } },
1740
+ );
1741
+
1742
+ if ( existingTag ) {
1743
+ return res.sendError( `zoneName "${tagName}" already exists for this client`, 409 );
1744
+ }
1745
+
1746
+ let groupDoc = null;
1747
+ if ( groupName ) {
1748
+ // Escape regex special characters in groupName to avoid invalid regex patterns
1749
+ const escapedGroupName = groupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
1750
+ // Case-insensitive check for existing group
1751
+ groupDoc = await customzonegrouping.findOne( { clientId, groupName: { $regex: `^${escapedGroupName}$`, $options: 'i' } } );
1752
+ }
1753
+
1754
+ if ( !isExistingGroup && groupDoc ) {
1755
+ return res.sendError( `groupName "${groupName}" already exists for this client`, 409 );
1756
+ }
1757
+
1758
+ /** Create Zone Tag */
1759
+ await customZoneTagService.create(
1760
+ [ {
1761
+ clientId,
1762
+ tagName,
1763
+ rgbColor,
1764
+ rgbBorderColor,
1765
+ groupName: groupName || null,
1766
+ productName,
1767
+ } ],
1768
+ );
1769
+
1770
+ /** Create or Update Zone Group */
1771
+ if ( groupName ) {
1772
+ if ( !groupDoc && !isExistingGroup ) {
1773
+ await customzonegrouping.create(
1774
+ [ {
1775
+ clientId,
1776
+ groupName,
1777
+ productName,
1778
+ zonesTagged: [ tagName ],
1779
+ } ],
1780
+ );
1781
+ } else if ( groupDoc && isExistingGroup ) {
1782
+ await customzonegrouping.updateOne(
1783
+ { _id: groupDoc._id },
1784
+ { $addToSet: { zonesTagged: tagName } },
1785
+ );
1786
+ }
1787
+ }
1788
+
1789
+ await taggingService.updateMany( { clientId, tagName }, { $set: { groupName } } );
1790
+
1791
+ logger.info( 'Zone Tag Created Successfully' );
1792
+ const logObj = {
1793
+ clientId: clientId,
1794
+ userName: req.user?.userName,
1795
+ email: req.user?.email,
1796
+ date: new Date(),
1797
+ logType: 'zone',
1798
+ logSubType: 'addZoneCustomTag',
1799
+ changes: [ `${tagName} custom zone tag` ],
1800
+ eventType: 'create',
1801
+ showTo: [ 'client', 'tango' ],
1802
+ };
1803
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
1804
+
1805
+ return res.sendSuccess( 'Zone Tag Created Successfully' );
1806
+ } catch ( e ) {
1807
+ logger.error( { error: e, function: 'addZoneCustomTag' } );
1808
+ return res.sendError( e.message || e, 500 );
1809
+ }
1810
+ };
1811
+
1812
+ export const uploadBulkZoneTag = async ( req, res ) => {
1813
+ try {
1814
+ const zonesArray = req.body;
1815
+ const clientId = req.bulkZoneClientId;
1816
+
1817
+ if ( !zonesArray || !Array.isArray( zonesArray ) || zonesArray.length === 0 ) {
1818
+ return res.sendError( 'zone-config file must not be empty', 400 );
1819
+ }
1820
+
1821
+ if ( !clientId ) {
1822
+ return res.sendError( 'clientId is required', 400 );
1823
+ }
1824
+
1825
+ const zoneTagDataList = [];
1826
+ const zoneGroupDataMap = new Map();
1827
+ const createdTagNames = [];
1828
+ // Map to store user-provided groupName (any case) -> actual groupName from DB (preserving existing case)
1829
+ const groupNameMapping = new Map();
1830
+
1831
+ // First pass: Check for existing groups and create mapping
1832
+ const uniqueGroupNames = [ ...new Set( zonesArray.map( ( zone ) => zone.groupName ).filter( ( name ) => name && name !== '' && name !== null ) ) ];
1833
+
1834
+ for ( const userGroupName of uniqueGroupNames ) {
1835
+ // Escape regex special characters in group name to avoid invalid regex patterns
1836
+ const escapedUserGroupName = userGroupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
1837
+
1838
+ // Case-insensitive check for existing group
1839
+ const existingGroupDoc = await customzonegrouping.findOne( {
1840
+ clientId,
1841
+ groupName: { $regex: `^${escapedUserGroupName}$`, $options: 'i' },
1842
+ } );
1843
+
1844
+ if ( existingGroupDoc ) {
1845
+ // Group exists - use the existing group's case
1846
+ groupNameMapping.set( userGroupName.toLowerCase(), existingGroupDoc.groupName );
1847
+ } else {
1848
+ // New group - use user-provided case
1849
+ groupNameMapping.set( userGroupName.toLowerCase(), userGroupName );
1850
+ }
1851
+ }
1852
+
1853
+ // Process each zone in the array
1854
+ for ( const zone of zonesArray ) {
1855
+ // Determine the correct groupName to use (existing case if group exists, user case if new)
1856
+ let normalizedGroupName = null;
1857
+ if ( zone.groupName && zone.groupName !== '' && zone.groupName !== null ) {
1858
+ normalizedGroupName = groupNameMapping.get( zone.groupName.toLowerCase() ) || zone.groupName;
1859
+ }
1860
+
1861
+ const zoneTagData = {
1862
+ clientId: clientId,
1863
+ tagName: zone.tagName,
1864
+ rgbColor: zone.rgbColor,
1865
+ rgbBorderColor: zone.rgbBorderColor,
1866
+ groupName: normalizedGroupName,
1867
+ productName: zone.productName,
1868
+ };
1869
+ zoneTagDataList.push( zoneTagData );
1870
+ createdTagNames.push( zone.tagName );
1871
+
1872
+ // Prepare zone group data if groupName is provided
1873
+ if ( normalizedGroupName ) {
1874
+ // Use normalized groupName (existing case or user case)
1875
+ if ( !zoneGroupDataMap.has( normalizedGroupName ) ) {
1876
+ zoneGroupDataMap.set( normalizedGroupName, {
1877
+ clientId: clientId,
1878
+ groupName: normalizedGroupName,
1879
+ productName: zone.productName,
1880
+ zonesTagged: new Set(),
1881
+ } );
1882
+ }
1883
+
1884
+ // Collect all tagNames for this group so that we can append them
1885
+ const groupEntry = zoneGroupDataMap.get( normalizedGroupName );
1886
+ groupEntry.zonesTagged.add( zone.tagName );
1887
+ }
1888
+ }
1889
+
1890
+ // Create all zone tags
1891
+ await customZoneTagService.create( zoneTagDataList );
1892
+
1893
+ // Create zone groups (convert Map values to array)
1894
+ // const zoneGroupDataList = Array.from( zoneGroupDataMap.values() );
1895
+ // console.log( 'zoneGroupDataList', zoneGroupDataList );
1896
+ // if ( zoneGroupDataList.length > 0 ) {
1897
+ // for ( const groupData of zoneGroupDataList ) {
1898
+ // await customzonegrouping.create( groupData );
1899
+ // }
1900
+ // }
1901
+
1902
+ // Create or Update Zone Groups (following addZoneCustomTag pattern)
1903
+ const zoneGroupDataList = Array.from( zoneGroupDataMap.values() ).map( ( groupData ) => ( {
1904
+ clientId: groupData.clientId,
1905
+ groupName: groupData.groupName,
1906
+ productName: groupData.productName,
1907
+ zonesTagged: Array.from( groupData.zonesTagged || [] ),
1908
+ } ) );
1909
+
1910
+ if ( zoneGroupDataList.length > 0 ) {
1911
+ for ( const groupData of zoneGroupDataList ) {
1912
+ // Case-insensitive check for existing group (same as addZoneCustomTag)
1913
+ const escapedGroupName = groupData.groupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
1914
+ const groupDoc = await customzonegrouping.findOne( {
1915
+ clientId,
1916
+ groupName: { $regex: `^${escapedGroupName}$`, $options: 'i' },
1917
+ } );
1918
+
1919
+ if ( !groupDoc ) {
1920
+ // Create new group with all zones from the bulk file
1921
+ await customzonegrouping.create( [ {
1922
+ clientId: groupData.clientId,
1923
+ groupName: groupData.groupName,
1924
+ productName: groupData.productName,
1925
+ zonesTagged: groupData.zonesTagged,
1926
+ } ] );
1927
+ } else {
1928
+ // Group exists - add new zones to existing zonesTagged array (preserving existing mappings)
1929
+ await customzonegrouping.updateOne(
1930
+ { _id: groupDoc._id },
1931
+ { $addToSet: { zonesTagged: { $each: groupData.zonesTagged } } },
1932
+ );
1933
+ }
1934
+ }
1935
+ }
1936
+
1937
+ // Ensure tagging documents are also mapped to the correct groupName for newly uploaded zones
1938
+ // Use normalized groupName (existing case if group exists, user case if new)
1939
+ for ( const zone of zonesArray ) {
1940
+ if ( zone.groupName && zone.groupName !== '' && zone.groupName !== null ) {
1941
+ const normalizedGroupName = groupNameMapping.get( zone.groupName.toLowerCase() ) || zone.groupName;
1942
+ await taggingService.updateMany(
1943
+ { clientId, tagName: zone.tagName },
1944
+ { $set: { groupName: normalizedGroupName } },
1945
+ );
1946
+ }
1947
+ }
1948
+
1949
+
1950
+ logger.info( `Bulk Zone Tags Created Successfully: ${createdTagNames.length} tags` );
1951
+ const logObj = {
1952
+ clientId: clientId,
1953
+ userName: req.user?.userName,
1954
+ email: req.user?.email,
1955
+ date: new Date(),
1956
+ logType: 'zone',
1957
+ logSubType: 'addBulkZoneCustomTag',
1958
+ changes: createdTagNames.map( ( tagName ) => `${tagName} custom tag` ),
1959
+ eventType: 'create',
1960
+ showTo: [ 'client', 'tango' ],
1961
+ };
1962
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
1963
+ return res.sendSuccess( {
1964
+ message: `Zone Tags Created Successfully`,
1965
+ count: createdTagNames.length,
1966
+ } );
1967
+ } catch ( e ) {
1968
+ console.log( 'uploadBulkZoneTags error :', e );
1969
+ logger.error( { error: e, function: 'addBulkZoneCustomTag' } );
1970
+ return res.sendError( 'Failed to upload zone-config file', 500 );
1971
+ }
1972
+ };
1973
+
1974
+ export const updateZoneCustomTag = async ( req, res ) => {
1975
+ try {
1976
+ const { clientId, tagName, oldTag, isExistingGroup, oldGroupName, groupName } = req.body;
1977
+
1978
+ if ( !clientId || !tagName ) {
1979
+ return res.sendError( 'clientId, zoneName are required', 400 );
1980
+ }
1981
+
1982
+ let findQuery = {};
1983
+
1984
+ // Escape regex special characters in tag names to avoid invalid regex patterns
1985
+ const escapedTagName = tagName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
1986
+ const escapedOldTag = oldTag ? oldTag.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' ) : null;
1987
+ if ( oldTag !== undefined && oldTag !== null && tagName !== oldTag ) {
1988
+ findQuery = {
1989
+ clientId,
1990
+ tagName: { $regex: `^${escapedOldTag}$`, $options: 'i' },
1991
+ };
1992
+ } else {
1993
+ findQuery = {
1994
+ clientId,
1995
+ tagName: { $regex: `^${escapedTagName}$`, $options: 'i' },
1996
+ };
1997
+ }
1998
+ await externalService.updateMany( { zoneName: oldTag, clientId: req.body.clientId }, { zoneName: req.body.tagName } );
1999
+ updateOldData( req, res );
2000
+ let customZoneTagDetails = await customZoneTagService.findOne( findQuery );
2001
+
2002
+ if ( !customZoneTagDetails ) {
2003
+ return res.sendError( 'No data found', 400 );
2004
+ }
2005
+
2006
+ // Get the actual oldGroupName from the database record
2007
+ const actualOldGroupName = customZoneTagDetails.groupName || null;
2008
+ // Get the actual tag name currently in the database
2009
+ const actualTagNameInDb = customZoneTagDetails.tagName;
2010
+
2011
+ // Validate that the new tagName does not already exist for this client (case-insensitive)
2012
+ // Check if tagName is being changed (either via oldTag or if it's different from DB value)
2013
+ const isTagNameChanging = ( oldTag && tagName.toLowerCase() !== oldTag.toLowerCase() ) || ( actualTagNameInDb && tagName.toLowerCase() !== actualTagNameInDb.toLowerCase() );
2014
+
2015
+ if ( isTagNameChanging ) {
2016
+ const existingTagWithNewName = await customZoneTagService.findOne( {
2017
+ clientId,
2018
+ tagName: { $regex: `^${escapedTagName}$`, $options: 'i' },
2019
+ } );
2020
+
2021
+ // If a tag with the new name exists and it's not the same record we're updating, return error
2022
+ if ( existingTagWithNewName && existingTagWithNewName._id.toString() !== customZoneTagDetails._id.toString() ) {
2023
+ return res.sendError( `zoneName "${tagName}" already exists for this client`, 409 );
2024
+ }
2025
+ }
2026
+
2027
+ let query = {
2028
+ clientId,
2029
+ };
2030
+ let payload = { tagName };
2031
+ if ( oldTag && tagName !== oldTag ) {
2032
+ query.tagName = oldTag;
2033
+ } else {
2034
+ query.tagName = tagName;
2035
+ }
2036
+ if ( groupName ) {
2037
+ payload.groupName = groupName || null;
2038
+ } else {
2039
+ payload.groupName = null;
2040
+ }
2041
+
2042
+ // Determine the tag name to use for group operations
2043
+ // Use the actual tag name from database for removing from old group
2044
+ const tagNameForRemoval = actualTagNameInDb;
2045
+ const newTagNameForGroup = tagName;
2046
+
2047
+ // Handle group changes: remove from old group and add to new group
2048
+ // Compare actual old group name from DB with new group name (case-insensitive)
2049
+ const oldGroupNameStr = actualOldGroupName ? String( actualOldGroupName ).trim().toLowerCase() : null;
2050
+ const newGroupNameStr = groupName ? String( groupName ).trim().toLowerCase() : null;
2051
+
2052
+ if ( oldGroupNameStr !== newGroupNameStr ) {
2053
+ // Remove tag from old group if oldGroupName exists
2054
+ if ( actualOldGroupName ) {
2055
+ // Escape regex special characters in actualOldGroupName
2056
+ const escapedActualOldGroupName = actualOldGroupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
2057
+ const oldGroupDoc = await customzonegrouping.findOne(
2058
+ { clientId, groupName: { $regex: `^${escapedActualOldGroupName}$`, $options: 'i' } },
2059
+ );
2060
+ if ( oldGroupDoc ) {
2061
+ await customzonegrouping.updateOne(
2062
+ { _id: oldGroupDoc._id },
2063
+ { $pull: { zonesTagged: tagNameForRemoval } },
2064
+ );
2065
+ } else {
2066
+ }
2067
+ }
2068
+
2069
+ // Add tag to new group if groupName exists
2070
+ if ( groupName ) {
2071
+ // Escape regex special characters in groupName
2072
+ const escapedGroupName = groupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
2073
+ let newGroupDoc = await customzonegrouping.findOne(
2074
+ { clientId, groupName: { $regex: `^${escapedGroupName}$`, $options: 'i' } },
2075
+ );
2076
+
2077
+ if ( !newGroupDoc && !isExistingGroup ) {
2078
+ await customzonegrouping.create(
2079
+ [ {
2080
+ clientId,
2081
+ groupName,
2082
+ productName: req.body.productName,
2083
+ zonesTagged: [ newTagNameForGroup ],
2084
+ } ],
2085
+ );
2086
+ } else if ( newGroupDoc ) {
2087
+ // Add to existing group
2088
+ await customzonegrouping.updateOne(
2089
+ { _id: newGroupDoc._id },
2090
+ { $addToSet: { zonesTagged: newTagNameForGroup } },
2091
+ );
2092
+ }
2093
+ }
2094
+ } else if ( groupName && oldGroupNameStr === newGroupNameStr && oldTag && tagName !== oldTag ) {
2095
+ // If groupName is same (and not null) but tagName changed, update the tagName in the group's zonesTagged
2096
+ console.log( 'Group name same but tagName changed, updating group zonesTagged' );
2097
+ // Escape regex special characters in groupName
2098
+ const escapedGroupName = groupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
2099
+ const groupDoc = await customzonegrouping.findOne( { clientId, groupName: { $regex: `^${escapedGroupName}$`, $options: 'i' } } );
2100
+ if ( groupDoc ) {
2101
+ await customzonegrouping.updateOne(
2102
+ { _id: groupDoc._id },
2103
+ [
2104
+ {
2105
+ $set: {
2106
+ zonesTagged: {
2107
+ $setUnion: [
2108
+ {
2109
+ $filter: {
2110
+ input: '$zonesTagged',
2111
+ cond: { $ne: [ '$$this', actualTagNameInDb ] },
2112
+ },
2113
+ },
2114
+ [ tagName ],
2115
+ ],
2116
+ },
2117
+ },
2118
+ },
2119
+ ],
2120
+ );
2121
+ }
2122
+ }
2123
+ // Update tagging collection if groupName changed or tagName changed (similar to addZoneCustomTag)
2124
+ if ( oldTag && tagName !== oldTag ) {
2125
+ // If tagName changed, update tagging collection records with oldTag to have new tagName and groupName
2126
+ await taggingService.updateMany(
2127
+ { clientId, tagName: oldTag },
2128
+ { $set: { tagName, 'groupName': groupName || null, 'coordinates.$[].zoneName': tagName } },
2129
+ );
2130
+ } else if ( groupName !== oldGroupName ) {
2131
+ // If only groupName changed, update tagging collection with new groupName
2132
+ await taggingService.updateMany(
2133
+ { clientId, tagName },
2134
+ { $set: { 'groupName': groupName || null } },
2135
+ );
2136
+ }
2137
+
2138
+ let zonetag = await customZoneTagService.updateOne( query, { $set: payload } );
2139
+ const logObj = {
2140
+ clientId: req.body.clientId,
2141
+ userName: req.user?.userName,
2142
+ email: req.user?.email,
2143
+ date: new Date(),
2144
+ logType: 'zone',
2145
+ logSubType: 'updateZoneCustomTag',
2146
+ changes: [ `zoneName changed from ${oldTag} to ${tagName}` ],
2147
+ eventType: 'update',
2148
+ previous: {
2149
+ tagName: oldTag,
2150
+ },
2151
+ current: {
2152
+ tagName: tagName,
2153
+ },
2154
+ oldData: {
2155
+ TagName: oldTag,
2156
+ },
2157
+ newData: {
2158
+ TagName: tagName,
2159
+ },
2160
+ showTo: [ 'tango', 'client' ],
2161
+ };
2162
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2163
+ if ( zonetag.modifiedCount || zonetag.matchedCount ) {
2164
+ logger.info( 'ZoneName Updated Successfully' );
2165
+ res.sendSuccess( 'ZoneName Updated Successfully' );
2166
+ } else {
2167
+ logger.error( { error: 'something went wrong', function: 'updateZoneCustomTag' } );
2168
+ return res.sendError( 'something went wrong', 500 );
2169
+ }
2170
+ } catch ( error ) {
2171
+ logger.error( { error: error, function: 'updateZoneCustomTag' } );
2172
+ return res.sendError( error, 500 );
2173
+ }
2174
+ };
2175
+
2176
+ export const deleteZoneCustomTag = async ( req, res ) => {
2177
+ try {
2178
+ const { clientId, tagName } = req.body;
2179
+ if ( !clientId || !tagName ) {
2180
+ return res.sendError( 'clientId and zoneName are required', 400 );
2181
+ }
2182
+
2183
+ let zoneTagDetails = await customZoneTagService.findOne( { clientId, tagName } );
2184
+ if ( !zoneTagDetails || zoneTagDetails?.length == 0 ) {
2185
+ return res.sendError( 'No data found', 400 );
2186
+ }
2187
+ updateOldData( req, res );
2188
+ await customZoneTagService.deleteOne( { clientId, tagName } );
2189
+ await taggingService.deleteMany(
2190
+ {
2191
+ clientId,
2192
+ tagName: { $in: tagName },
2193
+ },
2194
+ );
2195
+ const logObj = {
2196
+ clientId: req.body?.clientId,
2197
+ userName: req.user?.userName,
2198
+ email: req.user?.email,
2199
+ date: new Date(),
2200
+ logType: 'zone',
2201
+ logSubType: 'deleteZoneCustomTag',
2202
+ changes: [ `${tagName} zone tag` ],
2203
+ eventType: 'delete',
2204
+ showTo: [ 'client', 'tango' ],
2205
+ };
2206
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2207
+ // return res.sendSuccess( 'ZoneName Deleted Successfully' );
2208
+ } catch ( e ) {
2209
+ logger.error( { error: e, function: 'deleteZoneCustomTag' } );
2210
+ return res.sendError( e, 500 );
2211
+ }
2212
+ };
2213
+
2214
+
2215
+ // setting config - get zone grouping details
2216
+ export const getZoneGroupDetails = async ( req, res ) => {
2217
+ try {
2218
+ const {
2219
+ clientId,
2220
+ searchValue,
2221
+ sortColumName,
2222
+ sortBy,
2223
+ limit,
2224
+ offset = 1,
2225
+ export: isExport,
2226
+ } = req.body;
2227
+
2228
+ if ( !clientId ) {
2229
+ return res.sendError( 'clientId is required', 400 );
2230
+ }
2231
+
2232
+
2233
+ let groupData = await customzonegrouping.findOne( { clientId: clientId } );
2234
+ if ( !groupData ) {
2235
+ return res.sendSuccess( { result: [] } );
2236
+ }
2237
+
2238
+ const searchMatch = searchValue ?
2239
+ { groupName: { $regex: searchValue, $options: 'i' } } :
2240
+ null;
2241
+
2242
+ const pipeline = [
2243
+ { $match: { clientId } },
2244
+
2245
+ {
2246
+ $lookup: {
2247
+ from: 'taggings',
2248
+ let: { groupName: '$groupName' },
2249
+ pipeline: [
2250
+ {
2251
+ $match: {
2252
+ $expr: {
2253
+ $and: [
2254
+ { $eq: [ '$groupName', '$$groupName' ] },
2255
+ { $eq: [ '$clientId', clientId ] },
2256
+ ],
2257
+ },
2258
+ },
2259
+ },
2260
+ ],
2261
+ as: 'tagsCount',
2262
+ },
2263
+ },
2264
+
2265
+ {
2266
+ $lookup: {
2267
+ from: 'customzonetags',
2268
+ let: { groupName: '$groupName' },
2269
+ pipeline: [
2270
+ {
2271
+ $match: {
2272
+ $expr: {
2273
+ $and: [
2274
+ { $eq: [ '$groupName', '$$groupName' ] },
2275
+ { $eq: [ '$clientId', clientId ] },
2276
+ ],
2277
+ },
2278
+ },
2279
+ },
2280
+ ],
2281
+ as: 'zonesTagged',
2282
+ },
2283
+ },
2284
+
2285
+ {
2286
+ $project: {
2287
+ clientId: 1,
2288
+ groupName: 1,
2289
+ zonesTagged: 1,
2290
+ zonesCount: { $size: '$tagsCount' },
2291
+ createdAt: 1,
2292
+ },
2293
+ },
2294
+
2295
+ {
2296
+ $facet: {
2297
+ totalCount: [
2298
+ ...( searchMatch ? [ { $match: searchMatch } ] : [] ),
2299
+ { $count: 'count' },
2300
+ ],
2301
+
2302
+ data: [
2303
+ ...( searchMatch ? [ { $match: searchMatch } ] : [] ),
2304
+
2305
+ ...( sortColumName && sortBy ?
2306
+ [ { $sort: { [sortColumName]: sortBy } } ] :
2307
+ [ { $sort: { createdAt: -1 } } ] ),
2308
+
2309
+ ...( isExport ?
2310
+ [] :
2311
+ limit ? [
2312
+ { $skip: ( offset - 1 ) * limit },
2313
+ { $limit: Number( limit ) },
2314
+ ] : [] ),
2315
+ ],
2316
+ },
2317
+ },
2318
+ ];
2319
+
2320
+ const [ result ] = await customzonegrouping.aggregate( pipeline );
2321
+
2322
+ const zoneGroupList = result.data || [];
2323
+
2324
+ const totalCount = result.totalCount[0]?.count || 0;
2325
+ if ( totalCount === 0 ) {
2326
+ return res.sendError( 'No data', 204 );
2327
+ }
2328
+ // Get total count of zones for tabs (actual count, not filtered by search)
2329
+ const totalZoneCount = await customZoneTagService.count( { clientId } );
2330
+
2331
+ if ( isExport && zoneGroupList.length ) {
2332
+ const exportdata = zoneGroupList.map( ( element ) => ( {
2333
+ 'Zone Group Name': element.groupName || '--',
2334
+ 'Zones tagged': Array.isArray( element.zonesTagged ) ?
2335
+ element.zonesTagged.map( ( zone ) => zone.tagName ).filter( Boolean ).
2336
+ join( ', ' ) : '--',
2337
+ 'Zones tagged Count': element.zonesTagged.length || 0,
2338
+ } ) );
2339
+ await download( exportdata, res );
2340
+ return;
2341
+ }
2342
+
2343
+ return res.sendSuccess( {
2344
+ result: zoneGroupList,
2345
+ totalCount,
2346
+ totalZoneCount,
2347
+ } );
2348
+ } catch ( e ) {
2349
+ logger.error( { error: e, function: 'getZoneGroupDetails' } );
2350
+ return res.sendError( e, 500 );
2351
+ }
2352
+ // try {
2353
+ // const inputData = req.body;
2354
+ // if ( !inputData.clientId ) {
2355
+ // return res.sendError( 'clientId is required', 400 );
2356
+ // }
2357
+
2358
+ // let Query = [
2359
+ // {
2360
+ // $match: {
2361
+ // clientId: inputData.clientId,
2362
+ // },
2363
+ // },
2364
+ // {
2365
+ // $lookup: {
2366
+ // from: 'taggings',
2367
+ // let: { groupName: '$groupName' },
2368
+ // pipeline: [
2369
+ // {
2370
+ // $match: {
2371
+ // $expr: {
2372
+ // $and: [
2373
+ // {
2374
+ // $eq: [ '$groupName', '$$groupName' ],
2375
+ // },
2376
+ // {
2377
+ // $eq: [ '$clientId', inputData.clientId ],
2378
+ // },
2379
+ // ],
2380
+ // },
2381
+ // },
2382
+ // },
2383
+ // ], as: 'tagsCount',
2384
+ // },
2385
+ // },
2386
+ // {
2387
+ // $lookup: {
2388
+ // from: 'customzonetags',
2389
+ // let: { groupName: '$groupName' },
2390
+ // pipeline: [
2391
+ // {
2392
+ // $match: {
2393
+ // $expr: {
2394
+ // $and: [
2395
+ // {
2396
+ // $eq: [ '$groupName', '$$groupName' ],
2397
+ // },
2398
+ // {
2399
+ // $eq: [ '$clientId', inputData.clientId ],
2400
+ // },
2401
+ // ],
2402
+ // },
2403
+ // },
2404
+ // },
2405
+ // ], as: 'zonesTagged',
2406
+ // },
2407
+ // },
2408
+ // {
2409
+ // $project: {
2410
+ // clientId: 1,
2411
+ // groupName: 1,
2412
+ // zonesTagged: 1,
2413
+ // zonesCount: { $size: '$tagsCount' },
2414
+ // },
2415
+ // },
2416
+ // ];
2417
+
2418
+ // if ( req.body.searchValue && req.body.searchValue !== '' ) {
2419
+ // Query.push( {
2420
+ // $match: {
2421
+ // $or: [
2422
+ // { groupName: { $regex: req.body.searchValue, $options: 'i' } },
2423
+ // ],
2424
+ // },
2425
+ // } );
2426
+ // }
2427
+
2428
+ // if ( req.body.sortColumName && req.body.sortColumName !== '' && req.body.sortBy ) {
2429
+ // Query.push( {
2430
+ // $sort: { [req.body.sortColumName]: req.body.sortBy },
2431
+ // } );
2432
+ // } else {
2433
+ // // Default sort by createdAt descending if no sort specified
2434
+ // Query.push( {
2435
+ // $sort: { createdAt: -1 },
2436
+ // } );
2437
+ // }
2438
+
2439
+ // // Get total count before pagination
2440
+ // const countQuery = [ ...Query ];
2441
+ // const totalCountResult = await customzonegrouping.aggregate( [
2442
+ // ...countQuery,
2443
+ // { $count: 'total' },
2444
+ // ] );
2445
+ // console.log( '🚀 ~ getZoneGroupDetails ~ totalCountResult:', totalCountResult );
2446
+ // const totalCount = totalCountResult.length > 0 ? totalCountResult[0].total : 0;
2447
+
2448
+ // if ( req.body.limit && req.body.offset && !req.body.export ) {
2449
+ // Query.push(
2450
+ // { $skip: ( req.body.offset - 1 ) * req.body.limit },
2451
+ // { $limit: Number( req.body.limit ) },
2452
+ // );
2453
+ // }
2454
+
2455
+ // const zoneGroupList = await customzonegrouping.aggregate( Query );
2456
+
2457
+ // if ( req.body.export && zoneGroupList.length > 0 ) {
2458
+ // const exportdata = [];
2459
+ // zoneGroupList.forEach( ( element ) => {
2460
+ // const data = {
2461
+ // 'Zone Group Name': element.groupName || '--',
2462
+ // 'Zones tagged': Array.isArray( element.zonesTagged ) ? element.zonesTagged.join( ', ' ) : '--',
2463
+ // 'Zones tagged Count': element.zonesCount || 0,
2464
+ // // 'Stores Tagged Count': element.storesTaggedCount || 0,
2465
+ // };
2466
+ // exportdata.push( data );
2467
+ // } );
2468
+ // await download( exportdata, res );
2469
+ // return;
2470
+ // }
2471
+
2472
+ // return res.sendSuccess( {
2473
+ // result: zoneGroupList,
2474
+ // count: totalCount,
2475
+ // } );
2476
+ // } catch ( e ) {
2477
+ // logger.error( { error: e, function: 'getZoneGroupDetails' } );
2478
+ // console.error( 'getZoneGroupDetails error:', e );
2479
+ // return res.sendError( e, 500 );
2480
+ // }
2481
+ };
2482
+
2483
+ export const addZoneGroup = async ( req, res ) => {
2484
+ try {
2485
+ const { clientId, groupName, zonesTagged = [], productName = '' } = req.body;
2486
+
2487
+ if ( !clientId || !groupName ) {
2488
+ return res.sendError( 'clientId and groupName are required', 400 );
2489
+ }
2490
+
2491
+ // Escape regex special characters in groupName to avoid invalid regex patterns
2492
+ const escapedGroupName = groupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
2493
+
2494
+ // Case-insensitive check for existing group
2495
+ const existingGroup = await customzonegrouping.findOne( {
2496
+ clientId,
2497
+ groupName: { $regex: `^${escapedGroupName}$`, $options: 'i' },
2498
+ } );
2499
+
2500
+ if ( existingGroup ) {
2501
+ return res.sendError( `groupName "${groupName}" already exists for this client`, 409 );
2502
+ }
2503
+
2504
+ const payload = {
2505
+ clientId,
2506
+ groupName,
2507
+ zonesTagged: Array.isArray( zonesTagged ) ? zonesTagged : [],
2508
+ productName,
2509
+ };
2510
+
2511
+ await customzonegrouping.create( payload );
2512
+
2513
+ // Update groupName for all zone tags in zonesTagged array
2514
+ if ( Array.isArray( zonesTagged ) && zonesTagged.length > 0 ) {
2515
+ // Update all tags that are in the zonesTagged array to have this groupName
2516
+ await customZoneTagService.updateMany(
2517
+ {
2518
+ clientId,
2519
+ tagName: { $in: zonesTagged },
2520
+ },
2521
+ { $set: { groupName } },
2522
+ );
2523
+ await taggingService.updateMany(
2524
+ {
2525
+ clientId,
2526
+ tagName: { $in: zonesTagged },
2527
+ },
2528
+ { $set: { groupName } },
2529
+ );
2530
+ }
2531
+
2532
+ logger.info( 'Zone Group Created Successfully' );
2533
+
2534
+ const logObj = {
2535
+ clientId,
2536
+ userName: req.user?.userName,
2537
+ email: req.user?.email,
2538
+ date: new Date(),
2539
+ logType: 'zone',
2540
+ logSubType: 'addZoneGroup',
2541
+ changes: [ `${groupName} zone group` ],
2542
+ eventType: 'create',
2543
+ showTo: [ 'client', 'tango' ],
2544
+ };
2545
+
2546
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2547
+ return res.sendSuccess( 'Zone Group Created Successfully' );
2548
+ } catch ( e ) {
2549
+ // Duplicate key error from unique index
2550
+ if ( e.code === 11000 ) {
2551
+ return res.sendError( `groupName "${req.body.groupName}" already exists for this client`, 409 );
2552
+ }
2553
+
2554
+ logger.error( { error: e, function: 'addZoneGroup' } );
2555
+ return res.sendError( e.message || 'Internal Server Error', 500 );
2556
+ }
2557
+ };
2558
+
2559
+ export const updateZoneGroup = async ( req, res ) => {
2560
+ try {
2561
+ const { _id, clientId, groupName, productName, zonesTagged = [] } = req.body;
2562
+
2563
+ if ( !clientId || !groupName ) {
2564
+ return res.sendError( 'clientId and groupName are required', 400 );
2565
+ }
2566
+
2567
+ let findGroup = await customzonegrouping.findOne( { _id: _id } );
2568
+
2569
+ // Escape regex special characters in groupName to avoid invalid regex patterns
2570
+ const escapedGroupName = groupName.replace( /[.*+?^${}()|[\]\\]/g, '\\$&' );
2571
+
2572
+ // Case-insensitive check for existing group (excluding current group)
2573
+ const existingGroup = await customzonegrouping.findOne( {
2574
+ clientId: clientId,
2575
+ groupName: { $regex: `^${escapedGroupName}$`, $options: 'i' },
2576
+ _id: { $ne: _id },
2577
+ } );
2578
+ if ( existingGroup && existingGroup.groupName ) {
2579
+ return res.sendError( `groupName "${groupName}" already exists for this client`, 409 );
2580
+ }
2581
+
2582
+ if ( findGroup ) {
2583
+ let removedTags = findGroup.zonesTagged.filter( ( zone ) => !zonesTagged.includes( zone ) ); ;
2584
+ if ( removedTags && removedTags.length > 0 ) {
2585
+ let updateQuery = { clientId, tagName: { $in: removedTags } };
2586
+ await customZoneTagService.updateMany( updateQuery, { $set: { groupName: null } } );
2587
+ await taggingService.updateMany( updateQuery, { $set: { groupName: null } } );
2588
+ }
2589
+ }
2590
+
2591
+
2592
+ await customzonegrouping.updateOne( { _id: _id }, { groupName: groupName, zonesTagged: zonesTagged } );
2593
+ let updateQuery = { clientId, tagName: { $in: zonesTagged } };
2594
+ await customZoneTagService.updateMany( updateQuery, { $set: { groupName } } );
2595
+ await taggingService.updateMany( updateQuery, { $set: { groupName } } );
2596
+
2597
+
2598
+ const logObj = {
2599
+ clientId: clientId,
2600
+ userName: req.user?.userName,
2601
+ email: req.user?.email,
2602
+ date: new Date(),
2603
+ logType: 'zone',
2604
+ logSubType: 'updateZoneGroup',
2605
+ changes: [ `zone group updated from "${findGroup.groupName || groupName}" to "${groupName}"` ],
2606
+ eventType: 'update',
2607
+ previous: {
2608
+ groupName: findGroup.groupName || groupName,
2609
+ productName: findGroup.productName,
2610
+ zonesTagged: findGroup.zonesTagged,
2611
+ },
2612
+ current: {
2613
+ groupName: groupName,
2614
+ productName: productName,
2615
+ zonesTagged: zonesTagged,
2616
+ },
2617
+ oldData: {
2618
+ GroupName: findGroup.groupName || groupName,
2619
+ },
2620
+ newData: {
2621
+ GroupName: groupName,
2622
+ },
2623
+ showTo: [ 'client', 'tango' ],
2624
+ };
2625
+
2626
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2627
+
2628
+ return res.sendSuccess( 'Zone Group Updated Successfully' );
2629
+ } catch ( e ) {
2630
+ // Duplicate key error from unique index
2631
+ if ( e.code === 11000 ) {
2632
+ return res.sendError( `groupName "${req.body.groupName}" already exists for this client`, 409 );
2633
+ }
2634
+
2635
+ logger.error( { error: e, function: 'updateZoneGroup' } );
2636
+ return res.sendError( e.message || 'Internal Server Error', 500 );
2637
+ }
2638
+ };
2639
+
2640
+ export const uploadBulkZoneGroup = async ( req, res ) => {
2641
+ try {
2642
+ const groupsArray = req.body;
2643
+ const clientId = req.bulkZoneClientId;
2644
+
2645
+ if ( !groupsArray || !Array.isArray( groupsArray ) || groupsArray.length === 0 ) {
2646
+ return res.sendError( 'group-config file must not be empty', 400 );
2647
+ }
2648
+
2649
+ if ( !clientId ) {
2650
+ return res.sendError( 'clientId is required', 400 );
2651
+ }
2652
+
2653
+ const zoneGroupDataList = [];
2654
+ const createdGroupNames = [];
2655
+
2656
+ // Process each group in the array
2657
+ for ( const group of groupsArray ) {
2658
+ if ( !group.groupName || group.groupName === '' ) {
2659
+ continue; // Skip groups without groupName
2660
+ }
2661
+
2662
+ const zoneGroupData = {
2663
+ clientId: clientId,
2664
+ groupName: group.groupName,
2665
+ productName: group.productName,
2666
+ };
2667
+ zoneGroupDataList.push( zoneGroupData );
2668
+ createdGroupNames.push( group.groupName );
2669
+ }
2670
+
2671
+ // Create all zone groups
2672
+ if ( zoneGroupDataList.length > 0 ) {
2673
+ await customzonegrouping.create( zoneGroupDataList );
2674
+ }
2675
+
2676
+ logger.info( `Bulk Zone Groups Created Successfully: ${createdGroupNames.length} groups` );
2677
+ const logObj = {
2678
+ clientId: clientId,
2679
+ userName: req.user?.userName,
2680
+ email: req.user?.email,
2681
+ date: new Date(),
2682
+ logType: 'zone',
2683
+ logSubType: 'addBulkZoneGroup',
2684
+ changes: createdGroupNames.map( ( groupName ) => `${groupName} zone group` ),
2685
+ eventType: 'create',
2686
+ showTo: [ 'client', 'tango' ],
2687
+ };
2688
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2689
+ return res.sendSuccess( {
2690
+ message: `Zone Groups Created Successfully`,
2691
+ count: createdGroupNames.length,
2692
+ } );
2693
+ } catch ( e ) {
2694
+ logger.error( { error: e, function: 'uploadBulkZoneGroup' } );
2695
+ return res.sendError( 'Failed to upload group-config file', 500 );
2696
+ }
2697
+ };
2698
+
2699
+ export const deleteZoneGroup = async ( req, res ) => {
2700
+ try {
2701
+ let zoneGroupDetails = await customzonegrouping.findOne( { _id: req.body._id, clientId: req.body.clientId, groupName: req.body.groupName } );
2702
+ if ( !zoneGroupDetails || zoneGroupDetails?.length == 0 ) {
2703
+ return res.sendError( 'no data found', 204 );
2704
+ }
2705
+
2706
+ // Remove this group mapping from all custom zone tags (zones) that are linked to it
2707
+ await customZoneTagService.updateMany(
2708
+ {
2709
+ clientId: req.body.clientId,
2710
+ groupName: req.body.groupName,
2711
+ },
2712
+ { $set: { groupName: null } },
2713
+ );
2714
+ await taggingService.updateMany(
2715
+ {
2716
+ clientId: req.body.clientId,
2717
+ groupName: req.body.groupName,
2718
+ },
2719
+ { $set: { groupName: null } },
2720
+ );
2721
+ await customzonegrouping.deleteOne( { _id: req.body._id, clientId: req.body.clientId, groupName: req.body.groupName } );
2722
+
2723
+ const logObj = {
2724
+ clientId: req.body?.clientId,
2725
+ userName: req.user?.userName,
2726
+ email: req.user?.email,
2727
+ date: new Date(),
2728
+ logType: 'zone',
2729
+ logSubType: 'deleteZoneGroup',
2730
+ changes: [ `${req.body.groupName} zone group` ],
2731
+ eventType: 'delete',
2732
+ showTo: [ 'client', 'tango' ],
2733
+ };
2734
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2735
+ // await updatezoneTagging( req, res );
2736
+ return res.sendSuccess( 'Zone Group Deleted Successfully' );
2737
+ } catch ( e ) {
2738
+ logger.error( { error: e, function: 'deleteZoneGroup' } );
2739
+ return res.sendError( e, 500 );
2740
+ }
2741
+ };
2742
+
2743
+ export async function oldTagsMigration() {
2744
+ let uniqueTags = await taggingService.find( { productName: 'tangoZone', clientId: '440' } );
2745
+ const result = _.uniqBy( uniqueTags, 'tagName' );
2746
+
2747
+
2748
+ for ( let zone of result ) {
2749
+ let obj = {
2750
+ clientId: zone.clientId,
2751
+ tagName: zone.tagName,
2752
+ productName: zone.productName,
2753
+ rgbColor: zone.rgbColor,
2754
+ rgbBorderColor: zone.rgbBorderColor,
2755
+ groupName: null,
2756
+ };
2757
+ await customZoneTagService.create( [ obj ] );
2758
+ }
2759
+ }
2760
+