@tmlmobilidade/export-data 20260526.2143.45 → 20260527.947.12

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.
package/dist/index.js CHANGED
@@ -16,10 +16,11 @@ import { exportHashedShapesGeoJSON } from './tasks/hashed-shapes/hashed-shapes-g
16
16
  import { exportRidesRaw } from './tasks/rides/rides-raw.js';
17
17
  import { exportSamsRaw } from './tasks/sams/sams-raw.js';
18
18
  import { exportVehicleEventsRaw } from './tasks/vehicle-events/vehicle-events-raw.js';
19
- import { exportTypeLabels, exportTypesWithoutFilters } from './types.js';
19
+ import { exportTypeLabels, exportTypesWithoutEntityFilters, exportTypesWithoutFilters } from './types.js';
20
20
  import { initExportContext } from './utils/init-context.js';
21
21
  import { intro, log, outro, tasks } from '@clack/prompts';
22
22
  import { ASCII_CM_SHORT } from '@tmlmobilidade/consts';
23
+ import { exportValidationsPMunicipalities } from './tasks/municipalities-validations/validations_p_municipalities.js';
23
24
  import { exportExecutiveSummary } from './tasks/executive-summary-setup/index.js';
24
25
  /* * */
25
26
  await (async function main() {
@@ -41,9 +42,9 @@ await (async function main() {
41
42
  // Request the export types and which filters to apply
42
43
  const exportTypes = await promptExportTypes();
43
44
  //
44
- // Check if all selected export types don't require filters
45
- const selectedTypesWithoutFilters = exportTypes.filter(type => exportTypesWithoutFilters.includes(type));
46
- const shouldSkipFilters = selectedTypesWithoutFilters.length === exportTypes.length && exportTypes.length > 0;
45
+ // Check if all selected export types don't require entity filters or dates
46
+ const shouldSkipEntityFilters = exportTypes.length > 0 && exportTypes.every(type => exportTypesWithoutEntityFilters.includes(type) || exportTypesWithoutFilters.includes(type));
47
+ const shouldSkipDates = exportTypes.length > 0 && exportTypes.every(type => exportTypesWithoutFilters.includes(type));
47
48
  //
48
49
  // For hashed_shapes export, prompt for hashedshape IDs
49
50
  let hashedShapeIds = [];
@@ -57,8 +58,8 @@ await (async function main() {
57
58
  validationGroupFields = await promptValidationGroupFields();
58
59
  }
59
60
  //
60
- // Skip filters and dates if all selected export types don't require them
61
- if (!shouldSkipFilters) {
61
+ // Skip entity filters and/or dates when all selected export types don't require them
62
+ if (!shouldSkipEntityFilters) {
62
63
  const filterTypes = await promptFilterTypes();
63
64
  //
64
65
  // For the selected filters, request the filter values
@@ -72,6 +73,8 @@ await (async function main() {
72
73
  context.filters.stop_ids = await promptFilterByStopIds();
73
74
  if (filterTypes.includes('vehicle-ids'))
74
75
  context.filters.vehicle_ids = await promptFilterByVehicleIds();
76
+ }
77
+ if (!shouldSkipDates) {
75
78
  context.dates = await promptFilterByDates();
76
79
  }
77
80
  //
@@ -112,6 +115,11 @@ await (async function main() {
112
115
  task: async (message) => await exportExecutiveSummary({ context, message }),
113
116
  title: exportTypeLabels['executive-summary'],
114
117
  },
118
+ {
119
+ enabled: exportTypes.includes('validations-p-municipalities'),
120
+ task: async (message) => await exportValidationsPMunicipalities({ context, message }),
121
+ title: exportTypeLabels['validations-p-municipalities'],
122
+ },
115
123
  ]);
116
124
  //
117
125
  // Terminate the process
@@ -26,6 +26,9 @@ export async function promptExportTypes() {
26
26
  '6. Sumário Executivo': [
27
27
  { label: exportTypeLabels['executive-summary'], value: 'executive-summary' },
28
28
  ],
29
+ '7. Validações por município': [
30
+ { label: exportTypeLabels['validations-p-municipalities'], value: 'validations-p-municipalities' },
31
+ ],
29
32
  },
30
33
  required: true,
31
34
  });
@@ -1,68 +1,55 @@
1
- // ! THIS IS NOT WORKING, BECAUSE THE @tmlmobilidade/go-performance-pckg-dates is not a published package yet.
2
- // import { type TaskProps } from '../../types.js';
3
- // import { Dates } from '@tmlmobilidade/dates';
4
- // import { type CalendarEntry, fetchCalendarData } from '@tmlmobilidade/go-performance-pckg-dates';
5
- // import { rides } from '@tmlmobilidade/interfaces';
6
- export {};
7
- // export interface AgencyAverageRidesByDayTypeResult {
8
- // agencyId: string
9
- // averageRides: number
10
- // dayType: string
11
- // }
12
- // export async function calculateAverageRidesByAgencyByDayType(
13
- // { context, message }: TaskProps,
14
- // ): Promise<AgencyAverageRidesByDayTypeResult[]> {
15
- // message('Calculating average rides per agency by day type...');
16
- // const ridesCollection = await rides.getCollection();
17
- // const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
18
- // const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
19
- // // Load calendar JSON
20
- // const calendarJson = await fetchCalendarData();
21
- // // Build calendar map: operational_date -> CalendarEntry
22
- // const calendarMap = new Map<string, CalendarEntry>();
23
- // for (const day of calendarJson) {
24
- // calendarMap.set(day.date.toString(), day);
25
- // }
26
- // // Mongo aggregation: count total rides per agency and date
27
- // const aggCursor = ridesCollection.aggregate([
28
- // {
29
- // $match: {
30
- // agency_id: { $exists: true },
31
- // start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
32
- // system_status: 'complete',
33
- // },
34
- // },
35
- // {
36
- // $group: {
37
- // _id: { agencyId: '$agency_id', operationalDate: '$operational_date' },
38
- // totalTrips: { $sum: 1 },
39
- // },
40
- // },
41
- // ]);
42
- // // Map in-memory: (agencyId + dayType) -> totalRides / totalDays
43
- // const aggregationMap = new Map<string, { agencyId: string, dayType: string, totalDays: number, totalRides: number }>();
44
- // for await (const doc of aggCursor) {
45
- // const agencyId = doc._id.agencyId;
46
- // const operationalDate = doc._id.operationalDate;
47
- // const calendarProps = calendarMap.get(operationalDate);
48
- // const dayType = calendarProps?.day_type ?? '1';
49
- // const key = `${agencyId}:${dayType}`;
50
- // if (!aggregationMap.has(key)) {
51
- // aggregationMap.set(key, { agencyId, dayType, totalDays: 0, totalRides: 0 });
52
- // }
53
- // const entry = aggregationMap.get(key);
54
- // entry.totalRides += doc.totalTrips;
55
- // entry.totalDays += 1;
56
- // }
57
- // // Build final results
58
- // const results: AgencyAverageRidesByDayTypeResult[] = [];
59
- // for (const value of aggregationMap.values()) {
60
- // results.push({
61
- // agencyId: value.agencyId,
62
- // averageRides: parseFloat((value.totalRides / value.totalDays).toFixed(2)),
63
- // dayType: value.dayType,
64
- // });
65
- // }
66
- // message(`Processed ${results.length} agency/day-type combinations`);
67
- // return results;
68
- // }
1
+ import { Dates } from '@tmlmobilidade/dates';
2
+ import { rides } from '@tmlmobilidade/interfaces';
3
+ export async function calculateAverageRidesByAgencyByDayType({ context, message }) {
4
+ message('Calculating average rides per agency by day type...');
5
+ const ridesCollection = await rides.getCollection();
6
+ // Load calendar JSON
7
+ const calendarJson = await Dates.fetchCalendarData();
8
+ // Build calendar map: operational_date -> CalendarEntry
9
+ const calendarMap = new Map();
10
+ for (const day of calendarJson) {
11
+ calendarMap.set(day.date.toString(), day);
12
+ }
13
+ // Mongo aggregation: count total rides per agency and date
14
+ const aggCursor = ridesCollection.aggregate([
15
+ {
16
+ $match: {
17
+ agency_id: { $exists: true },
18
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
19
+ system_status: 'complete',
20
+ },
21
+ },
22
+ {
23
+ $group: {
24
+ _id: { agencyId: '$agency_id', operationalDate: '$operational_date' },
25
+ totalTrips: { $sum: 1 },
26
+ },
27
+ },
28
+ ]);
29
+ // Map in-memory: (agencyId + dayType) -> totalRides / totalDays
30
+ const aggregationMap = new Map();
31
+ for await (const doc of aggCursor) {
32
+ const agencyId = doc._id.agencyId;
33
+ const operationalDate = doc._id.operationalDate;
34
+ const calendarProps = calendarMap.get(operationalDate);
35
+ const dayType = calendarProps?.day_type ?? '1';
36
+ const key = `${agencyId}:${dayType}`;
37
+ if (!aggregationMap.has(key)) {
38
+ aggregationMap.set(key, { agencyId, dayType, totalDays: 0, totalRides: 0 });
39
+ }
40
+ const entry = aggregationMap.get(key);
41
+ entry.totalRides += doc.totalTrips;
42
+ entry.totalDays += 1;
43
+ }
44
+ // Build final results
45
+ const results = [];
46
+ for (const value of aggregationMap.values()) {
47
+ results.push({
48
+ agencyId: value.agencyId,
49
+ averageRides: parseFloat((value.totalRides / value.totalDays).toFixed(2)),
50
+ dayType: value.dayType,
51
+ });
52
+ }
53
+ message(`Processed ${results.length} agency/day-type combinations`);
54
+ return results;
55
+ }
@@ -8,17 +8,20 @@ export async function calculateObservedTrips({ context, message }) {
8
8
  message('Calculating observed trips metrics...');
9
9
  // Get the rides collection from MongoDB
10
10
  const ridesCollection = await rides.getCollection();
11
- // Convert context dates to YYYYMMDD
12
- const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
13
- const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
14
- message(`Date range: ${startDateStr} to ${endDateStr}`);
11
+ message(`Date range: ${context.dates.start} to ${context.dates.end}`);
15
12
  // Trips with passengers_observed = 0
16
13
  const pipelineZero = [
17
14
  {
18
15
  $match: {
16
+ $expr: {
17
+ $or: [
18
+ { $eq: ['$analysis.SIMPLE_ONE_APEX_VALIDATION.grade', 'pass'] },
19
+ { $eq: ['$analysis.SIMPLE_THREE_VEHICLE_EVENTS.grade', 'pass'] },
20
+ ],
21
+ },
19
22
  agency_id: { $exists: true },
23
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
20
24
  passengers_observed: 0,
21
- start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
22
25
  },
23
26
  },
24
27
  {
@@ -47,7 +50,7 @@ export async function calculateObservedTrips({ context, message }) {
47
50
  {
48
51
  $match: {
49
52
  agency_id: { $exists: true },
50
- operational_date: { $gte: startDateStr, $lte: endDateStr },
53
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
51
54
  passengers_observed: { $ne: 0 },
52
55
  },
53
56
  },
@@ -1,6 +1,6 @@
1
1
  import { log } from 'node:console';
2
2
  import fs from 'node:fs';
3
- // import { calculateAverageRidesByAgencyByDayType } from './avg-trips-day.js';
3
+ import { calculateAverageRidesByAgencyByDayType } from './avg-trips-day.js';
4
4
  import { calculateObservedTrips } from './empty-runs.js';
5
5
  import { calculateSupplyMetrics } from './km.js';
6
6
  import { calculateMedianSpeed } from './median-speed.js';
@@ -144,6 +144,7 @@ export async function exportExecutiveSummary({ context, message }) {
144
144
  km: { observed: 0, scheduled: 0, observedPct: 0 },
145
145
  product_id: {
146
146
  'id-prod-navegante-metro': 0,
147
+ 'id-prod-navegante-metro-418-gratuito': 0,
147
148
  'id-prod-navegante-metro-sub23-grat': 0,
148
149
  'id-prod-navegante-65': 0,
149
150
  'others': 0,
@@ -178,9 +179,12 @@ export async function exportExecutiveSummary({ context, message }) {
178
179
  const metro = byProduct['id-prod-navegante-metro'] ?? 0;
179
180
  const sub23 = byProduct['id-prod-navegante-metro-sub23-grat'] ?? 0;
180
181
  const p65 = byProduct['id-prod-navegante-65'] ?? 0;
182
+ const metro418 = byProduct['id-prod-navegante-metro-418-gratuito'] ?? 0;
183
+ ;
181
184
  const totalProducts = Object.values(byProduct).reduce((a, b) => a + b, 0);
182
- const others = totalProducts - (metro + sub23 + p65);
185
+ const others = totalProducts - (metro + sub23 + p65 + metro418);
183
186
  t.product_id['id-prod-navegante-metro'] += metro;
187
+ t.product_id['id-prod-navegante-metro-418-gratuito'] += metro418;
184
188
  t.product_id['id-prod-navegante-metro-sub23-grat'] += sub23;
185
189
  t.product_id['id-prod-navegante-65'] += p65;
186
190
  t.product_id.others += others;
@@ -225,13 +229,14 @@ export async function exportExecutiveSummary({ context, message }) {
225
229
  }
226
230
  }
227
231
  // Add avg_trips_day_type
228
- // const avgRidesByDayType = await calculateAverageRidesByAgencyByDayType({ context, message });
229
- // for (const row of avgRidesByDayType) {
230
- // const agencyTotals = totalsByAgency[row.agencyId];
231
- // if (!agencyTotals) continue;
232
- // const dayTypeKey = `avg_trips_day_type ${row.dayType}` as const;
233
- // agencyTotals[dayTypeKey] = row.averageRides;
234
- // }
232
+ const avgRidesByDayType = await calculateAverageRidesByAgencyByDayType({ context, message });
233
+ for (const row of avgRidesByDayType) {
234
+ const agencyTotals = totalsByAgency[row.agencyId];
235
+ if (!agencyTotals)
236
+ continue;
237
+ const dayTypeKey = `avg_trips_day_type ${row.dayType}`;
238
+ agencyTotals[dayTypeKey] = row.averageRides;
239
+ }
235
240
  // Build final output
236
241
  const finalOutput = {
237
242
  timestamp: Date.now(),
@@ -1,4 +1,3 @@
1
- import { Dates } from '@tmlmobilidade/dates';
2
1
  import { rides } from '@tmlmobilidade/interfaces';
3
2
  /**
4
3
  * Helper to calculate median from an array of numbers
@@ -22,8 +21,8 @@ function median(values) {
22
21
  export async function calculateMedianSpeed({ context, message }) {
23
22
  message('Calculating median speeds per agency...');
24
23
  const ridesCollection = await rides.getCollection();
25
- const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
26
- const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
24
+ // const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
25
+ // const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
27
26
  const pipeline = [
28
27
  {
29
28
  $match: {
@@ -32,7 +31,8 @@ export async function calculateMedianSpeed({ context, message }) {
32
31
  end_time_observed: { $exists: true },
33
32
  extension_scheduled: { $gt: 0 },
34
33
  start_time_observed: { $exists: true },
35
- start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
34
+ // start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
35
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
36
36
  },
37
37
  },
38
38
  {
@@ -1,16 +1,13 @@
1
- import { Dates } from '@tmlmobilidade/dates';
2
1
  import { rides } from '@tmlmobilidade/interfaces';
3
2
  export async function calculatePassengersPerKm({ context }) {
4
3
  console.log('Calculating passengers per km for executed trips...');
5
4
  const ridesCollection = await rides.getCollection();
6
- const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
7
- const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
8
- console.log(`Date range: ${startDateStr} to ${endDateStr}`);
5
+ console.log(`Date range: ${context.dates.start} to ${context.dates.end}`);
9
6
  const pipeline = [
10
7
  {
11
8
  $match: {
12
9
  agency_id: { $exists: true },
13
- start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
10
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
14
11
  },
15
12
  },
16
13
  {
@@ -57,10 +54,6 @@ export async function calculatePassengersPerKm({ context }) {
57
54
  const aggCursor = ridesCollection.aggregate(pipeline);
58
55
  const results = [];
59
56
  for await (const doc of aggCursor) {
60
- // // Total Passengers, Km, passengersPerKm DEBUG
61
- // console.log(
62
- // `[DEBUG] Agency: ${doc.agencyId} | Total Passengers: ${doc.totalPassengers} | Total Km: ${doc.totalKm} | passengersPerKm: ${doc.passengersPerKm}`,
63
- // );
64
57
  results.push({
65
58
  agencyId: doc.agencyId,
66
59
  passengersPerKm: doc.passengersPerKm,
@@ -7,14 +7,12 @@ import { rides } from '@tmlmobilidade/interfaces';
7
7
  export async function calculatePlannedTrips({ context, message }) {
8
8
  message('Calculating planned trips...');
9
9
  const ridesCollection = await rides.getCollection();
10
- const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
11
- const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
12
- message(`Date range: ${startDateStr} to ${endDateStr}`);
10
+ message(`Date range: ${context.dates.start} to ${context.dates.end}`);
13
11
  const pipeline = [
14
12
  {
15
13
  $match: {
16
14
  agency_id: { $exists: true },
17
- start_time_scheduled: { $gte: startDateStr, $lte: endDateStr },
15
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
18
16
  },
19
17
  },
20
18
  {
@@ -52,14 +50,12 @@ export async function calculatePlannedTrips({ context, message }) {
52
50
  export async function calculateCompletedTrips({ context, message }) {
53
51
  message('Calculating completed trips...');
54
52
  const ridesCollection = await rides.getCollection();
55
- const startDateStr = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').toFormat('yyyyMMdd');
56
- const endDateStr = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').toFormat('yyyyMMdd');
57
- message(`Date range: ${startDateStr} to ${endDateStr}`);
53
+ message(`Date range: ${context.dates.start} to ${context.dates.end}`);
58
54
  const pipeline = [
59
55
  {
60
56
  $match: {
61
57
  agency_id: { $exists: true },
62
- operational_date: { $gte: startDateStr, $lte: endDateStr },
58
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
63
59
  },
64
60
  },
65
61
  {
@@ -3,7 +3,7 @@ import { rides } from '@tmlmobilidade/interfaces';
3
3
  import { Logger } from '@tmlmobilidade/logger';
4
4
  import { Timer } from '@tmlmobilidade/timer';
5
5
  /* Helper Function */
6
- async function processDailyRides(stream, results, agencies, agency43Trips) {
6
+ async function processDailyRides(stream, results, agencies) {
7
7
  for (const rideData of stream) {
8
8
  const agency = rideData.agency_id;
9
9
  if (!agencies.includes(agency))
@@ -20,7 +20,11 @@ async function processDailyRides(stream, results, agencies, agency43Trips) {
20
20
  results.agencies[agency].early_rides++;
21
21
  results.total.early_rides++;
22
22
  }
23
- if (expectedStart >= 5) {
23
+ if (expectedStart > -1 && expectedStart <= 5) {
24
+ results.agencies[agency].ontime_rides++;
25
+ results.total.ontime_rides++;
26
+ }
27
+ if (expectedStart > 5) {
24
28
  results.agencies[agency].five_min_delays++;
25
29
  results.total.five_min_delays++;
26
30
  const passengers = rideData.passengers_observed ?? 0;
@@ -28,13 +32,6 @@ async function processDailyRides(stream, results, agencies, agency43Trips) {
28
32
  = (results.agencies[agency]['pax-affected-by-delay'] ?? 0) + passengers;
29
33
  results.total['pax-affected-by-delay']
30
34
  = (results.total['pax-affected-by-delay'] ?? 0) + passengers;
31
- if (agency === '43') {
32
- agency43Trips.push(rideData.trip_id);
33
- }
34
- }
35
- if (expectedStart > -1 && expectedStart < 5) {
36
- results.agencies[agency].ontime_rides++;
37
- results.total.ontime_rides++;
38
35
  }
39
36
  }
40
37
  }
@@ -53,17 +50,20 @@ function calculatePercentages(metrics) {
53
50
  export const calculateDailyServiceCompliance = async (context) => {
54
51
  const agencies = ['41', '42', '43', '44'];
55
52
  const ridesCollection = await rides.getCollection();
56
- const agency43Trips = [];
57
53
  const processingTimer = new Timer();
58
54
  let countProcessed = 0;
59
55
  const resultsByDay = {};
60
- const startDate = Dates.fromOperationalDate(context.dates.start, 'Europe/Lisbon').unix_timestamp;
61
- const endDate = Dates.fromOperationalDate(context.dates.end, 'Europe/Lisbon').unix_timestamp;
56
+ // const ridesQuery = {
57
+ // operational_date: { $gte: context.dates.start, $lte: context.dates.end },
58
+ // };
62
59
  const ridesQuery = {
63
- start_time_scheduled: {
64
- $gte: startDate,
65
- $lte: endDate,
60
+ $expr: {
61
+ $or: [
62
+ { $eq: ['$analysis.SIMPLE_ONE_APEX_VALIDATION.grade', 'pass'] },
63
+ { $eq: ['$analysis.SIMPLE_THREE_VEHICLE_EVENTS.grade', 'pass'] },
64
+ ],
66
65
  },
66
+ operational_date: { $gte: context.dates.start, $lte: context.dates.end },
67
67
  };
68
68
  const allRidesStream = ridesCollection.find(ridesQuery).batchSize(100_000).stream();
69
69
  for await (const ride of allRidesStream) {
@@ -83,7 +83,7 @@ export const calculateDailyServiceCompliance = async (context) => {
83
83
  };
84
84
  });
85
85
  }
86
- await processDailyRides([rideData], resultsByDay[dayOperationalStr], agencies, agency43Trips);
86
+ await processDailyRides([rideData], resultsByDay[dayOperationalStr], agencies);
87
87
  countProcessed++;
88
88
  if (countProcessed % 100 === 0) {
89
89
  // Logger.info(`Processed ${countProcessed} rides so far...`);
@@ -107,8 +107,5 @@ export const calculateDailyServiceCompliance = async (context) => {
107
107
  total: totalWithPct,
108
108
  };
109
109
  }
110
- /* LOG: Export agency 43 trips */
111
- // fs.writeFileSync('agency_43_trips.json', JSON.stringify(agency43Trips, null, 2));
112
- // Logger.info(`Exported ${agency43Trips.length} trip_id for agency 43 to agency_43_trips.json`);
113
110
  return formattedResults;
114
111
  };
@@ -0,0 +1,231 @@
1
+ import { Dates } from '@tmlmobilidade/dates';
2
+ import { municipalities, simplifiedApexValidations, stops } from '@tmlmobilidade/interfaces';
3
+ import ExcelJS from 'exceljs';
4
+ import fs from 'node:fs';
5
+ /* * */
6
+ const TASK_ID = 'validations-p-municipalities';
7
+ /* * */
8
+ export async function exportValidationsPMunicipalities({ context, message, }) {
9
+ //
10
+ message('A iniciar exportação de validações por município...');
11
+ //
12
+ // Prepare output directory
13
+ if (!fs.existsSync(context.output)) {
14
+ fs.mkdirSync(context.output, { recursive: true });
15
+ }
16
+ //
17
+ // Collections
18
+ message('A obter collections...');
19
+ const [stopsCol, validationsCol, municipalitiesCol] = await Promise.all([
20
+ stops.getCollection(),
21
+ simplifiedApexValidations.getCollection(),
22
+ municipalities.getCollection(),
23
+ ]);
24
+ if (!stopsCol || !validationsCol || !municipalitiesCol) {
25
+ throw new Error('Missing collections');
26
+ }
27
+ //
28
+ // Calendar
29
+ message('A obter calendário...');
30
+ const calendar = await Dates.fetchCalendarData();
31
+ const calendarMap = new Map();
32
+ for (const c of calendar) {
33
+ calendarMap.set(c.date, {
34
+ day_type: c.day_type,
35
+ period: c.period,
36
+ });
37
+ }
38
+ //
39
+ // Municipalities
40
+ message('A obter municípios...');
41
+ const municipalitiesData = await municipalitiesCol
42
+ .find({})
43
+ .toArray();
44
+ const municipalityMap = new Map();
45
+ for (const municipality of municipalitiesData) {
46
+ municipalityMap.set(municipality._id, municipality.properties?.name || 'unknown');
47
+ }
48
+ message(`Municípios encontrados: ${municipalityMap.size}`);
49
+ //
50
+ // Stops
51
+ message('A obter paragens...');
52
+ const stopsData = await stopsCol
53
+ .aggregate([
54
+ {
55
+ $unwind: '$flags',
56
+ },
57
+ {
58
+ $match: {
59
+ 'flags.agency_ids': {
60
+ $in: ['41', '42', '43', '44'],
61
+ },
62
+ },
63
+ },
64
+ {
65
+ $project: {
66
+ _id: 0,
67
+ municipality_id: 1,
68
+ stop_join_id: {
69
+ $toString: {
70
+ $ifNull: ['$flags.stop_id', '$_id'],
71
+ },
72
+ },
73
+ },
74
+ },
75
+ ])
76
+ .toArray();
77
+ message(`Paragens encontradas: ${stopsData.length}`);
78
+ //
79
+ // Stop lookup
80
+ const stopMap = new Map();
81
+ for (const stop of stopsData) {
82
+ stopMap.set(stop.stop_join_id, stop.municipality_id);
83
+ }
84
+ message(`Stop map construída: ${stopMap.size}`);
85
+ //
86
+ // Dates
87
+ const startTimestamp = Dates
88
+ .fromOperationalDate(context.dates.start, 'Europe/Lisbon')
89
+ .set({
90
+ hour: 4,
91
+ millisecond: 0,
92
+ minute: 0,
93
+ second: 0,
94
+ })
95
+ .unix_timestamp;
96
+ const endTimestamp = Dates
97
+ .fromOperationalDate(context.dates.end, 'Europe/Lisbon')
98
+ .plus({ days: 1 })
99
+ .set({
100
+ hour: 4,
101
+ millisecond: 0,
102
+ minute: 0,
103
+ second: 0,
104
+ })
105
+ .unix_timestamp;
106
+ //
107
+ // Validations
108
+ message('A agregar validações...');
109
+ const validations = await validationsCol
110
+ .aggregate([
111
+ {
112
+ $match: {
113
+ agency_id: {
114
+ $in: ['41', '42', '43', '44'],
115
+ },
116
+ created_at: {
117
+ $gte: startTimestamp,
118
+ $lt: endTimestamp,
119
+ },
120
+ is_passenger: true,
121
+ },
122
+ },
123
+ {
124
+ $addFields: {
125
+ operational_date: {
126
+ $let: {
127
+ in: {
128
+ $dateToString: {
129
+ date: {
130
+ $cond: {
131
+ else: '$$eventDate',
132
+ if: { $lt: ['$$hour', 4] },
133
+ then: {
134
+ $dateSubtract: {
135
+ amount: 1,
136
+ startDate: '$$eventDate',
137
+ unit: 'day',
138
+ },
139
+ },
140
+ },
141
+ },
142
+ format: '%Y%m%d',
143
+ timezone: 'Europe/Lisbon',
144
+ },
145
+ },
146
+ vars: {
147
+ eventDate: { $toDate: '$created_at' },
148
+ hour: {
149
+ $hour: {
150
+ date: { $toDate: '$created_at' },
151
+ timezone: 'Europe/Lisbon',
152
+ },
153
+ },
154
+ },
155
+ },
156
+ },
157
+ },
158
+ },
159
+ {
160
+ $group: {
161
+ _id: {
162
+ agency_id: '$agency_id',
163
+ date: '$operational_date',
164
+ stop_id: '$stop_id',
165
+ },
166
+ validations: { $sum: 1 },
167
+ },
168
+ },
169
+ ], { allowDiskUse: true })
170
+ .toArray();
171
+ message(`Grupos de validações: ${validations.length}`);
172
+ //
173
+ // Final aggregation
174
+ message('A construir dataset final...');
175
+ const finalMap = new Map();
176
+ for (const validation of validations) {
177
+ const municipalityId = stopMap.get(String(validation._id.stop_id));
178
+ if (!municipalityId)
179
+ continue;
180
+ const municipality = municipalityMap.get(municipalityId);
181
+ if (!municipality)
182
+ continue;
183
+ const key = [
184
+ validation._id.date,
185
+ validation._id.agency_id,
186
+ municipality,
187
+ ].join('|');
188
+ finalMap.set(key, (finalMap.get(key) || 0) + validation.validations);
189
+ }
190
+ message(`Rows finais: ${finalMap.size}`);
191
+ //
192
+ // Excel
193
+ message('A gerar Excel...');
194
+ const workbook = new ExcelJS.Workbook();
195
+ const worksheet = workbook.addWorksheet('municipalities_validations');
196
+ worksheet.columns = [
197
+ { header: 'Dia', key: 'day', width: 15 },
198
+ { header: 'Dia tipo', key: 'day_type', width: 15 },
199
+ { header: 'Periodo', key: 'period', width: 15 },
200
+ { header: 'Operador', key: 'operator', width: 15 },
201
+ { header: 'Municipio', key: 'municipality', width: 30 },
202
+ { header: 'Validations', key: 'validations', width: 20 },
203
+ ];
204
+ //
205
+ // Rows
206
+ for (const [key, validationsCount] of finalMap.entries()) {
207
+ const [day, operator, municipality] = key.split('|');
208
+ const calendarInfo = calendarMap.get(day);
209
+ worksheet.addRow({
210
+ day,
211
+ day_type: calendarInfo?.day_type || '',
212
+ municipality,
213
+ operator,
214
+ period: calendarInfo?.period || '',
215
+ validations: validationsCount,
216
+ });
217
+ }
218
+ //
219
+ // Style
220
+ worksheet.getRow(1).font = { bold: true };
221
+ worksheet.autoFilter = {
222
+ from: 'A1',
223
+ to: 'F1',
224
+ };
225
+ //
226
+ // Save
227
+ const outputFile = `${context.output}/${TASK_ID}-${context.dates.start}-${context.dates.end}.xlsx`;
228
+ await workbook.xlsx.writeFile(outputFile);
229
+ message(`Export concluído: ${outputFile}`);
230
+ //
231
+ }
package/dist/types.js CHANGED
@@ -8,7 +8,15 @@ export const exportTypeLabels = {
8
8
  'validations-aggregated': '1.1. Validações agregadas (escolher campos)',
9
9
  'validations-raw': '1.0. Validações em bruto',
10
10
  'vehicle-events-raw': '3.0. Vehicle Events em bruto',
11
+ 'validations-p-municipalities': '7.0. Validações por município',
11
12
  };
13
+ /**
14
+ * Export types that don't require entity filters (agency, line, stop, etc.).
15
+ * These exports may still require dates.
16
+ */
17
+ export const exportTypesWithoutEntityFilters = [
18
+ 'validations-p-municipalities',
19
+ ];
12
20
  /**
13
21
  * Export types that don't require filters or dates.
14
22
  * These exports use their own specific input methods (e.g., IDs).
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@tmlmobilidade/export-data",
3
3
  "description": "CLI tool to export data from GO.",
4
- "version": "20260526.2143.45",
4
+ "version": "20260527.947.12",
5
5
  "author": {
6
6
  "email": "iso@tmlmobilidade.pt",
7
7
  "name": "TML-ISO"
@@ -42,7 +42,8 @@
42
42
  "@tmlmobilidade/interfaces": "*",
43
43
  "@tmlmobilidade/strings": "*",
44
44
  "@tmlmobilidade/timer": "*",
45
- "@tmlmobilidade/writers": "*"
45
+ "@tmlmobilidade/writers": "*",
46
+ "exceljs": "^4.4.0"
46
47
  },
47
48
  "devDependencies": {
48
49
  "@tmlmobilidade/tsconfig": "*",