fleetmap-reports 2.0.384 → 2.0.386

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/lang/enGB.js CHANGED
@@ -195,6 +195,9 @@ module.exports = {
195
195
  titleStopReport: 'Stop Report',
196
196
  titleDriverRankingReport: 'Driver Ranking Report',
197
197
  titleSensorReport: 'Sensor Report',
198
+ titleRPMReport: 'RPM Report',
199
+ rpmThreshold: 'RPM Threshold',
200
+ timeAboveRPM: 'Time above threshold',
198
201
  sensor: 'Sensor',
199
202
  status: 'Status',
200
203
  rank: 'Rank',
package/lang/esCL.js CHANGED
@@ -185,6 +185,9 @@ module.exports = {
185
185
  titleStopReport: 'Informe de Paradas',
186
186
  titleDriverRankingReport: 'Informe de Ranking de Conductores',
187
187
  titleSensorReport: 'Informe de Sensores',
188
+ titleRPMReport: 'Informe de RPM',
189
+ rpmThreshold: 'Umbral de RPM',
190
+ timeAboveRPM: 'Tiempo sobre el umbral',
188
191
  sensor: 'Sensor',
189
192
  status: 'Estado',
190
193
  rank: 'Posición',
package/lang/frFR.json CHANGED
@@ -196,6 +196,9 @@
196
196
  "titleDelayedStartReport": "Rapport de Departs Tardifs",
197
197
  "titleDriverRankingReport": "Rapport d'conducteur",
198
198
  "titleSensorReport": "Rapport de capteurs",
199
+ "titleRPMReport": "Rapport de RPM",
200
+ "rpmThreshold": "Seuil de RPM",
201
+ "timeAboveRPM": "Temps au-dessus du seuil",
199
202
  "sensor": "Capteur",
200
203
  "status": "État",
201
204
  "from": "Dans",
package/lang/ptBR.js CHANGED
@@ -187,6 +187,9 @@ module.exports = {
187
187
  titleStopReport: 'Relatório de Paragens',
188
188
  titleDriverRankingReport: 'Relatório de Ranking de Motoristas',
189
189
  titleSensorReport: 'Relatório de Sensores',
190
+ titleRPMReport: 'Relatório de RPM',
191
+ rpmThreshold: 'Limite de RPM',
192
+ timeAboveRPM: 'Tempo acima do limite',
190
193
  sensor: 'Sensor',
191
194
  status: 'Estado',
192
195
  rank: 'Posição',
package/lang/ptPT.js CHANGED
@@ -193,6 +193,9 @@ module.exports = {
193
193
  titleStopReport: 'Relatório de Paragens',
194
194
  titleDriverRankingReport: 'Relatório de Ranking de Condutores',
195
195
  titleSensorReport: 'Relatório de Sensores',
196
+ titleRPMReport: 'Relatório de RPM',
197
+ rpmThreshold: 'Limite de RPM',
198
+ timeAboveRPM: 'Tempo acima do limite',
196
199
  sensor: 'Sensor',
197
200
  status: 'Estado',
198
201
  rank: 'Posição',
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "fleetmap-reports",
3
- "version": "2.0.384",
3
+ "version": "2.0.386",
4
4
  "description": "",
5
5
  "main": "src/index.js",
6
6
  "scripts": {
@@ -207,7 +207,8 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
207
207
  endTime: tripsByDay.length && tripsByDay[tripsByDay.length - 1].endTime,
208
208
  endAddress: tripsByDay.length && tripsByDay[tripsByDay.length - 1].endAddress,
209
209
  startTimeIsOut: tripsByDay.some(t => t.startTimeIsOut),
210
- endTimeIsOut: tripsByDay.some(t => t.endTimeIsOut)
210
+ endTimeIsOut: tripsByDay.some(t => t.endTimeIsOut),
211
+ activity: getDayActivity(tripsByDay, fromDay, toDay)
211
212
  })
212
213
  }
213
214
  } else {
@@ -232,6 +233,7 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
232
233
  summaryCurrentDay.endOdometer = summaryByDevice.endOdometer
233
234
  summaryCurrentDay.startTimeIsOut = tripsByDay.some(t => t.startTimeIsOut)
234
235
  summaryCurrentDay.endTimeIsOut = tripsByDay.some(t => t.endTimeIsOut)
236
+ summaryCurrentDay.activity = getDayActivity(tripsByDay, fromDay, toDay)
235
237
  summary.push(summaryCurrentDay)
236
238
  }
237
239
  }
