tango-app-api-store-zone 3.3.1-beta.19 → 3.3.1-beta.20

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';
9
+ import { signedUrl, listFileByPath, fileUpload, insertOpenSearchData, download } from 'tango-app-api-middleware';
8
10
  import * as checklistconfigService from '../services/checklistconfig.services.js';
9
-
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 );
@@ -193,7 +198,6 @@ export const customTagListv2 = async ( req, res ) => {
193
198
  { streamName: 1 },
194
199
  ),
195
200
  ] );
196
- console.log( req.query.clientId, camList.length );
197
201
 
198
202
  // Step 3: Add conditional tags
199
203
  if ( clientDetails?.featureConfigs?.isExcludedArea ) {
@@ -214,7 +218,7 @@ export const customTagListv2 = async ( req, res ) => {
214
218
  // Step 4: Fetch existing tags
215
219
  const tagInfo = await taggingService.find(
216
220
  { clientId: req.query.clientId, productName: req.query.selectedProduct },
217
- { tagName: 1, rgbColor: 1, rgbBorderColor: 1, productName: 1 },
221
+ { tagName: 1, rgbColor: 1, rgbBorderColor: 1, productName: 1, groupName: 1 },
218
222
  );
219
223
 
220
224
  // Merge all tags
@@ -226,7 +230,6 @@ export const customTagListv2 = async ( req, res ) => {
226
230
  // Step 6: Query counts per tag
227
231
  const tagNames = uniqueTags.map( ( t ) => t.tagName );
228
232
  let activeCam = camList.map( ( data ) => data.streamName );
229
- console.log( '🚀 ~ customTagListv2 ~ activeCam:', activeCam );
230
233
  const taggingDetails = await taggingService.aggregate( [
231
234
  {
232
235
  $match: {
@@ -255,18 +258,340 @@ export const customTagListv2 = async ( req, res ) => {
255
258
  'Tracker-out': { rgbColor: 'rgba(12, 195, 111, 0.5)', rgbBorderColor: 'rgb(22, 205, 125)' },
256
259
  };
257
260
 
261
+
258
262
  // Step 8: Final processing
259
263
  const finalTags = uniqueTags.map( ( tag ) => ( {
260
264
  tagName: tag.tagName,
261
265
  productName: tag.productName,
266
+ groupName: tag.groupName ? tag.groupName : '',
262
267
  count: countMap.get( tag.tagName ) || 0,
263
268
  rgbColor: tag.rgbColor || tagColors[tag.tagName]?.rgbColor,
264
269
  rgbBorderColor: tag.rgbBorderColor || tagColors[tag.tagName]?.rgbBorderColor,
265
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
+ }, [] );
266
295
 
267
296
 
268
297
  // Step 9: Sort by count (desc)
269
- finalTags.sort( ( a, b ) => b.count - a.count );
298
+ groupedData.sort( ( a, b ) => b.count - a.count );
299
+
300
+ if ( req.query.selectedProduct && req.query.selectedProduct === 'tangoTrax' ) {
301
+ let Query = [ {
302
+ $match: {
303
+ client_id: req.query.clientId,
304
+ publish: true,
305
+ checkListType: { $ne: 'custom' },
306
+ },
307
+ }, {
308
+ $group: {
309
+ _id: '$_id',
310
+ sourceCheckList_id: { $last: '$_id' },
311
+ tagName: { $last: '$checkListName' },
312
+ },
313
+ },
314
+ {
315
+ $lookup: {
316
+ from: 'cameras',
317
+ let: { checkListName: '$tagName' },
318
+ pipeline: [
319
+ {
320
+ $match: {
321
+ $expr: {
322
+ $anyElementTrue: {
323
+ $map: {
324
+ input: { $ifNull: [ '$taggedChecklist', [] ] }, // ✅ default to empty array
325
+ as: 'tc',
326
+ in: { $eq: [ '$$tc.checkListName', '$$checkListName' ] },
327
+ },
328
+ },
329
+ },
330
+ },
331
+ },
332
+ {
333
+ $project: {
334
+ storeId: 1,
335
+ },
336
+ },
337
+ ], as: 'cameraList',
338
+ },
339
+ },
340
+ {
341
+ $project: {
342
+ tagName: 1,
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,
359
+ count: { $size: '$cameraList' },
360
+ },
361
+ },
362
+ {
363
+ $sort: {
364
+ count: -1,
365
+ },
366
+ },
367
+
368
+ ];
369
+ let getChecklistData = await checklistconfigService.aggregate( Query );
370
+ if ( finalTags && finalTags.length > 0 ) {
371
+ let merged = getChecklistData.map( ( item ) => {
372
+ let match = finalTags.find( ( a ) => a.tagName === item.tagName );
373
+ if ( match ) {
374
+ return { ...item, ...match, count: item.count + match.count }; // add counts
375
+ }
376
+ return item;
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
+ // Split grouped vs ungrouped
549
+ {
550
+ $facet: {
551
+ grouped: [
552
+ { $match: { groupName: { $nin: [ null, '' ] } } },
553
+ {
554
+ $group: {
555
+ _id: '$groupName',
556
+ children: { $push: '$$ROOT' },
557
+ },
558
+ },
559
+ {
560
+ $project: {
561
+ _id: 0,
562
+ groupName: '$_id',
563
+ isGroup: { $literal: true },
564
+ children: 1,
565
+ },
566
+ },
567
+ ],
568
+
569
+ ungrouped: [
570
+ { $match: { groupName: { $in: [ null, '' ] } } },
571
+ {
572
+ $addFields: { isGroup: { $literal: false } },
573
+ },
574
+ ],
575
+ },
576
+ },
577
+
578
+ // Merge results
579
+ {
580
+ $project: {
581
+ result: { $concatArrays: [ '$ungrouped', '$grouped' ] },
582
+ },
583
+ },
584
+
585
+ { $unwind: '$result' },
586
+ { $replaceRoot: { newRoot: '$result' } },
587
+ ] );
588
+
589
+
590
+ console.log( '🚀 ~ customTagListv3 ~ taggingDetails:', zonefinalTags );
591
+ return res.sendSuccess( zonefinalTags );
592
+ }
593
+
594
+
270
595
  if ( req.query.selectedProduct && req.query.selectedProduct === 'tangoTrax' ) {
271
596
  let Query = [ {
272
597
  $match: {
@@ -337,7 +662,6 @@ export const customTagListv2 = async ( req, res ) => {
337
662
 
338
663
  ];
339
664
  let getChecklistData = await checklistconfigService.aggregate( Query );
340
- console.log( getChecklistData, finalTags );
341
665
  if ( finalTags && finalTags.length > 0 ) {
342
666
  let merged = getChecklistData.map( ( item ) => {
343
667
  let match = finalTags.find( ( a ) => a.tagName === item.tagName );
@@ -346,7 +670,6 @@ export const customTagListv2 = async ( req, res ) => {
346
670
  }
347
671
  return item;
348
672
  } );
349
- console.log( merged );
350
673
  // also include any arr1 items not in arr2
351
674
  finalTags.forEach( ( a ) => {
352
675
  if ( !merged.find( ( m ) => m.tagName === a.tagName ) ) {
@@ -525,20 +848,6 @@ export const tagging = async ( req, res ) => {
525
848
 
526
849
  await externalService.create( data );
527
850
  }
528
-
529
- let cameraDetails = await cameraService.findOne( { _id: InputData.cameraId, streamName: InputData.streamName }, { productModule: 1 } );
530
- if ( cameraDetails ) {
531
- if ( !cameraDetails?.productModule.includes( 'tangoZone' ) ) {
532
- cameraDetails.productModule.push( { productName: 'tangoZone', checked: true } );
533
- cameraDetails.save();
534
- } else {
535
- let camIndex = cameraDetails.productModule.findIndex( ( ele ) => ele.productName == 'tangoZone' );
536
- if ( !cameraDetails?.productModule?.[camIndex]?.checked ) {
537
- cameraDetails.productModule[camIndex].checked = true;
538
- cameraDetails.save();
539
- }
540
- }
541
- }
542
851
  await updatezoneTagging( req, res );
543
852
  } catch ( e ) {
544
853
  logger.error( { error: e, function: 'tagging' } );
@@ -1090,11 +1399,13 @@ export const getCameraStreamList = async ( req, res ) => {
1090
1399
  if ( !cameraDetails.length ) {
1091
1400
  return res.sendError( 'no data found', 204 );
1092
1401
  }
1093
- const folderPath = { file_path: `${req.body.storeId}/zone_base_images/`,
1402
+ const folderPath = {
1403
+ file_path: `${req.body.storeId}/zone_base_images/`,
1094
1404
  Bucket: JSON.parse( process.env.BUCKET ).zoneBaseImage, MaxKeys: 1000,
1095
1405
  };
1096
1406
  let fileList = await listFileByPath( folderPath );
1097
- const TaggedfolderPath = { file_path: `${req.body.storeId}/zone_tagged_image/`,
1407
+ const TaggedfolderPath = {
1408
+ file_path: `${req.body.storeId}/zone_tagged_image/`,
1098
1409
  Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage, MaxKeys: 1000,
1099
1410
  };
1100
1411
  let tagFileList = await listFileByPath( TaggedfolderPath );
@@ -1120,7 +1431,7 @@ export const getCameraStreamList = async ( req, res ) => {
1120
1431
  tagFileList.data.forEach( ( item ) => {
1121
1432
  if ( item.Key.length > 1 ) {
1122
1433
  let splitStream = item.Key.split( '/' );
1123
- let getStream = splitStream[splitStream.length -1].split( '.' );
1434
+ let getStream = splitStream[splitStream.length - 1].split( '.' );
1124
1435
 
1125
1436
  if ( getStream && getStream[0] == `${req.body.storeId}_${camera.streamName}` ) {
1126
1437
  tagPath = item.Key;
@@ -1131,21 +1442,23 @@ export const getCameraStreamList = async ( req, res ) => {
1131
1442
  if ( fileList?.data.length ) {
1132
1443
  fileList.data.forEach( ( ele ) => {
1133
1444
  let splitStream = ele.Key.split( '/' );
1134
- let getStream = splitStream[splitStream.length -1].split( '.' );
1445
+ let getStream = splitStream[splitStream.length - 1].split( '.' );
1135
1446
  if ( getStream && getStream[0] == `${req.body.storeId}_${camera.streamName}` ) {
1136
1447
  imgPath = ele.Key;
1137
1448
  }
1138
1449
  } );
1139
1450
  }
1140
1451
  if ( tagPath ) {
1141
- const params = { file_path: tagPath,
1452
+ const params = {
1453
+ file_path: tagPath,
1142
1454
  Bucket: JSON.parse( process.env.BUCKET ).zoneTaggingImage,
1143
1455
  };
1144
1456
  const cameraTagImage = await signedUrl( params );
1145
1457
  camera.tagImg = cameraTagImage;
1146
1458
  }
1147
1459
  if ( imgPath ) {
1148
- const baseParams = { file_path: imgPath,
1460
+ const baseParams = {
1461
+ file_path: imgPath,
1149
1462
  Bucket: JSON.parse( process.env.BUCKET ).zoneBaseImage,
1150
1463
  };
1151
1464
  const cameraBaseImage = await signedUrl( baseParams );
@@ -1161,3 +1474,1114 @@ export const getCameraStreamList = async ( req, res ) => {
1161
1474
  return res.sendError( e, 500 );
1162
1475
  }
1163
1476
  };
1477
+
1478
+
1479
+ // setting config - zone - get tagging details
1480
+ export const getZoneTaggingDetails = async ( req, res ) => {
1481
+ try {
1482
+ const {
1483
+ clientId,
1484
+ searchValue,
1485
+ sortColumName,
1486
+ sortBy,
1487
+ limit = 10,
1488
+ offset = 1,
1489
+ export: isExport,
1490
+ } = req.body;
1491
+
1492
+ if ( !clientId ) {
1493
+ return res.sendError( 'clientId is required', 400 );
1494
+ }
1495
+
1496
+ const matchStage = {
1497
+ clientId,
1498
+ };
1499
+
1500
+ const searchStage = searchValue ?
1501
+ {
1502
+ $or: [
1503
+ { tagName: { $regex: searchValue, $options: 'i' } },
1504
+ { groupName: { $regex: searchValue, $options: 'i' } },
1505
+ ],
1506
+ } :
1507
+ null;
1508
+
1509
+ const pipeline = [
1510
+ { $match: matchStage },
1511
+
1512
+ {
1513
+ $lookup: {
1514
+ from: 'taggings',
1515
+ let: { tagName: '$tagName' },
1516
+ pipeline: [
1517
+ {
1518
+ $match: {
1519
+ $expr: {
1520
+ $and: [
1521
+ { $eq: [ '$tagName', '$$tagName' ] },
1522
+ { $eq: [ '$clientId', clientId ] },
1523
+ ],
1524
+ },
1525
+ },
1526
+ },
1527
+ ],
1528
+ as: 'tagsCount',
1529
+ },
1530
+ },
1531
+
1532
+ {
1533
+ $project: {
1534
+ clientId: 1,
1535
+ tagName: 1,
1536
+ groupName: 1,
1537
+ storesTaggedCount: { $size: '$tagsCount' },
1538
+ },
1539
+ },
1540
+
1541
+ {
1542
+ $facet: {
1543
+ totalCount: [
1544
+ ...( searchStage ? [ { $match: searchStage } ] : [] ),
1545
+ { $count: 'count' },
1546
+ ],
1547
+
1548
+ filteredData: [
1549
+ ...( searchStage ? [ { $match: searchStage } ] : [] ),
1550
+ ...( sortColumName && sortBy ?
1551
+ [ { $sort: { [sortColumName]: sortBy } } ] :
1552
+ [] ),
1553
+ ...( isExport ?
1554
+ [] :
1555
+ [
1556
+ { $skip: ( offset - 1 ) * limit },
1557
+ { $limit: Number( limit ) },
1558
+ ] ),
1559
+ ],
1560
+ },
1561
+ },
1562
+ ];
1563
+
1564
+ const [ result ] = await customZoneTagService.aggregate( pipeline );
1565
+
1566
+ const totalCount = result.totalCount[0]?.count || 0;
1567
+ const zoneList = result.filteredData;
1568
+
1569
+ // Get total count of groups for tabs (actual count, not filtered by search)
1570
+ const totalGroupCount = await customzonegrouping.count( { clientId } );
1571
+
1572
+ if ( isExport && zoneList.length ) {
1573
+ const exportdata = zoneList.map( ( z ) => ( {
1574
+ 'Zone Name': z.tagName || '--',
1575
+ 'Zone Groups': z.groupName || '--',
1576
+ 'Stores tagged Count': z.storesTaggedCount || 0,
1577
+ } ) );
1578
+ await download( exportdata, res );
1579
+ return;
1580
+ }
1581
+
1582
+ return res.sendSuccess( {
1583
+ result: zoneList,
1584
+ totalCount,
1585
+ totalGroupCount,
1586
+ } );
1587
+ } catch ( e ) {
1588
+ logger.error( { error: e, function: 'getZoneTaggingDetails' } );
1589
+ return res.sendError( e, 500 );
1590
+ }
1591
+ // try {
1592
+ // const inputData = req.body;
1593
+ // let Query = [
1594
+ // {
1595
+ // $match: {
1596
+ // clientId: inputData.clientId,
1597
+ // },
1598
+ // },
1599
+ // {
1600
+ // $lookup: {
1601
+ // from: 'taggings',
1602
+ // let: { tagName: '$tagName' },
1603
+ // pipeline: [
1604
+ // {
1605
+ // $match: {
1606
+ // $expr: {
1607
+ // $and: [
1608
+ // {
1609
+ // $eq: [ '$tagName', '$$tagName' ],
1610
+ // },
1611
+ // {
1612
+ // $eq: [ '$clientId', inputData.clientId ],
1613
+ // },
1614
+ // ],
1615
+ // },
1616
+ // },
1617
+ // },
1618
+ // ], as: 'tagsCount',
1619
+ // },
1620
+ // },
1621
+ // {
1622
+ // $project: {
1623
+ // clientId: 1,
1624
+ // tagName: 1,
1625
+ // groupName: 1,
1626
+ // storesTaggedCount: { $size: '$tagsCount' },
1627
+ // },
1628
+ // },
1629
+ // ];
1630
+ // if ( req.body.searchValue && req.body.searchValue !== '' ) {
1631
+ // Query.push( {
1632
+ // $match: {
1633
+ // $or: [
1634
+ // { tagName: { $regex: req.body.searchValue, $options: 'i' } },
1635
+ // { groupName: { $regex: req.body.searchValue, $options: 'i' } },
1636
+ // ],
1637
+ // },
1638
+ // } );
1639
+ // }
1640
+
1641
+ // if ( req.body.sortColumName && req.body.sortColumName !== '' && req.body.sortBy ) {
1642
+ // Query.push( {
1643
+ // $sort: { [req.body.sortColumName]: req.body.sortBy },
1644
+ // } );
1645
+ // }
1646
+
1647
+ // const totalCount = await customZoneTagService.aggregate( Query );
1648
+ // if ( req.body.limit && req.body.offset && !req.body.export ) {
1649
+ // Query.push(
1650
+ // { $skip: ( req.body.offset - 1 ) * req.body.limit },
1651
+ // { $limit: Number( req.body.limit ) },
1652
+ // );
1653
+ // }
1654
+ // const zoneList = await customZoneTagService.aggregate( Query );
1655
+
1656
+ // if ( req.body.export && zoneList.length > 0 ) {
1657
+ // const exportdata = [];
1658
+ // zoneList.forEach( ( element ) => {
1659
+ // const data = {
1660
+ // 'Zone Name': element.tagName || '--',
1661
+ // 'Zone Groups': element.groupName || '--',
1662
+ // 'Stores tagged Count': element.storesTaggedCount || 0,
1663
+ // };
1664
+ // exportdata.push( data );
1665
+ // } );
1666
+ // await download( exportdata, res );
1667
+ // return;
1668
+ // }
1669
+
1670
+ // return res.sendSuccess( {
1671
+ // result: zoneList,
1672
+ // count: totalCount.length,
1673
+ // } );
1674
+ // } catch ( e ) {
1675
+ // logger.error( { error: e, function: 'getZoneTaggingDetails' } );
1676
+ // console.error( 'getZoneTaggingDetails error:', e );
1677
+ // return res.sendError( e, 500 );
1678
+ // }
1679
+ };
1680
+
1681
+ export const addZoneCustomTag = async ( req, res ) => {
1682
+ try {
1683
+ const {
1684
+ clientId,
1685
+ tagName,
1686
+ groupName,
1687
+ productName,
1688
+ isExistingGroup,
1689
+ rgbColor,
1690
+ rgbBorderColor,
1691
+ _id,
1692
+ } = req.body;
1693
+
1694
+ if ( !clientId || !tagName ) {
1695
+ return res.sendError( 'clientId and tagName are required', 400 );
1696
+ }
1697
+
1698
+ const existingTag = await customZoneTagService.findOne(
1699
+ { clientId, tagName, _id: { $ne: _id } },
1700
+ );
1701
+
1702
+ if ( existingTag ) {
1703
+ return res.sendError( `tagName "${tagName}" already exists for this client`, 409 );
1704
+ }
1705
+
1706
+ let groupDoc = null;
1707
+ if ( groupName ) {
1708
+ groupDoc = await customzonegrouping.findOne( { clientId, groupName } );
1709
+ }
1710
+
1711
+ if ( !isExistingGroup && groupDoc ) {
1712
+ return res.sendError( `groupName "${groupName}" already exists for this client`, 409 );
1713
+ }
1714
+
1715
+ /** Create Zone Tag */
1716
+ await customZoneTagService.create(
1717
+ [ {
1718
+ clientId,
1719
+ tagName,
1720
+ rgbColor,
1721
+ rgbBorderColor,
1722
+ groupName: groupName || null,
1723
+ productName,
1724
+ } ],
1725
+ );
1726
+
1727
+ /** Create or Update Zone Group */
1728
+ if ( groupName ) {
1729
+ if ( !groupDoc && !isExistingGroup ) {
1730
+ await customzonegrouping.create(
1731
+ [ {
1732
+ clientId,
1733
+ groupName,
1734
+ productName,
1735
+ zonesTagged: [ tagName ],
1736
+ } ],
1737
+ );
1738
+ } else if ( groupDoc && isExistingGroup ) {
1739
+ await customzonegrouping.updateOne(
1740
+ { _id: groupDoc._id },
1741
+ { $addToSet: { zonesTagged: tagName } },
1742
+ );
1743
+ }
1744
+ }
1745
+
1746
+ await taggingService.updateMany( { clientId, tagName }, { $set: { groupName } } );
1747
+
1748
+ logger.info( 'Zone Tag Created Successfully' );
1749
+ const logObj = {
1750
+ clientId: clientId,
1751
+ userName: req.user?.userName,
1752
+ email: req.user?.email,
1753
+ date: new Date(),
1754
+ logType: 'zone',
1755
+ logSubType: 'addZoneCustomTag',
1756
+ changes: [ `${tagName} custom zone tag` ],
1757
+ eventType: 'create',
1758
+ showTo: [ 'client', 'tango' ],
1759
+ };
1760
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
1761
+
1762
+ return res.sendSuccess( 'Zone Tag Created Successfully' );
1763
+ } catch ( e ) {
1764
+ logger.error( { error: e, function: 'addZoneCustomTag' } );
1765
+ return res.sendError( e.message || e, 500 );
1766
+ }
1767
+ };
1768
+
1769
+ export const uploadBulkZoneTag = async ( req, res ) => {
1770
+ try {
1771
+ const zonesArray = req.body;
1772
+ const clientId = req.bulkZoneClientId;
1773
+
1774
+ if ( !zonesArray || !Array.isArray( zonesArray ) || zonesArray.length === 0 ) {
1775
+ return res.sendError( 'zone-config file must not be empty', 400 );
1776
+ }
1777
+
1778
+ if ( !clientId ) {
1779
+ return res.sendError( 'clientId is required', 400 );
1780
+ }
1781
+
1782
+ const zoneTagDataList = [];
1783
+ const zoneGroupDataMap = new Map();
1784
+ const createdTagNames = [];
1785
+
1786
+ // Process each zone in the array
1787
+ for ( const zone of zonesArray ) {
1788
+ const zoneTagData = {
1789
+ clientId: clientId,
1790
+ tagName: zone.tagName,
1791
+ rgbColor: zone.rgbColor,
1792
+ rgbBorderColor: zone.rgbBorderColor,
1793
+ groupName: zone.groupName && zone.groupName !== '' ? zone.groupName : null,
1794
+ productName: zone.productName,
1795
+ };
1796
+ zoneTagDataList.push( zoneTagData );
1797
+ createdTagNames.push( zone.tagName );
1798
+
1799
+ // Prepare zone group data if groupName is provided
1800
+ if ( zone.groupName && zone.groupName !== '' && zone.groupName !== null ) {
1801
+ if ( !zoneGroupDataMap.has( zone.groupName ) ) {
1802
+ zoneGroupDataMap.set( zone.groupName, {
1803
+ clientId: clientId,
1804
+ groupName: zone.groupName,
1805
+ productName: zone.productName,
1806
+ } );
1807
+ }
1808
+ }
1809
+ }
1810
+
1811
+ // Create all zone tags
1812
+ await customZoneTagService.create( zoneTagDataList );
1813
+
1814
+ // Create zone groups (convert Map values to array)
1815
+ const zoneGroupDataList = Array.from( zoneGroupDataMap.values() );
1816
+ if ( zoneGroupDataList.length > 0 ) {
1817
+ for ( const groupData of zoneGroupDataList ) {
1818
+ await customzonegrouping.create( groupData );
1819
+ }
1820
+ }
1821
+
1822
+ logger.info( `Bulk Zone Tags Created Successfully: ${createdTagNames.length} tags` );
1823
+ const logObj = {
1824
+ clientId: clientId,
1825
+ userName: req.user?.userName,
1826
+ email: req.user?.email,
1827
+ date: new Date(),
1828
+ logType: 'zone',
1829
+ logSubType: 'addBulkZoneCustomTag',
1830
+ changes: createdTagNames.map( ( tagName ) => `${tagName} custom tag` ),
1831
+ eventType: 'create',
1832
+ showTo: [ 'client', 'tango' ],
1833
+ };
1834
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
1835
+ return res.sendSuccess( {
1836
+ message: `Zone Tags Created Successfully`,
1837
+ count: createdTagNames.length,
1838
+ } );
1839
+ } catch ( e ) {
1840
+ logger.error( { error: e, function: 'addBulkZoneCustomTag' } );
1841
+ return res.sendError( 'Failed to upload zone-config file', 500 );
1842
+ }
1843
+ };
1844
+
1845
+ export const updateZoneCustomTag = async ( req, res ) => {
1846
+ try {
1847
+ const { clientId, tagName, oldTag, isExistingGroup, oldGroupName, groupName } = req.body;
1848
+
1849
+ if ( !clientId || !tagName ) {
1850
+ return res.sendError( 'clientId, tagName are required', 400 );
1851
+ }
1852
+
1853
+ let findQuery = {};
1854
+ if ( oldTag !== undefined && oldTag !== null && tagName !== oldTag ) {
1855
+ findQuery = {
1856
+ clientId,
1857
+ tagName: oldTag,
1858
+ };
1859
+ } else {
1860
+ findQuery = {
1861
+ clientId,
1862
+ tagName,
1863
+ };
1864
+ }
1865
+
1866
+ let customZoneTagDetails = await customZoneTagService.findOne( findQuery );
1867
+
1868
+ if ( !customZoneTagDetails ) {
1869
+ return res.sendError( 'No data found', 400 );
1870
+ }
1871
+
1872
+ // Get the actual oldGroupName from the database record
1873
+ const actualOldGroupName = customZoneTagDetails.groupName || null;
1874
+ // Get the actual tag name currently in the database
1875
+ const actualTagNameInDb = customZoneTagDetails.tagName;
1876
+
1877
+ // Validate that the new tagName does not already exist for this client
1878
+ // Check if tagName is being changed (either via oldTag or if it's different from DB value)
1879
+ const isTagNameChanging = ( oldTag && tagName !== oldTag ) || ( actualTagNameInDb && tagName !== actualTagNameInDb );
1880
+
1881
+ if ( isTagNameChanging ) {
1882
+ const existingTagWithNewName = await customZoneTagService.findOne( {
1883
+ clientId,
1884
+ tagName,
1885
+ } );
1886
+
1887
+ // If a tag with the new name exists and it's not the same record we're updating, return error
1888
+ if ( existingTagWithNewName && existingTagWithNewName._id.toString() !== customZoneTagDetails._id.toString() ) {
1889
+ return res.sendError( `tagName "${tagName}" already exists for this client`, 409 );
1890
+ }
1891
+ }
1892
+
1893
+ let query = {
1894
+ clientId,
1895
+ };
1896
+ let payload = { tagName };
1897
+ if ( oldTag && tagName !== oldTag ) {
1898
+ query.tagName = oldTag;
1899
+ } else {
1900
+ query.tagName = tagName;
1901
+ }
1902
+ if ( groupName ) {
1903
+ payload.groupName = groupName || null;
1904
+ } else {
1905
+ payload.groupName = null;
1906
+ }
1907
+
1908
+ // Determine the tag name to use for group operations
1909
+ // Use the actual tag name from database for removing from old group
1910
+ const tagNameForRemoval = actualTagNameInDb;
1911
+ const newTagNameForGroup = tagName;
1912
+
1913
+ // Handle group changes: remove from old group and add to new group
1914
+ // Compare actual old group name from DB with new group name
1915
+ const oldGroupNameStr = actualOldGroupName ? String( actualOldGroupName ).trim() : null;
1916
+ const newGroupNameStr = groupName ? String( groupName ).trim() : null;
1917
+
1918
+ if ( oldGroupNameStr !== newGroupNameStr ) {
1919
+ // Remove tag from old group if oldGroupName exists
1920
+ if ( actualOldGroupName ) {
1921
+ const oldGroupDoc = await customzonegrouping.findOne(
1922
+ { clientId, groupName: actualOldGroupName },
1923
+ );
1924
+ if ( oldGroupDoc ) {
1925
+ await customzonegrouping.updateOne(
1926
+ { _id: oldGroupDoc._id },
1927
+ { $pull: { zonesTagged: tagNameForRemoval } },
1928
+ );
1929
+ } else {
1930
+ }
1931
+ }
1932
+
1933
+ // Add tag to new group if groupName exists
1934
+ if ( groupName ) {
1935
+ let newGroupDoc = await customzonegrouping.findOne(
1936
+ { clientId, groupName },
1937
+ );
1938
+
1939
+ if ( !newGroupDoc && !isExistingGroup ) {
1940
+ await customzonegrouping.create(
1941
+ [ {
1942
+ clientId,
1943
+ groupName,
1944
+ productName: req.body.productName,
1945
+ zonesTagged: [ newTagNameForGroup ],
1946
+ } ],
1947
+ );
1948
+ } else if ( newGroupDoc ) {
1949
+ // Add to existing group
1950
+ await customzonegrouping.updateOne(
1951
+ { _id: newGroupDoc._id },
1952
+ { $addToSet: { zonesTagged: newTagNameForGroup } },
1953
+ );
1954
+ }
1955
+ }
1956
+ } else if ( groupName && oldGroupNameStr === newGroupNameStr && oldTag && tagName !== oldTag ) {
1957
+ // If groupName is same (and not null) but tagName changed, update the tagName in the group's zonesTagged
1958
+ const groupDoc = await customzonegrouping.findOne( { clientId, groupName } );
1959
+ if ( groupDoc ) {
1960
+ await customzonegrouping.updateOne(
1961
+ { _id: groupDoc._id },
1962
+ [
1963
+ {
1964
+ $set: {
1965
+ zonesTagged: {
1966
+ $setUnion: [
1967
+ {
1968
+ $filter: {
1969
+ input: '$zonesTagged',
1970
+ cond: { $ne: [ '$$this', actualTagNameInDb ] },
1971
+ },
1972
+ },
1973
+ [ tagName ],
1974
+ ],
1975
+ },
1976
+ },
1977
+ },
1978
+ ],
1979
+ );
1980
+ }
1981
+ }
1982
+ // Update tagging collection if groupName changed or tagName changed (similar to addZoneCustomTag)
1983
+ if ( oldTag && tagName !== oldTag ) {
1984
+ // If tagName changed, update tagging collection records with oldTag to have new tagName and groupName
1985
+ await taggingService.updateMany(
1986
+ { clientId, tagName: oldTag },
1987
+ { $set: { tagName, groupName: groupName || null } },
1988
+ );
1989
+ } else if ( groupName !== oldGroupName ) {
1990
+ // If only groupName changed, update tagging collection with new groupName
1991
+ await taggingService.updateMany(
1992
+ { clientId, tagName },
1993
+ { $set: { groupName: groupName || null } },
1994
+ );
1995
+ }
1996
+
1997
+ let zonetag = await customZoneTagService.updateOne( query, { $set: payload } );
1998
+ const logObj = {
1999
+ clientId: req.body.clientId,
2000
+ userName: req.user?.userName,
2001
+ email: req.user?.email,
2002
+ date: new Date(),
2003
+ logType: 'zone',
2004
+ logSubType: 'updateZoneCustomTag',
2005
+ changes: [ `tagName changed from ${oldTag} to ${tagName}` ],
2006
+ eventType: 'update',
2007
+ previous: {
2008
+ tagName: oldTag,
2009
+ },
2010
+ current: {
2011
+ tagName: tagName,
2012
+ },
2013
+ oldData: {
2014
+ TagName: oldTag,
2015
+ },
2016
+ newData: {
2017
+ TagName: tagName,
2018
+ },
2019
+ showTo: [ 'tango', 'client' ],
2020
+ };
2021
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2022
+ if ( zonetag.modifiedCount || zonetag.matchedCount ) {
2023
+ logger.info( 'Zone Tag Updated Successfully' );
2024
+ res.sendSuccess( 'Zone Tag Updated Successfully' );
2025
+ } else {
2026
+ logger.error( { error: 'something went wrong', function: 'updateZoneCustomTag' } );
2027
+ return res.sendError( 'something went wrong', 500 );
2028
+ }
2029
+ } catch ( error ) {
2030
+ logger.error( { error: error, function: 'updateZoneCustomTag' } );
2031
+ return res.sendError( error, 500 );
2032
+ }
2033
+ };
2034
+
2035
+ export const deleteZoneCustomTag = async ( req, res ) => {
2036
+ try {
2037
+ const { clientId, tagName } = req.body;
2038
+ if ( !clientId || !tagName ) {
2039
+ return res.sendError( 'clientId and tagName are required', 400 );
2040
+ }
2041
+
2042
+ let zoneTagDetails = await customZoneTagService.findOne( { clientId, tagName } );
2043
+ if ( !zoneTagDetails || zoneTagDetails?.length == 0 ) {
2044
+ return res.sendError( 'No data found', 400 );
2045
+ }
2046
+
2047
+ await customZoneTagService.deleteOne( { clientId, tagName } );
2048
+ const logObj = {
2049
+ clientId: req.body?.clientId,
2050
+ userName: req.user?.userName,
2051
+ email: req.user?.email,
2052
+ date: new Date(),
2053
+ logType: 'zone',
2054
+ logSubType: 'deleteZoneCustomTag',
2055
+ changes: [ `${tagName} zone tag` ],
2056
+ eventType: 'delete',
2057
+ showTo: [ 'client', 'tango' ],
2058
+ };
2059
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2060
+ return res.sendSuccess( 'Zone Tag Deleted Successfully' );
2061
+ } catch ( e ) {
2062
+ logger.error( { error: e, function: 'deleteZoneCustomTag' } );
2063
+ return res.sendError( e, 500 );
2064
+ }
2065
+ };
2066
+
2067
+
2068
+ // setting config - get zone grouping details
2069
+ export const getZoneGroupDetails = async ( req, res ) => {
2070
+ try {
2071
+ const {
2072
+ clientId,
2073
+ searchValue,
2074
+ sortColumName,
2075
+ sortBy,
2076
+ limit = 10,
2077
+ offset = 1,
2078
+ export: isExport,
2079
+ } = req.body;
2080
+
2081
+ if ( !clientId ) {
2082
+ return res.sendError( 'clientId is required', 400 );
2083
+ }
2084
+
2085
+ const searchMatch = searchValue ?
2086
+ { groupName: { $regex: searchValue, $options: 'i' } } :
2087
+ null;
2088
+
2089
+ const pipeline = [
2090
+ { $match: { clientId } },
2091
+
2092
+ {
2093
+ $lookup: {
2094
+ from: 'taggings',
2095
+ let: { groupName: '$groupName' },
2096
+ pipeline: [
2097
+ {
2098
+ $match: {
2099
+ $expr: {
2100
+ $and: [
2101
+ { $eq: [ '$groupName', '$$groupName' ] },
2102
+ { $eq: [ '$clientId', clientId ] },
2103
+ ],
2104
+ },
2105
+ },
2106
+ },
2107
+ ],
2108
+ as: 'tagsCount',
2109
+ },
2110
+ },
2111
+
2112
+ {
2113
+ $lookup: {
2114
+ from: 'customzonetags',
2115
+ let: { groupName: '$groupName' },
2116
+ pipeline: [
2117
+ {
2118
+ $match: {
2119
+ $expr: {
2120
+ $and: [
2121
+ { $eq: [ '$groupName', '$$groupName' ] },
2122
+ { $eq: [ '$clientId', clientId ] },
2123
+ ],
2124
+ },
2125
+ },
2126
+ },
2127
+ ],
2128
+ as: 'zonesTagged',
2129
+ },
2130
+ },
2131
+
2132
+ {
2133
+ $project: {
2134
+ clientId: 1,
2135
+ groupName: 1,
2136
+ zonesTagged: 1,
2137
+ zonesCount: { $size: '$tagsCount' },
2138
+ createdAt: 1,
2139
+ },
2140
+ },
2141
+
2142
+ {
2143
+ $facet: {
2144
+ totalCount: [
2145
+ ...( searchMatch ? [ { $match: searchMatch } ] : [] ),
2146
+ { $count: 'count' },
2147
+ ],
2148
+
2149
+ data: [
2150
+ ...( searchMatch ? [ { $match: searchMatch } ] : [] ),
2151
+
2152
+ ...( sortColumName && sortBy ?
2153
+ [ { $sort: { [sortColumName]: sortBy } } ] :
2154
+ [ { $sort: { createdAt: -1 } } ] ),
2155
+
2156
+ ...( isExport ?
2157
+ [] :
2158
+ [
2159
+ { $skip: ( offset - 1 ) * limit },
2160
+ { $limit: Number( limit ) },
2161
+ ] ),
2162
+ ],
2163
+ },
2164
+ },
2165
+ ];
2166
+
2167
+ const [ result ] = await customzonegrouping.aggregate( pipeline );
2168
+
2169
+ const zoneGroupList = result.data || [];
2170
+
2171
+ const totalCount = result.totalCount[0]?.count || 0;
2172
+
2173
+ // Get total count of zones for tabs (actual count, not filtered by search)
2174
+ const totalZoneCount = await customZoneTagService.count( { clientId } );
2175
+
2176
+ if ( isExport && zoneGroupList.length ) {
2177
+ const exportdata = zoneGroupList.map( ( element ) => ( {
2178
+ 'Zone Group Name': element.groupName || '--',
2179
+ 'Zones tagged': Array.isArray( element.zonesTagged ) ?
2180
+ element.zonesTagged.map( ( zone ) => zone.tagName ).filter( Boolean ).
2181
+ join( ', ' ) : '--',
2182
+ 'Zones tagged Count': element.zonesCount || 0,
2183
+ } ) );
2184
+ await download( exportdata, res );
2185
+ return;
2186
+ }
2187
+
2188
+ return res.sendSuccess( {
2189
+ result: zoneGroupList,
2190
+ totalCount,
2191
+ totalZoneCount,
2192
+ } );
2193
+ } catch ( e ) {
2194
+ logger.error( { error: e, function: 'getZoneGroupDetails' } );
2195
+ return res.sendError( e, 500 );
2196
+ }
2197
+ // try {
2198
+ // const inputData = req.body;
2199
+ // if ( !inputData.clientId ) {
2200
+ // return res.sendError( 'clientId is required', 400 );
2201
+ // }
2202
+
2203
+ // let Query = [
2204
+ // {
2205
+ // $match: {
2206
+ // clientId: inputData.clientId,
2207
+ // },
2208
+ // },
2209
+ // {
2210
+ // $lookup: {
2211
+ // from: 'taggings',
2212
+ // let: { groupName: '$groupName' },
2213
+ // pipeline: [
2214
+ // {
2215
+ // $match: {
2216
+ // $expr: {
2217
+ // $and: [
2218
+ // {
2219
+ // $eq: [ '$groupName', '$$groupName' ],
2220
+ // },
2221
+ // {
2222
+ // $eq: [ '$clientId', inputData.clientId ],
2223
+ // },
2224
+ // ],
2225
+ // },
2226
+ // },
2227
+ // },
2228
+ // ], as: 'tagsCount',
2229
+ // },
2230
+ // },
2231
+ // {
2232
+ // $lookup: {
2233
+ // from: 'customzonetags',
2234
+ // let: { groupName: '$groupName' },
2235
+ // pipeline: [
2236
+ // {
2237
+ // $match: {
2238
+ // $expr: {
2239
+ // $and: [
2240
+ // {
2241
+ // $eq: [ '$groupName', '$$groupName' ],
2242
+ // },
2243
+ // {
2244
+ // $eq: [ '$clientId', inputData.clientId ],
2245
+ // },
2246
+ // ],
2247
+ // },
2248
+ // },
2249
+ // },
2250
+ // ], as: 'zonesTagged',
2251
+ // },
2252
+ // },
2253
+ // {
2254
+ // $project: {
2255
+ // clientId: 1,
2256
+ // groupName: 1,
2257
+ // zonesTagged: 1,
2258
+ // zonesCount: { $size: '$tagsCount' },
2259
+ // },
2260
+ // },
2261
+ // ];
2262
+
2263
+ // if ( req.body.searchValue && req.body.searchValue !== '' ) {
2264
+ // Query.push( {
2265
+ // $match: {
2266
+ // $or: [
2267
+ // { groupName: { $regex: req.body.searchValue, $options: 'i' } },
2268
+ // ],
2269
+ // },
2270
+ // } );
2271
+ // }
2272
+
2273
+ // if ( req.body.sortColumName && req.body.sortColumName !== '' && req.body.sortBy ) {
2274
+ // Query.push( {
2275
+ // $sort: { [req.body.sortColumName]: req.body.sortBy },
2276
+ // } );
2277
+ // } else {
2278
+ // // Default sort by createdAt descending if no sort specified
2279
+ // Query.push( {
2280
+ // $sort: { createdAt: -1 },
2281
+ // } );
2282
+ // }
2283
+
2284
+ // // Get total count before pagination
2285
+ // const countQuery = [ ...Query ];
2286
+ // const totalCountResult = await customzonegrouping.aggregate( [
2287
+ // ...countQuery,
2288
+ // { $count: 'total' },
2289
+ // ] );
2290
+ // console.log( '🚀 ~ getZoneGroupDetails ~ totalCountResult:', totalCountResult );
2291
+ // const totalCount = totalCountResult.length > 0 ? totalCountResult[0].total : 0;
2292
+
2293
+ // if ( req.body.limit && req.body.offset && !req.body.export ) {
2294
+ // Query.push(
2295
+ // { $skip: ( req.body.offset - 1 ) * req.body.limit },
2296
+ // { $limit: Number( req.body.limit ) },
2297
+ // );
2298
+ // }
2299
+
2300
+ // const zoneGroupList = await customzonegrouping.aggregate( Query );
2301
+
2302
+ // if ( req.body.export && zoneGroupList.length > 0 ) {
2303
+ // const exportdata = [];
2304
+ // zoneGroupList.forEach( ( element ) => {
2305
+ // const data = {
2306
+ // 'Zone Group Name': element.groupName || '--',
2307
+ // 'Zones tagged': Array.isArray( element.zonesTagged ) ? element.zonesTagged.join( ', ' ) : '--',
2308
+ // 'Zones tagged Count': element.zonesCount || 0,
2309
+ // // 'Stores Tagged Count': element.storesTaggedCount || 0,
2310
+ // };
2311
+ // exportdata.push( data );
2312
+ // } );
2313
+ // await download( exportdata, res );
2314
+ // return;
2315
+ // }
2316
+
2317
+ // return res.sendSuccess( {
2318
+ // result: zoneGroupList,
2319
+ // count: totalCount,
2320
+ // } );
2321
+ // } catch ( e ) {
2322
+ // logger.error( { error: e, function: 'getZoneGroupDetails' } );
2323
+ // console.error( 'getZoneGroupDetails error:', e );
2324
+ // return res.sendError( e, 500 );
2325
+ // }
2326
+ };
2327
+
2328
+ export const addZoneGroup = async ( req, res ) => {
2329
+ try {
2330
+ const { clientId, groupName, zonesTagged = [], productName = '' } = req.body;
2331
+
2332
+ if ( !clientId || !groupName ) {
2333
+ return res.sendError( 'clientId and groupName are required', 400 );
2334
+ }
2335
+
2336
+ const payload = {
2337
+ clientId,
2338
+ groupName,
2339
+ zonesTagged: Array.isArray( zonesTagged ) ? zonesTagged : [],
2340
+ productName,
2341
+ };
2342
+
2343
+ await customzonegrouping.create( payload );
2344
+
2345
+ // Update groupName for all zone tags in zonesTagged array
2346
+ if ( Array.isArray( zonesTagged ) && zonesTagged.length > 0 ) {
2347
+ // Update all tags that are in the zonesTagged array to have this groupName
2348
+ await customZoneTagService.updateMany(
2349
+ {
2350
+ clientId,
2351
+ tagName: { $in: zonesTagged },
2352
+ },
2353
+ { $set: { groupName } },
2354
+ );
2355
+ await taggingService.updateMany(
2356
+ {
2357
+ clientId,
2358
+ tagName: { $in: zonesTagged },
2359
+ },
2360
+ { $set: { groupName } },
2361
+ );
2362
+ }
2363
+
2364
+ logger.info( 'Zone Group Created Successfully' );
2365
+
2366
+ const logObj = {
2367
+ clientId,
2368
+ userName: req.user?.userName,
2369
+ email: req.user?.email,
2370
+ date: new Date(),
2371
+ logType: 'zone',
2372
+ logSubType: 'addZoneGroup',
2373
+ changes: [ `${groupName} zone group` ],
2374
+ eventType: 'create',
2375
+ showTo: [ 'client', 'tango' ],
2376
+ };
2377
+
2378
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2379
+ return res.sendSuccess( 'Zone Group Created Successfully' );
2380
+ } catch ( e ) {
2381
+ // Duplicate key error from unique index
2382
+ if ( e.code === 11000 ) {
2383
+ return res.sendError( `groupName "${req.body.groupName}" already exists for this client`, 409 );
2384
+ }
2385
+
2386
+ logger.error( { error: e, function: 'addZoneGroup' } );
2387
+ return res.sendError( e.message || 'Internal Server Error', 500 );
2388
+ }
2389
+ };
2390
+
2391
+ export const updateZoneGroup = async ( req, res ) => {
2392
+ try {
2393
+ const { _id, clientId, groupName, productName, zonesTagged = [] } = req.body;
2394
+
2395
+ if ( !clientId || !groupName ) {
2396
+ return res.sendError( 'clientId and groupName are required', 400 );
2397
+ }
2398
+
2399
+ let findGroup = await customzonegrouping.findOne( { _id: _id } );
2400
+ const existingGroup = await customzonegrouping.findOne( {
2401
+ clientId: clientId,
2402
+ groupName: groupName,
2403
+ _id: { $ne: _id },
2404
+ } );
2405
+ if ( existingGroup && existingGroup.groupName ) {
2406
+ return res.sendError( `groupName "${existingGroup.groupName}" already exists for this client`, 409 );
2407
+ }
2408
+
2409
+ if ( findGroup ) {
2410
+ let removedTags = findGroup.zonesTagged.filter( ( zone ) => !zonesTagged.includes( zone ) ); ;
2411
+ if ( removedTags && removedTags.length > 0 ) {
2412
+ let updateQuery = { clientId, tagName: { $in: removedTags } };
2413
+ await customZoneTagService.updateMany( updateQuery, { $set: { groupName: null } } );
2414
+ await taggingService.updateMany( updateQuery, { $set: { groupName: null } } );
2415
+ }
2416
+ }
2417
+
2418
+
2419
+ await customzonegrouping.updateOne( { _id: _id }, { groupName: groupName, zonesTagged: zonesTagged } );
2420
+ let updateQuery = { clientId, tagName: { $in: zonesTagged } };
2421
+ await customZoneTagService.updateMany( updateQuery, { $set: { groupName } } );
2422
+ await taggingService.updateMany( updateQuery, { $set: { groupName } } );
2423
+
2424
+
2425
+ const logObj = {
2426
+ clientId: clientId,
2427
+ userName: req.user?.userName,
2428
+ email: req.user?.email,
2429
+ date: new Date(),
2430
+ logType: 'zone',
2431
+ logSubType: 'updateZoneGroup',
2432
+ changes: [ `zone group updated from "${findGroup.groupName || groupName}" to "${groupName}"` ],
2433
+ eventType: 'update',
2434
+ previous: {
2435
+ groupName: findGroup.groupName || groupName,
2436
+ productName: findGroup.productName,
2437
+ zonesTagged: findGroup.zonesTagged,
2438
+ },
2439
+ current: {
2440
+ groupName: groupName,
2441
+ productName: productName,
2442
+ zonesTagged: zonesTagged,
2443
+ },
2444
+ oldData: {
2445
+ GroupName: findGroup.groupName || groupName,
2446
+ },
2447
+ newData: {
2448
+ GroupName: groupName,
2449
+ },
2450
+ showTo: [ 'client', 'tango' ],
2451
+ };
2452
+
2453
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2454
+
2455
+ return res.sendSuccess( 'Zone Group Updated Successfully' );
2456
+ } catch ( e ) {
2457
+ // Duplicate key error from unique index
2458
+ if ( e.code === 11000 ) {
2459
+ return res.sendError( `groupName "${req.body.groupName}" already exists for this client`, 409 );
2460
+ }
2461
+
2462
+ logger.error( { error: e, function: 'updateZoneGroup' } );
2463
+ return res.sendError( e.message || 'Internal Server Error', 500 );
2464
+ }
2465
+ };
2466
+
2467
+ export const uploadBulkZoneGroup = async ( req, res ) => {
2468
+ try {
2469
+ const groupsArray = req.body;
2470
+ const clientId = req.bulkZoneClientId;
2471
+
2472
+ if ( !groupsArray || !Array.isArray( groupsArray ) || groupsArray.length === 0 ) {
2473
+ return res.sendError( 'group-config file must not be empty', 400 );
2474
+ }
2475
+
2476
+ if ( !clientId ) {
2477
+ return res.sendError( 'clientId is required', 400 );
2478
+ }
2479
+
2480
+ const zoneGroupDataList = [];
2481
+ const createdGroupNames = [];
2482
+
2483
+ // Process each group in the array
2484
+ for ( const group of groupsArray ) {
2485
+ if ( !group.groupName || group.groupName === '' ) {
2486
+ continue; // Skip groups without groupName
2487
+ }
2488
+
2489
+ const zoneGroupData = {
2490
+ clientId: clientId,
2491
+ groupName: group.groupName,
2492
+ productName: group.productName,
2493
+ };
2494
+ zoneGroupDataList.push( zoneGroupData );
2495
+ createdGroupNames.push( group.groupName );
2496
+ }
2497
+
2498
+ // Create all zone groups
2499
+ if ( zoneGroupDataList.length > 0 ) {
2500
+ await customzonegrouping.create( zoneGroupDataList );
2501
+ }
2502
+
2503
+ logger.info( `Bulk Zone Groups Created Successfully: ${createdGroupNames.length} groups` );
2504
+ const logObj = {
2505
+ clientId: clientId,
2506
+ userName: req.user?.userName,
2507
+ email: req.user?.email,
2508
+ date: new Date(),
2509
+ logType: 'zone',
2510
+ logSubType: 'addBulkZoneGroup',
2511
+ changes: createdGroupNames.map( ( groupName ) => `${groupName} zone group` ),
2512
+ eventType: 'create',
2513
+ showTo: [ 'client', 'tango' ],
2514
+ };
2515
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2516
+ return res.sendSuccess( {
2517
+ message: `Zone Groups Created Successfully`,
2518
+ count: createdGroupNames.length,
2519
+ } );
2520
+ } catch ( e ) {
2521
+ logger.error( { error: e, function: 'uploadBulkZoneGroup' } );
2522
+ return res.sendError( 'Failed to upload group-config file', 500 );
2523
+ }
2524
+ };
2525
+
2526
+ export const deleteZoneGroup = async ( req, res ) => {
2527
+ try {
2528
+ let zoneGroupDetails = await customzonegrouping.findOne( { _id: req.body._id, clientId: req.body.clientId, groupName: req.body.groupName } );
2529
+ if ( !zoneGroupDetails || zoneGroupDetails?.length == 0 ) {
2530
+ return res.sendError( 'no data found', 204 );
2531
+ }
2532
+
2533
+ // Remove this group mapping from all custom zone tags (zones) that are linked to it
2534
+ await customZoneTagService.updateMany(
2535
+ {
2536
+ clientId: req.body.clientId,
2537
+ groupName: req.body.groupName,
2538
+ },
2539
+ { $set: { groupName: null } },
2540
+ );
2541
+ await taggingService.updateMany(
2542
+ {
2543
+ clientId: req.body.clientId,
2544
+ groupName: req.body.groupName,
2545
+ },
2546
+ { $set: { groupName: null } },
2547
+ );
2548
+ await customzonegrouping.deleteOne( { _id: req.body._id, clientId: req.body.clientId, groupName: req.body.groupName } );
2549
+
2550
+ const logObj = {
2551
+ clientId: req.body?.clientId,
2552
+ userName: req.user?.userName,
2553
+ email: req.user?.email,
2554
+ date: new Date(),
2555
+ logType: 'zone',
2556
+ logSubType: 'deleteZoneGroup',
2557
+ changes: [ `${req.body.groupName} zone group` ],
2558
+ eventType: 'delete',
2559
+ showTo: [ 'client', 'tango' ],
2560
+ };
2561
+ insertOpenSearchData( JSON.parse( process.env.OPENSEARCH )?.activityLog, logObj );
2562
+ // await updatezoneTagging( req, res );
2563
+ return res.sendSuccess( 'Zone Group Deleted Successfully' );
2564
+ } catch ( e ) {
2565
+ logger.error( { error: e, function: 'deleteZoneGroup' } );
2566
+ return res.sendError( e, 500 );
2567
+ }
2568
+ };
2569
+
2570
+ export async function oldTagsMigration() {
2571
+ let uniqueTags = await taggingService.find( { productName: 'tangoZone' } );
2572
+ const result = _.uniqBy( uniqueTags, 'tagName' );
2573
+
2574
+
2575
+ for ( let zone of result ) {
2576
+ let obj = {
2577
+ clientId: zone.clientId,
2578
+ tagName: zone.tagName,
2579
+ productName: zone.productName,
2580
+ rgbColor: zone.rgbColor,
2581
+ rgbBorderColor: zone.rgbBorderColor,
2582
+ groupName: null,
2583
+ };
2584
+ await customZoneTagService.create( [ obj ] );
2585
+ }
2586
+ }
2587
+