fleetmap-reports 2.0.373 → 2.0.375

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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetmap-reports",
3
- "version": "2.0.373",
3
+ "version": "2.0.375",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -3,14 +3,131 @@ const traccarHelper = require('./util/traccar')
3
3
  const odoo = require('./util/odoo')
4
4
  const automaticReports = require('./automaticReports')
5
5
  const { devicesToProcess } = require('./util/device')
6
- const { getTranslations, convertToLocaleString, getLanguage, convertMS, formatNumber, getDates, isClientSide, executeServerSide } = require('./util/utils')
6
+ const { getTranslations, convertToLocaleString, getLanguage, convertMS, formatNumber, isClientSide, executeServerSide } = require('./util/utils')
7
7
  const { getStyle } = require('./reportStyle')
8
8
  const { getUserPartner } = require('fleetmap-partners')
9
9
  const { addTable, headerFromUser } = require('./util/pdfDocument')
10
10
  const jsPDF = require('jspdf')
11
- const { calculateConsumption, calculateTotalFuelFromSensor} = require('./util/fuel')
12
- const { getSummaryByDay } = require('./util/traccar')
13
- const { getDataByDay } = require('./util/trips')
11
+ const { calculateConsumption, calculateTotalFuelFromSensor } = require('./util/fuel')
12
+ const { getSummary } = require('./util/traccar')
13
+ const { calculatePartialTrip } = require('./util/trips')
14
+
15
+ // --- Timezone-consistent day bucketing (fuel-consumption report only) --------
16
+ // The shared getDates / getSummaryByDay / getDataByDay helpers resolve calendar
17
+ // day boundaries from the JS engine's timezone when they run in the browser, but
18
+ // from the user's configured timezone when they run on the server. So the same
19
+ // report computed client-side and server-side attributes midnight-straddling
20
+ // trips (and their distance / fuel / duration) to different days. The helpers
21
+ // below always key off the user's timezone, matching the server, so both
22
+ // execution paths produce identical daily figures. They are local on purpose -
23
+ // changing the shared helpers would move every other report's numbers.
24
+
25
+ function getReportTimezone (userData) {
26
+ const tz = userData.user.attributes.timezone || getUserPartner(userData.user).timezone
27
+ try {
28
+ Intl.DateTimeFormat(undefined, { timeZone: tz })
29
+ return tz
30
+ } catch (e) {
31
+ return undefined
32
+ }
33
+ }
34
+
35
+ // Wall-clock parts of `instant` as seen in `tz` (UTC parts when tz is unusable).
36
+ function zonedParts (instant, tz) {
37
+ const d = new Date(instant)
38
+ if (!tz) {
39
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth(), day: d.getUTCDate(), hour: d.getUTCHours(), minute: d.getUTCMinutes(), second: d.getUTCSeconds() }
40
+ }
41
+ const p = {}
42
+ for (const part of new Intl.DateTimeFormat('en-US', {
43
+ timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
44
+ }).formatToParts(d)) {
45
+ if (part.type !== 'literal') p[part.type] = part.value
46
+ }
47
+ return { year: Number(p.year), month: Number(p.month) - 1, day: Number(p.day), hour: Number(p.hour) % 24, minute: Number(p.minute), second: Number(p.second) }
48
+ }
49
+
50
+ // UTC Date for a given wall-clock time in `tz`. Single correction step - exact
51
+ // except inside the ~1h DST fall-back overlap, and identical on client & server.
52
+ function zonedToUTC (year, month, day, hour, minute, second, tz) {
53
+ const guess = Date.UTC(year, month, day, hour || 0, minute || 0, second || 0)
54
+ if (!tz) return new Date(guess)
55
+ const seen = zonedParts(guess, tz)
56
+ const seenUTC = Date.UTC(seen.year, seen.month, seen.day, seen.hour, seen.minute, seen.second)
57
+ return new Date(guess - (seenUTC - guess))
58
+ }
59
+
60
+ // One noon-UTC marker per calendar day in [from, to], days counted in `tz`.
61
+ // Mirrors the server behaviour of getDates(from, to, tz).
62
+ function getReportDates (from, to, tz) {
63
+ const start = zonedParts(from, tz)
64
+ const end = zonedParts(to, tz)
65
+ const dates = []
66
+ let cursor = Date.UTC(start.year, start.month, start.day, 12, 0, 0, 0)
67
+ const last = Date.UTC(end.year, end.month, end.day, 12, 0, 0, 0)
68
+ while (cursor <= last) {
69
+ dates.push(new Date(cursor))
70
+ cursor += 24 * 60 * 60 * 1000
71
+ }
72
+ return dates
73
+ }
74
+
75
+ function parseHMS (value) {
76
+ const [h, m, s] = String(value).split(':').map(Number)
77
+ return [h || 0, m || 0, s || 0]
78
+ }
79
+
80
+ // Midnight-to-23:59:59 window for `date`'s calendar day, in `tz`. Mirrors the
81
+ // server branch of getDataByDay.
82
+ function getDayWindow (date, tz) {
83
+ const { year, month, day } = zonedParts(date, tz)
84
+ return [zonedToUTC(year, month, day, 0, 0, 0, tz), zonedToUTC(year, month, day, 23, 59, 59, tz)]
85
+ }
86
+
87
+ // Local, timezone-consistent equivalent of getSummaryByDay for this report.
88
+ async function getReportSummaryByDay (dates, userData, traccar, devices, tz, from, to) {
89
+ const days = []
90
+ for (const [index, date] of dates.entries()) {
91
+ const { year, month, day } = zonedParts(date, tz)
92
+
93
+ let startDate = zonedToUTC(year, month, day, ...parseHMS(userData.dayHours.startTime), tz)
94
+ if (index === 0) {
95
+ const fromParts = zonedParts(from, tz)
96
+ const fromClamp = zonedToUTC(year, month, day, fromParts.hour, fromParts.minute, fromParts.second, tz)
97
+ startDate = startDate.getTime() > fromClamp.getTime() ? startDate : fromClamp
98
+ }
99
+
100
+ let endDate = zonedToUTC(year, month, day, ...parseHMS(userData.dayHours.endTime), tz)
101
+ if (index === dates.length - 1) {
102
+ const toParts = zonedParts(to, tz)
103
+ const toClamp = zonedToUTC(year, month, day, toParts.hour, toParts.minute, toParts.second, tz)
104
+ endDate = endDate.getTime() < toClamp.getTime() ? endDate : toClamp
105
+ }
106
+
107
+ const summary = await getSummary(traccar, startDate, endDate, devices)
108
+ days.push({ date, summary })
109
+ }
110
+ return days
111
+ }
112
+
113
+ // Local, timezone-consistent equivalent of getDataByDay for this report.
114
+ async function getReportDataByDay (device, startDate, endDate, data) {
115
+ const tripsByDay = (data.trips || [])
116
+ .filter(t => new Date(t.endTime) > startDate && new Date(t.startTime) < endDate)
117
+ .map(t => ({ ...t }))
118
+ for (const t of tripsByDay) {
119
+ await calculatePartialTrip(device, startDate, endDate, data.route, t)
120
+ }
121
+
122
+ const stopsByDay = (data.stops || [])
123
+ .filter(t => new Date(t.endTime) > startDate && new Date(t.startTime) < endDate)
124
+ .map(t => ({ ...t }))
125
+
126
+ const routeByDay = data.route ? data.route.filter(p => new Date(p.fixTime) > startDate && new Date(p.fixTime) < endDate) : []
127
+ const refuelingPositionsByDay = data.refuelingPositions ? data.refuelingPositions.filter(p => new Date(p.date) > startDate && new Date(p.date) < endDate) : []
128
+
129
+ return { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay }
130
+ }
14
131
 
