fleetmap-reports 2.0.374 → 2.0.376

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.374",
3
+ "version": "2.0.376",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -3,14 +3,120 @@ 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
+ function getReportTimezone (userData) {
16
+ const tz = userData.user.attributes.timezone || getUserPartner(userData.user).timezone
17
+ try {
18
+ Intl.DateTimeFormat(undefined, { timeZone: tz })
19
+ return tz
20
+ } catch (e) {
21
+ return undefined
22
+ }
23
+ }
24
+
25
+ // Wall-clock parts of `instant` as seen in `tz` (UTC parts when tz is unusable).
26
+ function zonedParts (instant, tz) {
27
+ const d = new Date(instant)
28
+ if (!tz) {
29
+ return { year: d.getUTCFullYear(), month: d.getUTCMonth(), day: d.getUTCDate(), hour: d.getUTCHours(), minute: d.getUTCMinutes(), second: d.getUTCSeconds() }
30
+ }
31
+ const p = {}
32
+ for (const part of new Intl.DateTimeFormat('en-US', {
33
+ timeZone: tz, year: 'numeric', month: '2-digit', day: '2-digit', hour: '2-digit', minute: '2-digit', second: '2-digit', hour12: false
34
+ }).formatToParts(d)) {
35
+ if (part.type !== 'literal') p[part.type] = part.value
36
+ }
37
+ 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) }
38
+ }
39
+
40
+ // UTC Date for a given wall-clock time in `tz`. Single correction step - exact
41
+ // except inside the ~1h DST fall-back overlap, and identical on client & server.
42
+ function zonedToUTC (year, month, day, hour, minute, second, tz) {
43
+ const guess = Date.UTC(year, month, day, hour || 0, minute || 0, second || 0)
44
+ if (!tz) return new Date(guess)
45
+ const seen = zonedParts(guess, tz)
46
+ const seenUTC = Date.UTC(seen.year, seen.month, seen.day, seen.hour, seen.minute, seen.second)
47
+ return new Date(guess - (seenUTC - guess))
48
+ }
49
+
50
+ // One noon-UTC marker per calendar day in [from, to], days counted in `tz`.
51
+ // Mirrors the server behaviour of getDates(from, to, tz).
52
+ function getReportDates (from, to, tz) {
53
+ const start = zonedParts(from, tz)
54
+ const end = zonedParts(to, tz)
55
+ const dates = []
56
+ let cursor = Date.UTC(start.year, start.month, start.day, 12, 0, 0, 0)
57
+ const last = Date.UTC(end.year, end.month, end.day, 12, 0, 0, 0)
58
+ while (cursor <= last) {
59
+ dates.push(new Date(cursor))
60
+ cursor += 24 * 60 * 60 * 1000
61
+ }
62
+ return dates
63
+ }
64
+
65
+ function parseHMS (value) {
66
+ const [h, m, s] = String(value).split(':').map(Number)
67
+ return [h || 0, m || 0, s || 0]
68
+ }
69
+
70
+ // Midnight-to-23:59:59 window for `date`'s calendar day, in `tz`. Mirrors the
71
+ // server branch of getDataByDay.
72
+ function getDayWindow (date, tz) {
73
+ const { year, month, day } = zonedParts(date, tz)
74
+ return [zonedToUTC(year, month, day, 0, 0, 0, tz), zonedToUTC(year, month, day, 23, 59, 59, tz)]
75
+ }
76
+
77
+ async function getReportSummaryByDay (dates, userData, traccar, devices, tz, reportFrom, reportTo, firstDate, lastDate) {
78
+ const days = []
79
+ for (const date of dates) {
80
+ const { year, month, day } = zonedParts(date, tz)
81
+
82
+ let startDate = zonedToUTC(year, month, day, ...parseHMS(userData.dayHours.startTime), tz)
83
+ if (date.getTime() === firstDate.getTime()) {
84
+ const p = zonedParts(reportFrom, tz)
85
+ const clamp = zonedToUTC(year, month, day, p.hour, p.minute, p.second, tz)
86
+ if (clamp.getTime() > startDate.getTime()) startDate = clamp
87
+ }
88
+
89
+ let endDate = zonedToUTC(year, month, day, ...parseHMS(userData.dayHours.endTime), tz)
90
+ if (date.getTime() === lastDate.getTime()) {
91
+ const p = zonedParts(reportTo, tz)
92
+ const clamp = zonedToUTC(year, month, day, p.hour, p.minute, p.second, tz)
93
+ if (clamp.getTime() < endDate.getTime()) endDate = clamp
94
+ }
95
+
96
+ const summary = await getSummary(traccar, startDate, endDate, devices)
97
+ days.push({ date, summary })
98
+ }
99
+ return days
100
+ }
101
+
102
+ // Local, timezone-consistent equivalent of getDataByDay for this report.
103
+ async function getReportDataByDay (device, startDate, endDate, data) {
104
+ const tripsByDay = (data.trips || [])
105
+ .filter(t => new Date(t.endTime) > startDate && new Date(t.startTime) < endDate)
106
+ .map(t => ({ ...t }))
107
+ for (const t of tripsByDay) {
108
+ await calculatePartialTrip(device, startDate, endDate, data.route, t)
109
+ }
110
+
111
+ const stopsByDay = (data.stops || [])
112
+ .filter(t => new Date(t.endTime) > startDate && new Date(t.startTime) < endDate)
113
+ .map(t => ({ ...t }))
114
+
115
+ const routeByDay = data.route ? data.route.filter(p => new Date(p.fixTime) > startDate && new Date(p.fixTime) < endDate) : []
116
+ const refuelingPositionsByDay = data.refuelingPositions ? data.refuelingPositions.filter(p => new Date(p.date) > startDate && new Date(p.date) < endDate) : []
117
+
118
+ return { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay }
119
+ }
14
120
 
