@trafficgroup/knex-rel 0.1.21 → 0.1.23-rc.0

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.
Files changed (121) hide show
  1. package/.claude/settings.local.json +5 -2
  2. package/CLAUDE.md +2 -11
  3. package/dist/constants/folder.constants.d.ts +2 -2
  4. package/dist/constants/folder.constants.js +6 -15
  5. package/dist/constants/folder.constants.js.map +1 -1
  6. package/dist/constants/study.constants.d.ts +1 -1
  7. package/dist/constants/study.constants.js +1 -1
  8. package/dist/constants/video.constants.d.ts +2 -2
  9. package/dist/constants/video.constants.js +5 -9
  10. package/dist/constants/video.constants.js.map +1 -1
  11. package/dist/dao/VideoMinuteResultDAO.d.ts +19 -2
  12. package/dist/dao/VideoMinuteResultDAO.js +75 -29
  13. package/dist/dao/VideoMinuteResultDAO.js.map +1 -1
  14. package/dist/dao/auth/auth.dao.js +1 -4
  15. package/dist/dao/auth/auth.dao.js.map +1 -1
  16. package/dist/dao/batch/batch.dao.d.ts +0 -10
  17. package/dist/dao/batch/batch.dao.js +14 -51
  18. package/dist/dao/batch/batch.dao.js.map +1 -1
  19. package/dist/dao/camera/camera.dao.js +7 -10
  20. package/dist/dao/camera/camera.dao.js.map +1 -1
  21. package/dist/dao/chat/chat.dao.d.ts +1 -1
  22. package/dist/dao/chat/chat.dao.js +40 -27
  23. package/dist/dao/chat/chat.dao.js.map +1 -1
  24. package/dist/dao/folder/folder.dao.js +7 -18
  25. package/dist/dao/folder/folder.dao.js.map +1 -1
  26. package/dist/dao/location/location.dao.js +9 -16
  27. package/dist/dao/location/location.dao.js.map +1 -1
  28. package/dist/dao/message/message.dao.d.ts +1 -1
  29. package/dist/dao/message/message.dao.js +26 -18
  30. package/dist/dao/message/message.dao.js.map +1 -1
  31. package/dist/dao/report-configuration/report-configuration.dao.js +68 -39
  32. package/dist/dao/report-configuration/report-configuration.dao.js.map +1 -1
  33. package/dist/dao/study/study.dao.d.ts +8 -1
  34. package/dist/dao/study/study.dao.js +20 -15
  35. package/dist/dao/study/study.dao.js.map +1 -1
  36. package/dist/dao/systemConfiguration/SystemConfigurationDAO.d.ts +2 -2
  37. package/dist/dao/systemConfiguration/SystemConfigurationDAO.js +26 -26
  38. package/dist/dao/systemConfiguration/SystemConfigurationDAO.js.map +1 -1
  39. package/dist/dao/user/user.dao.js +1 -4
  40. package/dist/dao/user/user.dao.js.map +1 -1
  41. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js +8 -26
  42. package/dist/dao/user-push-notification-token/user-push-notification-token.dao.js.map +1 -1
  43. package/dist/dao/video/video.dao.js +28 -30
  44. package/dist/dao/video/video.dao.js.map +1 -1
  45. package/dist/index.d.ts +10 -9
  46. package/dist/index.js +4 -4
  47. package/dist/index.js.map +1 -1
  48. package/dist/interfaces/batch/batch.interfaces.d.ts +1 -1
  49. package/dist/interfaces/camera/camera.interfaces.d.ts +1 -1
  50. package/dist/interfaces/chat/chat.interfaces.d.ts +3 -3
  51. package/dist/interfaces/folder/folder.interfaces.d.ts +1 -1
  52. package/dist/interfaces/message/message.interfaces.d.ts +2 -2
  53. package/dist/interfaces/report-configuration/report-configuration.interfaces.d.ts +1 -0
  54. package/dist/interfaces/study/study.interfaces.d.ts +2 -8
  55. package/dist/interfaces/user/user.interfaces.d.ts +1 -1
  56. package/dist/interfaces/user-push-notification-token/user-push-notification-token.interfaces.d.ts +1 -1
  57. package/dist/interfaces/video/video.interfaces.d.ts +2 -2
  58. package/migrations/20250717160737_migration.ts +1 -1
  59. package/migrations/20250717160908_migration.ts +2 -5
  60. package/migrations/20250717161310_migration.ts +1 -1
  61. package/migrations/20250717161406_migration.ts +3 -3
  62. package/migrations/20250717162431_migration.ts +1 -1
  63. package/migrations/20250717173228_migration.ts +2 -2
  64. package/migrations/20250717204731_migration.ts +1 -1
  65. package/migrations/20250722210109_migration.ts +4 -8
  66. package/migrations/20250722211019_migration.ts +1 -1
  67. package/migrations/20250723153852_migration.ts +10 -13
  68. package/migrations/20250723162257_migration.ts +7 -4
  69. package/migrations/20250723171109_migration.ts +7 -4
  70. package/migrations/20250723205331_migration.ts +9 -6
  71. package/migrations/20250724191345_migration.ts +11 -8
  72. package/migrations/20250730180932_migration.ts +13 -14
  73. package/migrations/20250730213625_migration.ts +11 -8
  74. package/migrations/20250804124509_migration.ts +21 -26
  75. package/migrations/20250804132053_migration.ts +8 -5
  76. package/migrations/20250804164518_migration.ts +7 -7
  77. package/migrations/20250823223016_migration.ts +21 -32
  78. package/migrations/20250910015452_migration.ts +6 -18
  79. package/migrations/20250911000000_migration.ts +4 -18
  80. package/migrations/20250917144153_migration.ts +7 -14
  81. package/migrations/20250930200521_migration.ts +4 -8
  82. package/migrations/20251010143500_migration.ts +6 -27
  83. package/migrations/20251020225758_migration.ts +15 -51
  84. package/migrations/20251112120000_migration.ts +2 -10
  85. package/migrations/20251112120200_migration.ts +7 -19
  86. package/migrations/20251112120300_migration.ts +2 -7
  87. package/migrations/20260109140000_migration.ts +2 -7
  88. package/package.json +3 -3
  89. package/src/constants/folder.constants.ts +8 -17
  90. package/src/constants/study.constants.ts +1 -1
  91. package/src/constants/video.constants.ts +7 -11
  92. package/src/d.types.ts +14 -18
  93. package/src/dao/VideoMinuteResultDAO.ts +127 -83
  94. package/src/dao/auth/auth.dao.ts +55 -58
  95. package/src/dao/batch/batch.dao.ts +100 -145
  96. package/src/dao/camera/camera.dao.ts +121 -124
  97. package/src/dao/chat/chat.dao.ts +45 -45
  98. package/src/dao/folder/folder.dao.ts +90 -105
  99. package/src/dao/location/location.dao.ts +87 -109
  100. package/src/dao/message/message.dao.ts +32 -32
  101. package/src/dao/reconciliation-log/reconciliation-log.dao.ts +1 -1
  102. package/src/dao/report-configuration/report-configuration.dao.ts +381 -370
  103. package/src/dao/study/study.dao.ts +83 -94
  104. package/src/dao/systemConfiguration/SystemConfigurationDAO.ts +35 -41
  105. package/src/dao/user/user.dao.ts +50 -52
  106. package/src/dao/user-push-notification-token/user-push-notification-token.dao.ts +48 -80
  107. package/src/dao/video/video.dao.ts +345 -396
  108. package/src/entities/BaseEntity.ts +1 -1
  109. package/src/index.ts +30 -43
  110. package/src/interfaces/auth/auth.interfaces.ts +10 -10
  111. package/src/interfaces/batch/batch.interfaces.ts +1 -1
  112. package/src/interfaces/camera/camera.interfaces.ts +9 -9
  113. package/src/interfaces/chat/chat.interfaces.ts +4 -4
  114. package/src/interfaces/folder/folder.interfaces.ts +2 -2
  115. package/src/interfaces/location/location.interfaces.ts +7 -7
  116. package/src/interfaces/message/message.interfaces.ts +3 -3
  117. package/src/interfaces/report-configuration/report-configuration.interfaces.ts +17 -16
  118. package/src/interfaces/study/study.interfaces.ts +3 -10
  119. package/src/interfaces/user/user.interfaces.ts +9 -9
  120. package/src/interfaces/user-push-notification-token/user-push-notification-token.interfaces.ts +9 -9
  121. package/src/interfaces/video/video.interfaces.ts +34 -34