@@ -285,6 +287,17 @@ async function processDevices (from, to, devices, data, userData, traccarInstanc
285
287
  return devicesResult
286
288
  }
287
289
 
290
+ // Trips of one day as [{ start, end }] in hours since the start of that day (0-24),
291
+ // used to draw the daily activity timeline on screen and in the PDF.
292
+ function getDayActivity (tripsByDay, fromDay, toDay) {
293
+ const dayStart = new Date(fromDay).getTime()
294
+ const dayEnd = new Date(toDay).getTime()
295
+ const toHours = time => (Math.min(Math.max(new Date(time).getTime(), dayStart), dayEnd) - dayStart) / 3600000
296
+ return tripsByDay
297
+ .map(t => ({ start: toHours(t.startTime), end: toHours(t.endTime) }))
298
+ .filter(a => a.end > a.start)
299
+ }
300
+
288
301
  function processDrivers (from, to, drivers, driversData, userData) {
289
302
  const driversResult = []
290
303
  drivers.forEach(d => {
@@ -508,6 +521,63 @@ function exportActivityReportToPDFVehicleGroupByDay (doc, translations, reportDa
508
521
 
509
522
  const style = getStyle(getUserPartner(userData.user))
510
523
  addTable(doc, headers, data, footValues, style, 40)
524
+ addActivityTimeline(doc, translations, d.summary, weekDays)
525
+ })
526
+ }
527
+
528
+ // Draws one bar per day (00h-24h) below the table: grey when stopped, green while driving.
529
+ function addActivityTimeline (doc, translations, summary, weekDays) {
530
+ if (!summary.some(s => s.activity)) {
531
+ return
532
+ }
533
+ const pageWidth = doc.internal.pageSize.getWidth()
534
+ const pageHeight = doc.internal.pageSize.getHeight()
535
+ const labelX = 14
536
+ const left = 50
537
+ const width = pageWidth - 14 - left
538
+ const rowHeight = 4
539
+ const rowGap = 2
540
+ const bottom = pageHeight - 15
541
+
542
+ const drawAxis = (y) => {
543
+ doc.setFontSize(7)
544
+ doc.setTextColor(80, 80, 80)
545
+ for (let h = 0; h <= 24; h += 2) {
546
+ doc.text(String(h).padStart(2, '0') + 'h', left + h / 24 * width, y, { align: 'center' })
547
+ }
548
+ return y + 2
549
+ }
550
+
551
+ let y = doc.lastAutoTable.finalY + 10
552
+ if (y + 20 > bottom) {
553
+ doc.addPage()
554
+ y = 20
555
+ }
556
+ doc.setFillColor(103, 194, 58)
557
+ doc.rect(labelX, y - 2.5, 3, 3, 'F')
558
+ doc.setFontSize(8)
559
+ doc.setTextColor(0, 0, 0)
560
+ doc.text(translations.vehicleTable.moving_vehicles, labelX + 4, y)
561
+ doc.setFillColor(220, 220, 220)
562
+ doc.rect(labelX + 30, y - 2.5, 3, 3, 'F')
563
+ doc.text(translations.vehicleTable.stopped_vehicles, labelX + 34, y)
564
+ y = drawAxis(y + 6)
565
+
566
+ summary.forEach(s => {
567
+ if (y + rowHeight > bottom) {
568
+ doc.addPage()
569
+ y = drawAxis(20)
570
+ }
571
+ doc.setFontSize(7)
572
+ doc.setTextColor(0, 0, 0)
573
+ doc.text(new Date(s.date).toLocaleDateString() + ' - ' + weekDays[new Date(s.date).getDay()], labelX, y + rowHeight - 0.8)
574
+ doc.setFillColor(220, 220, 220)
575
+ doc.rect(left, y, width, rowHeight, 'F')
576
+ doc.setFillColor(103, 194, 58)
577
+ for (const a of (s.activity || [])) {
578
+ doc.rect(left + a.start / 24 * width, y, Math.max((a.end - a.start) / 24 * width, 0.3), rowHeight, 'F')
579
+ }
580
+ y += rowHeight + rowGap
511
581
  })
512
582
  }
513
583
 
package/src/index.js CHANGED
@@ -240,5 +240,15 @@ function Reports (config, axios) {
240
240
  this.sensorReportToPDF = (userData, reportData) => {
241
241
  return require('./sensor-report').exportToPDF(userData, reportData)
242
242
  }
243
+
244
+ this.rpmReport = (from, to, userData) => {
245
+ return require('./rpm-report').createRPMReport(from, to, userData, this.traccar)
246
+ }
247
+ this.rpmReportToPDF = (userData, reportData) => {
248
+ return require('./rpm-report').exportRPMReportToPDF(userData, reportData)
249
+ }
250
+ this.rpmReportToExcel = (userData, reportData) => {
251
+ return require('./rpm-report').exportRPMReportToExcel(userData, reportData)
252
+ }
243
253
  }
244
254
  module.exports = Reports
@@ -0,0 +1,194 @@
1
+ const jsPDF = require('jspdf')
2
+ require('jspdf-autotable')
3
+ const { getStyle } = require('./reportStyle')
4
+ const { headerFromUser, addTable, pdfPageByDay } = require('./util/pdfDocument')
5
+ const { getUserPartner } = require('fleetmap-partners')
6
+ const traccar = require('./util/traccar')
7
+ const { getDates, getTranslations, convertMS } = require('./util/utils')
8
+ const { devicesToProcess } = require('./util/device')
9
+ const automaticReports = require('./automaticReports')
10
+ const { calculateRPMSections } = require('./partnerReports/performance-report')
11
+
12
+ const fileName = 'RPMReport'
13
+ const defaultThreshold = 2500
14
+
15
+ async function createRPMReport (from, to, userData, traccarInstance) {
16
+ console.log('Create RPMReport', from, to)
17
+ const allDevices = devicesToProcess(userData)
18
+
19
+ const allData = {
20
+ devices: [],
21
+ from,
22
+ to
23
+ }
24
+
25
+ const dates = getDates(from, to, userData.user.attributes.timezone || getUserPartner(userData.user).timezone)
26
+
27
+ let deviceCount = 0
28
+ const sliced = automaticReports.sliceArray(allDevices, 20)
29
+ for (const devices of sliced) {
30
+ const { route } = await traccar.getAllInOne(traccarInstance, from, to, devices, true, false, false, false, deviceCount, allDevices.length, 10, 1)
31
+ for (const d of devices) {
32
+ const deviceRoute = route.filter(p => p.deviceId === d.id && p.attributes.rpm !== undefined)
33
+ if (deviceRoute.length) {
34
+ allData.devices.push(processDevice(d, deviceRoute, dates))
35
+ }
36
+ }
37
+ deviceCount += devices.length
38
+ }
39
+
40
+ return [allData]
41
+ }
42
+
43
+ function getThreshold (device) {
44
+ return Number(device.attributes.highRPM) || defaultThreshold
45
+ }
46
+
47
+ function dayKey (date) {
48
+ return date.getFullYear() + '-' + date.getMonth() + '-' + date.getDate()
49
+ }
50
+
51
+ // time between consecutive positions that are both above the threshold, attributed to the day of the first one
52
+ function processDevice (device, deviceRoute, dates) {
53
+ const threshold = getThreshold(device)
54
+ const days = dates.map(date => ({ date, time: 0 }))
55
+ calculateRPMSections(deviceRoute, threshold).forEach(section => {
56
+ for (let i = 1; i < section.length; i++) {
57
+ const start = new Date(section[i - 1].fixTime)
58
+ const duration = new Date(section[i].fixTime).getTime() - start.getTime()
59
+ const day = days.find(a => dayKey(a.date) === dayKey(start))
60
+ if (day) {
61
+ day.time += duration
62
+ }
63
+ }
64
+ })
65
+ return {
66
+ device,
67
+ threshold,
68
+ time: days.reduce((a, b) => a + b.time, 0),
69
+ days
70
+ }
71
+ }
72
+
73
+ async function exportRPMReportToPDF (userData, reportData) {
74
+ const translations = getTranslations(userData)
75
+ const weekDays = getWeekDays(translations)
76
+ const style = getStyle(getUserPartner(userData.user))
77
+
78
+ const orientation = userData.groupByDay ? 'p' : 'l'
79
+ // eslint-disable-next-line new-cap
80
+ const doc = new jsPDF.jsPDF(orientation)
81
+ await headerFromUser(doc, translations.report.titleRPMReport, userData.user, orientation)
82
+
83
+ if (userData.groupByDay) {
84
+ const headers = [translations.report.date, translations.report.timeAboveRPM]
85
+ reportData.devices.forEach((d, index) => {
86
+ pdfPageByDay(userData, d, index, doc, translations, reportData)
87
+ const data = d.days.map(day => [
88
+ new Date(day.date).toLocaleDateString() + ' - ' + weekDays[new Date(day.date).getDay()],
89
+ convertMS(day.time)
90
+ ])
91
+ const footValues = [translations.report.rpmThreshold + ': ' + d.threshold, convertMS(d.time)]
92
+ addTable(doc, headers, data, footValues, style, 40, { 1: { halign: 'right' } })
93
+ })
94
+ } else {
95
+ doc.setFontSize(11)
96
+ doc.text(new Date(reportData.from).toLocaleString() + ' - ' + new Date(reportData.to).toLocaleString(), 20, 33)
97
+ const headers = [
98
+ translations.report.vehicle,
99
+ translations.settings.vehicle_licenseplate,
100
+ translations.report.group,
101
+ translations.report.rpmThreshold,
102
+ translations.report.timeAboveRPM
103
+ ]
104
+ const data = reportData.devices.map(d => {
105
+ const group = userData.groups.find(g => d.device.groupId === g.id)
106
+ return [
107
+ d.device.name,
108
+ d.device.attributes.license_plate || '',
109
+ group ? group.name : '',
110
+ d.threshold,
111
+ convertMS(d.time)
112
+ ]
113
+ })
114
+ const footValues = [
115
+ 'Total:' + reportData.devices.length,
116
+ '',
117
+ '',
118
+ '',
119
+ convertMS(reportData.devices.reduce((a, b) => a + b.time, 0))
120
+ ]
121
+ addTable(doc, headers, data, footValues, style, 35, { 3: { halign: 'right' }, 4: { halign: 'right' } })
122
+ }
123
+ return doc
124
+ }
125
+
126
+ function exportRPMReportToExcel (userData, reportData) {
127
+ const translations = getTranslations(userData)
128
+ const weekDays = getWeekDays(translations)
129
+
130
+ const settings = {
131
+ sheetName: translations.report.titleRPMReport.substring(0, 31),
132
+ fileName
133
+ }
134
+
135
+ const headers = [
136
+ { label: translations.report.vehicle, value: 'name' },
137
+ { label: translations.settings.vehicle_licenseplate, value: 'licenseplate' },
138
+ { label: translations.report.group, value: 'group' },
139
+ { label: translations.report.rpmThreshold, value: 'threshold' }
140
+ ]
141
+ if (userData.groupByDay) {
142
+ headers.push(
143
+ { label: translations.report.date, value: 'date' },
144
+ { label: translations.report.weekDay, value: 'weekday' })
145
+ }
146
+ headers.push({ label: translations.report.timeAboveRPM, value: 'time' })
147
+
148
+ const data = []
149
+ reportData.devices.forEach(d => {
150
+ const group = userData.groups.find(g => d.device.groupId === g.id)
151
+ const row = {
152
+ name: d.device.name,
153
+ licenseplate: d.device.attributes.license_plate,
154
+ group: group ? group.name : '',
155
+ threshold: d.threshold
156
+ }
157
+ if (userData.groupByDay) {
158
+ data.push(...d.days.map(day => ({
159
+ ...row,
160
+ date: new Date(day.date).toLocaleDateString(),
161
+ weekday: weekDays[new Date(day.date).getDay()],
162
+ time: convertMS(day.time)
163
+ })))
164
+ } else {
165
+ data.push({ ...row, time: convertMS(d.time) })
166
+ }
167
+ })
168
+
169
+ return {
170
+ headers,
171
+ data,
172
+ settings
173
+ }
174
+ }
175
+
176
+ function getWeekDays (translations) {
177
+ return [
178
+ translations.report.sunday,
179
+ translations.report.monday,
180
+ translations.report.tuesday,
181
+ translations.report.wednesday,
182
+ translations.report.thursday,
183
+ translations.report.friday,
184
+ translations.report.saturday
185
+ ]
186
+ }
187
+
188
+ exports.processDevice = processDevice
189
+ exports.createRPMReport = createRPMReport
190
+ exports.exportRPMReportToPDF = exportRPMReportToPDF
191
+ exports.exportRPMReportToExcel = exportRPMReportToExcel
192
+ exports.create = createRPMReport
193
+ exports.exportToPDF = exportRPMReportToPDF
194
+ exports.exportToExcel = exportRPMReportToExcel
@@ -0,0 +1,33 @@
1
+ /* eslint-disable no-undef */
2
+ const assert = require('assert')
3
+ const { processDevice } = require('../rpm-report')
4
+
5
+ function position (fixTime, rpm) {
6
+ return { fixTime: new Date(fixTime).toISOString(), attributes: { rpm } }
7
+ }
8
+
9
+ describe('RPM_Report', function () {
10
+ const dates = [new Date(2026, 9, 1, 12), new Date(2026, 9, 2, 12)]
11
+ const route = [
12
+ position(new Date(2026, 9, 1, 10, 0), 2000),
13
+ position(new Date(2026, 9, 1, 10, 1), 2600),
14
+ position(new Date(2026, 9, 1, 10, 2), 2700),
15
+ position(new Date(2026, 9, 1, 10, 4), 3000),
16
+ position(new Date(2026, 9, 1, 10, 5), 1500),
17
+ position(new Date(2026, 9, 2, 9, 0), 2600),
18
+ position(new Date(2026, 9, 2, 9, 0, 30), 2600)
19
+ ]
20
+ it('sums time above the default threshold per day', () => {
21
+ const result = processDevice({ id: 1, attributes: {} }, route, dates)
22
+ assert.equal(result.threshold, 2500)
23
+ assert.equal(result.days[0].time, 3 * 60 * 1000)
24
+ assert.equal(result.days[1].time, 30 * 1000)
25
+ assert.equal(result.time, 3 * 60 * 1000 + 30 * 1000)
26
+ })
27
+ it('uses the device highRPM attribute as threshold', () => {
28
+ const result = processDevice({ id: 1, attributes: { highRPM: 2650 } }, route, dates)
29
+ assert.equal(result.threshold, 2650)
30
+ assert.equal(result.days[0].time, 2 * 60 * 1000)
31
+ assert.equal(result.days[1].time, 0)
32
+ })
33
+ })