@tmlmobilidade/export-data 20260526.2143.45 → 20260527.939.40
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 +14 -6
- package/dist/prompts/export-types.js +3 -0
- package/dist/tasks/executive-summary-setup/avg-trips-day.js +55 -68
- package/dist/tasks/executive-summary-setup/empty-runs.js +9 -6
- package/dist/tasks/executive-summary-setup/index.js +14 -9
- package/dist/tasks/executive-summary-setup/median-speed.js +4 -4
- package/dist/tasks/executive-summary-setup/paxperkm.js +2 -9
- package/dist/tasks/executive-summary-setup/trips.js +4 -8
- package/dist/tasks/executive-summary-setup/tripstatus.js +16 -19
- package/dist/tasks/municipalities-validations/validations_p_municipalities.js +231 -0
- package/dist/types.js +8 -0
- package/package.json +3 -2
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
|
|
46
|
-
const
|
|
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
|
|
61
|
-
if (!
|
|
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
|
-
|
|
2
|
-
|
|
3
|
-
|
|
4
|
-
|
|
5
|
-
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
//
|
|
9
|
-
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
13
|
-
//
|
|
14
|
-
|
|
15
|
-
|
|
16
|
-
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
21
|
-
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
//
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
//
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
229
|
-
|
|
230
|
-
|
|
231
|
-
|
|
232
|
-
|
|
233
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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:
|
|
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
|
|
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
|
|
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
|
|
61
|
-
|
|
56
|
+
// const ridesQuery = {
|
|
57
|
+
// operational_date: { $gte: context.dates.start, $lte: context.dates.end },
|
|
58
|
+
// };
|
|
62
59
|
const ridesQuery = {
|
|
63
|
-
|
|
64
|
-
$
|
|
65
|
-
|
|
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
|
|
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": "
|
|
4
|
+
"version": "20260527.939.40",
|
|
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": "*",
|