@@ -23,6 +23,14 @@ interface ITMCVehicles {
23
23
  [vehicleClass: string]: ITMCDirections;
24
24
  }
25
25
 
26
+ interface ICrosswalkResults {
27
+ [crosswalkName: string]: {
28
+ [entityClass: string]: {
29
+ [direction: string]: number;
30
+ };
31
+ };
32
+ }
33
+
26
34
  interface ITMCResult {
27
35
  vehicles: ITMCVehicles;
28
36
  counts: {
@@ -33,6 +41,8 @@ interface ITMCResult {
33
41
  totalcount: number;
34
42
  detected_classes: { [vehicleClass: string]: number };
35
43
  study_type: "TMC";
44
+ crosswalks?: ICrosswalkResults;
45
+ crosswalk_totals?: { pedestrian: number; bicycle: number };
36
46
  }
37
47
 
38
48
  interface IATRVehicles {
@@ -69,15 +79,15 @@ interface IGroupedResponse {
69
79
  }
70
80
 
71
81
  interface IStudyTimeGroupResult {
72
- absoluteTime: string; // ISO 8601 start of bucket
82
+ absoluteTime: string; // ISO 8601 start of bucket
73
83
  groupIndex: number;
74
- startMinute: number; // Start minute number (0-based from video start)
75
- endMinute: number; // End minute number (0-based from video start)
76
- label: string; // Formatted label
84
+ startMinute: number; // Start minute number (0-based from video start)
85
+ endMinute: number; // End minute number (0-based from video start)
86
+ label: string; // Formatted label
77
87
  results: ITMCResult | IATRResult;
78
88
  minuteCount: number;
79
89
  videoCount: number;
80
- contributingVideos: string[]; // Video UUIDs
90
+ contributingVideos: string[]; // Video UUIDs
81
91
  }
82
92
 
83
93
  interface IGroupedStudyResponse {
@@ -87,7 +97,7 @@ interface IGroupedStudyResponse {
87
97
  study: {
88
98
  uuid: string;
89
99
  name: string;
90
- type: "TMC" | "ATR";
100
+ type: 'TMC' | 'ATR';
91
101
  status: string;
92
102
  };
93
103
  videoCount: number;
@@ -406,11 +416,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
406
416
  groupIndex: row.minute_number,
407
417
  startMinute: row.minute_number,
408
418
  endMinute: row.minute_number,
409
- label: this.formatTimeLabel(
410
- row.minute_number,
411
- row.minute_number,
412
- video.recordingStartedAt,
413
- ),
419
+ label: this.formatTimeLabel(row.minute_number, row.minute_number, video.recordingStartedAt),
414
420
  results: row.results,
415
421
  minuteCount: 1,
416
422
  })),
@@ -427,41 +433,37 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
427
433
  // Use Knex query builder for safe parameter binding
428
434
  const groupingQuery = this.knex(this.tableName)
429
435
  .select(
430
- this.knex.raw("FLOOR(minute_number / ?) as group_index", [
431
- groupingMinutes,
432
- ]),
433
- this.knex.raw("MIN(minute_number) as start_minute"),
434
- this.knex.raw("MAX(minute_number) as end_minute"),
435
- this.knex.raw("COUNT(*) as minute_count"),
436
- this.knex.raw(
437
- "array_agg(results ORDER BY minute_number) as all_results",
438
- ),
436
+ this.knex.raw('FLOOR(minute_number / ?) as group_index', [groupingMinutes]),
437
+ this.knex.raw('MIN(minute_number) as start_minute'),
438
+ this.knex.raw('MAX(minute_number) as end_minute'),
439
+ this.knex.raw('COUNT(*) as minute_count'),
440
+ this.knex.raw('array_agg(results ORDER BY minute_number) as all_results')
439
441
  )
440
- .where("video_id", video.id);
442
+ .where('video_id', video.id);
441
443
 
442
444
  if (startMinute !== undefined) {
443
- groupingQuery.where("minute_number", ">=", startMinute);
445
+ groupingQuery.where('minute_number', '>=', startMinute);
444
446
  }
445
447
 
446
448
  if (endMinute !== undefined) {
447
- groupingQuery.where("minute_number", "<=", endMinute);
449
+ groupingQuery.where('minute_number', '<=', endMinute);
448
450
  }
449
451
 
450
452
  const rows = await groupingQuery
451
- .groupBy("group_index")
452
- .orderBy("group_index");
453
+ .groupBy('group_index')
454
+ .orderBy('group_index');
453
455
 
454
456
  // Aggregate the results in TypeScript based on video type
455
457
  const aggregatedGroups: IGroupedResult[] = rows.map((row: any) => {
456
- if (!row || typeof row !== "object") {
457
- throw new Error("Invalid row data received from database query");
458
+ if (!row || typeof row !== 'object') {
459
+ throw new Error('Invalid row data received from database query');
458
460
  }
459
461
 
460
462
  const allResults = Array.isArray(row.all_results) ? row.all_results : [];
461
-
463
+
462
464
  // Determine video type based on multiple factors
463
- let studyType = video.videoType || "ATR"; // Default fallback to ATR
464
-
465
+ let studyType = video.videoType || 'ATR'; // Default fallback to ATR
466
+
465
467
  // Check if minute data has study_type field (ATR usually does)
466
468
  if (allResults.length > 0 && allResults[0].study_type) {
467
469
  studyType = allResults[0].study_type;
@@ -473,24 +475,20 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
473
475
  const vehicleKeys = Object.keys(sampleResult.vehicles);
474
476
  if (vehicleKeys.length > 0) {
475
477
  const firstVehicleType = sampleResult.vehicles[vehicleKeys[0]];
476
- if (firstVehicleType && typeof firstVehicleType === "object") {
478
+ if (firstVehicleType && typeof firstVehicleType === 'object') {
477
479
  const directions = Object.keys(firstVehicleType);
478
- if (
479
- directions.includes("NORTH") ||
480
- directions.includes("SOUTH") ||
481
- directions.includes("EAST") ||
482
- directions.includes("WEST")
483
- ) {
484
- studyType = "TMC";
480
+ if (directions.includes('NORTH') || directions.includes('SOUTH') ||
481
+ directions.includes('EAST') || directions.includes('WEST')) {
482
+ studyType = 'TMC';
485
483
  }
486
484
  }
487
485
  }
488
486
  }
489
487
  }
490
-
488
+
491
489
  // Aggregate based on determined video type
492
490
  let aggregatedResult;
493
- if (studyType === "TMC") {
491
+ if (studyType === 'TMC') {
494
492
  aggregatedResult = this.aggregateTMCResults(allResults);
495
493
  } else {
496
494
  aggregatedResult = this.aggregateATRResults(allResults);
@@ -500,11 +498,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
500
498
  groupIndex: row.group_index,
501
499
  startMinute: row.start_minute,
502
500
  endMinute: row.end_minute,
503
- label: this.formatTimeLabel(
504
- row.start_minute,
505
- row.end_minute,
506
- video.recordingStartedAt,
507
- ),
501
+ label: this.formatTimeLabel(row.start_minute, row.end_minute, video.recordingStartedAt),
508
502
  results: aggregatedResult,
509
503
  minuteCount: row.minute_count,
510
504
  };
@@ -585,8 +579,8 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
585
579
  "v.uuid as videoUuid",
586
580
  "v.recordingStartedAt",
587
581
  this.knex.raw(
588
- '"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"',
589
- ),
582
+ '"v"."recordingStartedAt" + (vmr.minute_number || \' minutes\')::INTERVAL as "absoluteTime"'
583
+ )
590
584
  )
591
585
  .whereIn("v.id", videoIds)
592
586
  .orderBy("absoluteTime", "asc");
@@ -624,7 +618,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
624
618
  for (const minute of minuteResults) {
625
619
  const absoluteTime = new Date(minute.absoluteTime);
626
620
  const minutesSinceEarliest = Math.floor(
627
- (absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60),
621
+ (absoluteTime.getTime() - earliestTime.getTime()) / (1000 * 60)
628
622
  );
629
623
  const bucketIndex = Math.floor(minutesSinceEarliest / groupingMinutes);
630
624
 
@@ -632,7 +626,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
632
626
  // Calculate bucket start time
633
627
  const bucketStartMinutes = bucketIndex * groupingMinutes;
634
628
  const bucketStartTime = new Date(
635
- earliestTime.getTime() + bucketStartMinutes * 60 * 1000,
629
+ earliestTime.getTime() + bucketStartMinutes * 60 * 1000
636
630
  );
637
631
 
638
632
  buckets.set(bucketIndex, {
@@ -649,14 +643,12 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
649
643
  }
650
644
 
651
645
  // Step 5: Aggregate using existing methods based on study type
652
- const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(
653
- buckets.values(),
654
- )
646
+ const aggregatedGroups: IStudyTimeGroupResult[] = Array.from(buckets.values())
655
647
  .sort((a, b) => a.groupIndex - b.groupIndex)
656
648
  .map((bucket) => {
657
649
  let aggregatedResult: ITMCResult | IATRResult;
658
650
 
659
- if (study.type === "TMC") {
651
+ if (study.type === 'TMC') {
660
652
  aggregatedResult = this.aggregateTMCResults(bucket.results);
661
653
  } else {
662
654
  aggregatedResult = this.aggregateATRResults(bucket.results);
@@ -667,10 +659,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
667
659
  groupIndex: bucket.groupIndex,
668
660
  startMinute: bucket.groupIndex * groupingMinutes,
669
661
  endMinute: (bucket.groupIndex + 1) * groupingMinutes - 1,
670
- label: this.formatStudyTimeLabel(
671
- bucket.absoluteTime,
672
- groupingMinutes,
673
- ),
662
+ label: this.formatStudyTimeLabel(bucket.absoluteTime, groupingMinutes),
674
663
  results: aggregatedResult,
675
664
  minuteCount: bucket.results.length,
676
665
  videoCount: bucket.videoUuids.size,
@@ -733,27 +722,25 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
733
722
  vehicles: {},
734
723
  counts: {
735
724
  total_vehicles: 0,
736
- entry_vehicles: 0,
725
+ entry_vehicles: 0
737
726
  },
738
727
  total: 0,
739
728
  totalcount: 0,
740
729
  detected_classes: {},
741
- study_type: "TMC",
730
+ study_type: "TMC"
742
731
  };
743
732
 
744
733
  for (const minute of minutes) {
745
734
  const results = minute; // minute is already the results object from array_agg
746
735
 
747
736
  // Aggregate vehicle movements by class and direction
748
- if (results.vehicles && typeof results.vehicles === "object") {
749
- for (const [vehicleClass, directions] of Object.entries(
750
- results.vehicles,
751
- )) {
737
+ if (results.vehicles && typeof results.vehicles === 'object') {
738
+ for (const [vehicleClass, directions] of Object.entries(results.vehicles)) {
752
739
  // Skip the 'total' pseudo vehicle class - validate it's actually aggregate data
753
- if (vehicleClass === "total" && typeof directions === "number") {
740
+ if (vehicleClass === 'total' && typeof directions === 'number') {
754
741
  continue; // This is aggregate total data, not a vehicle class
755
742
  }
756
-
743
+
757
744
  if (!aggregated.vehicles[vehicleClass]) {
758
745
  aggregated.vehicles[vehicleClass] = {};
759
746
  }
@@ -776,12 +763,11 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
776
763
  for (const [turnType, count] of Object.entries(turns)) {
777
764
  const turnCount = (count as number) || 0;
778
765
  aggregated.vehicles[vehicleClass][direction][turnType] =
779
- (aggregated.vehicles[vehicleClass][direction][turnType] ||
780
- 0) + turnCount;
781
-
766
+ (aggregated.vehicles[vehicleClass][direction][turnType] || 0) + turnCount;
767
+
782
768
  // Add to detected_classes count for this vehicle type
783
769
  aggregated.detected_classes[vehicleClass] += turnCount;
784
-
770
+
785
771
  // Add to total counts
786
772
  aggregated.total += turnCount;
787
773
  aggregated.totalcount += turnCount;
@@ -789,12 +775,10 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
789
775
  }
790
776
  }
791
777
  }
792
-
778
+
793
779
  // Also process the 'total' entry for validation but don't count it as a vehicle class
794
780
  if (results.vehicles.total) {
795
- for (const [direction, turns] of Object.entries(
796
- results.vehicles.total as any,
797
- )) {
781
+ for (const [direction, turns] of Object.entries(results.vehicles.total as any)) {
798
782
  if (typeof turns === "object" && turns !== null) {
799
783
  for (const [turnType, count] of Object.entries(turns)) {
800
784
  const turnCount = (count as number) || 0;
@@ -807,12 +791,75 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
807
791
  }
808
792
 
809
793
  // Set entry_vehicles same as total_vehicles for TMC
810
- aggregated.counts.entry_vehicles =
811
- aggregated.counts.total_vehicles || aggregated.total;
794
+ aggregated.counts.entry_vehicles = aggregated.counts.total_vehicles || aggregated.total;
795
+
796
+ // Aggregate crosswalk results if any minute contains crosswalk data
797
+ const crosswalkResult = this.aggregateCrosswalkResults(minutes);
798
+ if (crosswalkResult.crosswalks) {
799
+ aggregated.crosswalks = crosswalkResult.crosswalks;
800
+ aggregated.crosswalk_totals = crosswalkResult.crosswalk_totals;
801
+ }
812
802
 
813
803
  return aggregated;
814
804
  }
815
805
 
806
+ /**
807
+ * Aggregate crosswalk results from minute-level data
808
+ * Sums counts by crosswalk name, entity class, and direction
809
+ */
810
+ private aggregateCrosswalkResults(minutes: any[]): {
811
+ crosswalks?: ICrosswalkResults;
812
+ crosswalk_totals?: { pedestrian: number; bicycle: number };
813
+ } {
814
+ let hasCrosswalkData = false;
815
+ const aggregated: ICrosswalkResults = {};
816
+ const totals = { pedestrian: 0, bicycle: 0 };
817
+
818
+ for (const minute of minutes) {
819
+ const results = minute;
820
+ if (!results.crosswalks || typeof results.crosswalks !== 'object') {
821
+ continue;
822
+ }
823
+
824
+ hasCrosswalkData = true;
825
+
826
+ for (const [crosswalkName, classData] of Object.entries(results.crosswalks)) {
827
+ if (!aggregated[crosswalkName]) {
828
+ aggregated[crosswalkName] = {};
829
+ }
830
+
831
+ if (typeof classData !== 'object' || classData === null) continue;
832
+
833
+ for (const [entityClass, directions] of Object.entries(classData as Record<string, any>)) {
834
+ if (!aggregated[crosswalkName][entityClass]) {
835
+ aggregated[crosswalkName][entityClass] = {};
836
+ }
837
+
838
+ if (typeof directions !== 'object' || directions === null) continue;
839
+
840
+ for (const [direction, count] of Object.entries(directions as Record<string, any>)) {
841
+ const numericCount = typeof count === 'number' ? count : 0;
842
+ aggregated[crosswalkName][entityClass][direction] =
843
+ (aggregated[crosswalkName][entityClass][direction] || 0) + numericCount;
844
+
845
+ // Accumulate totals by entity class
846
+ if (entityClass === 'pedestrian') {
847
+ totals.pedestrian += numericCount;
848
+ } else if (entityClass === 'bicycle') {
849
+ totals.bicycle += numericCount;
850
+ }
851
+ }
852
+ }
853
+ }
854
+ }
855
+
856
+ if (!hasCrosswalkData) {
857
+ return {};
858
+ }
859
+
860
+ return { crosswalks: aggregated, crosswalk_totals: totals };
861
+ }
862
+
816
863
  /**
817
864
  * Aggregate ATR (Automatic Traffic Recorder) results
818
865
  */
@@ -832,7 +879,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
832
879
  if (results.vehicles) {
833
880
  for (const [vehicleClass, lanes] of Object.entries(results.vehicles)) {
834
881
  // Skip 'total' pseudo-class if present
835
- if (vehicleClass === "total") {
882
+ if (vehicleClass === 'total') {
836
883
  continue;
837
884
  }
838
885
 
@@ -841,8 +888,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
841
888
  }
842
889
 
843
890
  for (const [laneId, count] of Object.entries(lanes as any)) {
844
- const numericCount =
845
- typeof count === "number" ? count : parseInt(String(count)) || 0;
891
+ const numericCount = typeof count === 'number' ? count : (parseInt(String(count)) || 0);
846
892
  aggregated.vehicles[vehicleClass][laneId] =
847
893
  (aggregated.vehicles[vehicleClass][laneId] || 0) + numericCount;
848
894
  }
@@ -939,7 +985,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
939
985
  private formatTimeLabel(
940
986
  startMinute: number,
941
987
  endMinute: number,
942
- recordingStartedAt?: Date | string | null,
988
+ recordingStartedAt?: Date | string | null
943
989
  ): string {
944
990
  if (recordingStartedAt) {
945
991
  // Calculate actual wall-clock times using recordingStartedAt
@@ -978,10 +1024,7 @@ export class VideoMinuteResultDAO implements IBaseDAO<IVideoMinuteResult> {
978
1024
  * Used when results are already grouped by date in the UI
979
1025
  * Uses UTC time for consistency with absoluteTime field
980
1026
  */
981
- private formatStudyTimeLabel(
982
- startTime: Date,
983
- durationMinutes: number,
984
- ): string {
1027
+ private formatStudyTimeLabel(startTime: Date, durationMinutes: number): string {
985
1028
  const endTime = new Date(startTime.getTime() + durationMinutes * 60 * 1000);
986
1029
 
987
1030
  const formatTime = (date: Date): string => {
@@ -1002,6 +1045,7 @@ export type {
1002
1045
  IGroupedResult,
1003
1046
  ITMCResult,
1004
1047
  IATRResult,
1048
+ ICrosswalkResults,
1005
1049
  };
1006
1050
 
1007
1051
  export default VideoMinuteResultDAO;
@@ -4,61 +4,58 @@ import { IAuth } from "../../interfaces/auth/auth.interfaces";
4
4
  import KnexManager from "../../KnexConnection";
5
5
 
6
6
  export class AuthDAO implements IBaseDAO<IAuth> {
7
- private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
8
-
9
- async create(item: IAuth): Promise<IAuth> {
10
- const [createdAuth] = await this._knex("auth").insert(item).returning("*");
11
- return createdAuth;
12
- }
13
-
14
- async getById(id: number): Promise<IAuth | null> {
15
- const auth = await this._knex("auth").where({ id }).first();
16
- return auth || null;
17
- }
18
-
19
- async getByUuid(uuid: string): Promise<IAuth | null> {
20
- // Auth table doesn't have uuid, so we'll return null or throw error
21
- return null;
22
- }
23
-
24
- async getByUserId(userId: number): Promise<IAuth | null> {
25
- const auth = await this._knex("auth").where({ userId }).first();
26
- return auth || null;
27
- }
28
-
29
- async getByEmailToken(emailToken: string): Promise<IAuth | null> {
30
- const auth = await this._knex("auth").where({ emailToken }).first();
31
- return auth || null;
32
- }
33
-
34
- async update(id: number, item: Partial<IAuth>): Promise<IAuth | null> {
35
- const [updatedAuth] = await this._knex("auth")
36
- .where({ id })
37
- .update(item)
38
- .returning("*");
39
- return updatedAuth || null;
40
- }
41
-
42
- async delete(id: number): Promise<boolean> {
43
- const result = await this._knex("auth").where({ id }).del();
44
- return result > 0;
45
- }
46
-
47
- async getAll(page: number, limit: number): Promise<IDataPaginator<IAuth>> {
48
- const offset = (page - 1) * limit;
49
-
50
- const [countResult] = await this._knex("auth").count("* as count");
51
- const totalCount = +countResult.count;
52
- const auths = await this._knex("auth").limit(limit).offset(offset);
53
-
54
- return {
55
- success: true,
56
- data: auths,
57
- page,
58
- limit,
59
- count: auths.length,
60
- totalCount,
61
- totalPages: Math.ceil(totalCount / limit),
62
- };
63
- }
64
- }
7
+ private _knex: Knex<any, unknown[]> = KnexManager.getConnection();
8
+
9
+ async create(item: IAuth): Promise<IAuth> {
10
+ const [createdAuth] = await this._knex("auth").insert(item).returning("*");
11
+ return createdAuth;
12
+ }
13
+
14
+ async getById(id: number): Promise<IAuth | null> {
15
+ const auth = await this._knex("auth").where({ id }).first();
16
+ return auth || null;
17
+ }
18
+
19
+ async getByUuid(uuid: string): Promise<IAuth | null> {
20
+ // Auth table doesn't have uuid, so we'll return null or throw error
21
+ return null;
22
+ }
23
+
24
+ async getByUserId(userId: number): Promise<IAuth | null> {
25
+ const auth = await this._knex("auth").where({ userId }).first();
26
+ return auth || null;
27
+ }
28
+
29
+ async getByEmailToken(emailToken: string): Promise<IAuth | null> {
30
+ const auth = await this._knex("auth").where({ emailToken }).first();
31
+ return auth || null;
32
+ }
33
+
34
+ async update(id: number, item: Partial<IAuth>): Promise<IAuth | null> {
35
+ const [updatedAuth] = await this._knex("auth").where({ id }).update(item).returning("*");
36
+ return updatedAuth || null;
37
+ }
38
+
39
+ async delete(id: number): Promise<boolean> {
40
+ const result = await this._knex("auth").where({ id }).del();
41
+ return result > 0;
42
+ }
43
+
44
+ async getAll(page: number, limit: number): Promise<IDataPaginator<IAuth>> {
45
+ const offset = (page - 1) * limit;
46
+
47
+ const [countResult] = await this._knex("auth").count("* as count");
48
+ const totalCount = +countResult.count;
49
+ const auths = await this._knex("auth").limit(limit).offset(offset);
50
+
51
+ return {
52
+ success: true,
53
+ data: auths,
54
+ page,
55
+ limit,
56
+ count: auths.length,
57
+ totalCount,
58
+ totalPages: Math.ceil(totalCount / limit),
59
+ };
60
+ }
61
+ }