15
121
  async function createFuelConsumptionReport (from, to, userData, traccar) {
16
122
  console.log('Create FuelConsumption Report')
@@ -37,21 +143,19 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
37
143
 
38
144
  const daysByDevice = new Map()
39
145
 
40
- // Process the period in chunks of whole calendar days (computed once up front, then grouped) so
41
- // we never hold more than ~a week of raw GPS positions for the whole device set in memory at
42
- // once - a single busy device can have tens of thousands of positions over a month. Chunking the
43
- // 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)
146
+ const timezone = getReportTimezone(userData)
147
+ const allDates = getReportDates(from, to, timezone)
46
148
  const dateGroups = automaticReports.sliceArray(allDates, 7)
149
+ const firstDate = allDates[0]
150
+ const lastDate = allDates[allDates.length - 1]
47
151
 
48
152
  for (let chunkIndex = 0; chunkIndex < dateGroups.length; chunkIndex++) {
49
153
  const dates = dateGroups[chunkIndex]
50
- const chunkFrom = chunkIndex === 0 ? from : dates[0]
51
- const chunkTo = chunkIndex === dateGroups.length - 1 ? to : dateGroups[chunkIndex + 1][0]
154
+ const chunkFrom = chunkIndex === 0 ? from : getDayWindow(dates[0], timezone)[0]
155
+ const chunkTo = chunkIndex === dateGroups.length - 1 ? to : getDayWindow(dates[dates.length - 1], timezone)[1]
52
156
 
53
157
  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)
158
+ const summary = await getReportSummaryByDay(dates, userData, traccar, devices, timezone, from, to, firstDate, lastDate)
55
159
 
56
160
  const fuelServicesData = []
57
161
  if (devices.some(d => d.attributes.odooId)) {
@@ -69,7 +173,8 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
69
173
  const refuelingPositions = await refuelingReport.calculateRefuelingPositions(userData, d, { route, trips, fuelServices: deviceFuelServices })
70
174
  const days = daysByDevice.get(d.id) || []
71
175
  for (const date of dates) {
72
- const { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay } = await getDataByDay(d, date, { trips, stops, route, refuelingPositions }, userData, traccar)
176
+ const [dayStart, dayEnd] = getDayWindow(date, timezone)
177
+ const { tripsByDay, stopsByDay, routeByDay, refuelingPositionsByDay } = await getReportDataByDay(d, dayStart, dayEnd, { trips, stops, route, refuelingPositions })
73
178
  const summaryByDay = summary.find(a => a.date === date).summary
74
179
  const summaryByDevice = summaryByDay.find(a => a.deviceId === d.id)
75
180
 
@@ -80,9 +185,9 @@ async function createFuelConsumptionReport (from, to, userData, traccar) {
80
185
  'fuel_high_threshold' in d.attributes &&
81
186
  d.attributes.fuel_tank_capacity
82
187
 
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 })
188
+ const spentFuel = hasFuelSensor
189
+ ? tripsByDay.length > 0 ? calculateTotalFuelFromSensor(tripsByDay, stopsByDay, route, d, firstStopIndex) : 0
190
+ : calculateConsumption(d, { trips: tripsByDay, stops: stopsByDay, route: routeByDay, summary: summaryByDevice })
86
191
 
87
192
  const dataRow = {
88
193
  date,