15
132
  async function createFuelConsumptionReport (from, to, userData, traccar) {
16
133
  console.log('Create FuelConsumption Report')
@@ -41,8 +158,9 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
41
158
  // we never hold more than ~a week of raw GPS positions for the whole device set in memory at
42
159
  // once - a single busy device can have tens of thousands of positions over a month. Chunking the
43
160
  // already-computed dates list, rather than re-deriving dates per raw millisecond sub-range, avoids
44
- // any risk of a boundary day being counted twice (or dropped) due to getDates' timezone rounding.
45
- const allDates = getDates(from, to, userData.user.attributes.timezone || getUserPartner(userData.user).timezone)
161
+ // any risk of a boundary day being counted twice (or dropped) due to timezone rounding.
162
+ const timezone = getReportTimezone(userData)
163
+ const allDates = getReportDates(from, to, timezone)
46
164
  const dateGroups = automaticReports.sliceArray(allDates, 7)
47
165
 
48
166
  for (let chunkIndex = 0; chunkIndex < dateGroups.length; chunkIndex++) {
@@ -51,7 +169,7 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
51
169
  const chunkTo = chunkIndex === dateGroups.length - 1 ? to : dateGroups[chunkIndex + 1][0]
52
170
 
53
171
  const allInOne = await traccarHelper.getAllInOne(traccar, chunkFrom, chunkTo, devices, true, true, true, false)
54
- const summary = await getSummaryByDay(dates, userData, traccar, devices, true, chunkFrom, chunkTo)
172
+ const summary = await getReportSummaryByDay(dates, userData, traccar, devices, timezone, chunkFrom, chunkTo)
55
173
 
56
174
  const fuelServicesData = []
57
175
  if (devices.some(d => d.attributes.odooId)) {
@@ -69,7 +187,8 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
69
187
  const refuelingPositions = await refuelingReport.calculateRefuelingPositions(userData, d, { route, trips, fuelServices: deviceFuelServices })
70
188
  const days = daysByDevice.get(d.id) || []
71
189
  for (const date of dates) {
72
- const { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay } = await getDataByDay(d, date, { trips, stops, route, refuelingPositions }, userData, traccar)
190
+ const [dayStart, dayEnd] = getDayWindow(date, timezone)
191
+ const { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay } = await getReportDataByDay(d, dayStart, dayEnd, { trips, stops, route, refuelingPositions })
73
192
  const summaryByDay = summary.find(a => a.date === date).summary
74
193
  const summaryByDevice = summaryByDay.find(a => a.deviceId === d.id)
75
194
 
@@ -80,9 +199,9 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
80
199
  'fuel_high_threshold' in d.attributes &&
81
200
  d.attributes.fuel_tank_capacity
82
201
 
83
- const spentFuel = hasFuelSensor ?
84
- tripsByDay.length > 0 ? calculateTotalFuelFromSensor(tripsByDay, stopsByDay, route, d, firstStopIndex) : 0
85
- : calculateConsumption(d, { trips: tripsByDay, stops: stopsByDay, route: routeByDay, summary: summaryByDevice })
202
+ const spentFuel = hasFuelSensor
203
+ ? tripsByDay.length > 0 ? calculateTotalFuelFromSensor(tripsByDay, stopsByDay, route, d, firstStopIndex) : 0
204
+ : calculateConsumption(d, { trips: tripsByDay, stops: stopsByDay, route: routeByDay, summary: summaryByDevice })
86
205
 
87
206
  const dataRow = {
88
207
  date,
@@ -77,7 +77,7 @@ function processDevices (from, to, devices, data, userData) {
77
77
  }
78
78
 
79
79
  routeInsideGeofence.every(p => {
80
- const geofence = userData.geofences.filter(g => g && g.area.startsWith('POLYGON')).find(g =>
80
+ const geofence = userData.geofences.filter(g => g && g.area && g.area.startsWith('POLYGON')).find(g =>
81
81
  insideGeofence({ latitude: p.latitude, longitude: p.longitude }, g)
82
82
  )
83
83
  if (geofence) {