fleetmap-reports 2.0.357 → 2.0.358

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.
Files changed (87) hide show
  1. package/.claude/worktrees/eager-stonebraker-c08ac7/.eslintrc.js +15 -0
  2. package/.claude/worktrees/eager-stonebraker-c08ac7/lang/enGB.js +309 -0
  3. package/.claude/worktrees/eager-stonebraker-c08ac7/lang/esCL.js +297 -0
  4. package/.claude/worktrees/eager-stonebraker-c08ac7/lang/frFR.json +324 -0
  5. package/.claude/worktrees/eager-stonebraker-c08ac7/lang/index.js +35 -0
  6. package/.claude/worktrees/eager-stonebraker-c08ac7/lang/ptBR.js +301 -0
  7. package/.claude/worktrees/eager-stonebraker-c08ac7/lang/ptPT.js +310 -0
  8. package/.claude/worktrees/eager-stonebraker-c08ac7/package-lock.json +11944 -0
  9. package/.claude/worktrees/eager-stonebraker-c08ac7/package.json +45 -0
  10. package/.claude/worktrees/eager-stonebraker-c08ac7/src/activity-report.js +822 -0
  11. package/.claude/worktrees/eager-stonebraker-c08ac7/src/automaticReports.js +34 -0
  12. package/.claude/worktrees/eager-stonebraker-c08ac7/src/carbone/index.js +13 -0
  13. package/.claude/worktrees/eager-stonebraker-c08ac7/src/custom/vistawasteActivity-report.js +544 -0
  14. package/.claude/worktrees/eager-stonebraker-c08ac7/src/events-report.js +360 -0
  15. package/.claude/worktrees/eager-stonebraker-c08ac7/src/fuel-consumption-report.js +296 -0
  16. package/.claude/worktrees/eager-stonebraker-c08ac7/src/fuel-drop-report.js +128 -0
  17. package/.claude/worktrees/eager-stonebraker-c08ac7/src/here.js +172 -0
  18. package/.claude/worktrees/eager-stonebraker-c08ac7/src/idle-report.js +403 -0
  19. package/.claude/worktrees/eager-stonebraker-c08ac7/src/index.js +244 -0
  20. package/.claude/worktrees/eager-stonebraker-c08ac7/src/kms-report.js +549 -0
  21. package/.claude/worktrees/eager-stonebraker-c08ac7/src/location-report.js +496 -0
  22. package/.claude/worktrees/eager-stonebraker-c08ac7/src/machines-report.js +278 -0
  23. package/.claude/worktrees/eager-stonebraker-c08ac7/src/partnerReports/afriquia.js +129 -0
  24. package/.claude/worktrees/eager-stonebraker-c08ac7/src/partnerReports/dailyuse-report.js +80 -0
  25. package/.claude/worktrees/eager-stonebraker-c08ac7/src/partnerReports/delayedstart-report.js +223 -0
  26. package/.claude/worktrees/eager-stonebraker-c08ac7/src/partnerReports/driverranking-report.js +328 -0
  27. package/.claude/worktrees/eager-stonebraker-c08ac7/src/partnerReports/gpsjump-report.js +61 -0
  28. package/.claude/worktrees/eager-stonebraker-c08ac7/src/partnerReports/performance-report.js +359 -0
  29. package/.claude/worktrees/eager-stonebraker-c08ac7/src/refueling-report.js +345 -0
  30. package/.claude/worktrees/eager-stonebraker-c08ac7/src/reportStyle.js +34 -0
  31. package/.claude/worktrees/eager-stonebraker-c08ac7/src/sensor-report.js +447 -0
  32. package/.claude/worktrees/eager-stonebraker-c08ac7/src/speeding-report.js +663 -0
  33. package/.claude/worktrees/eager-stonebraker-c08ac7/src/stop-report.js +242 -0
  34. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/ActivityReportTemplate.mjml +51 -0
  35. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/EmptyReportTemplate.mjml +25 -0
  36. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/EventsReportTemplate.mjml +75 -0
  37. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/FuelDropReportTemplate.mjml +60 -0
  38. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/IdleReportTemplate.mjml +54 -0
  39. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/KmsReportGroupByDayTemplate.mjml +43 -0
  40. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/KmsReportTemplate.mjml +44 -0
  41. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/LocationReportTemplate.mjml +37 -0
  42. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/RefuelingReportTemplate.mjml +60 -0
  43. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/SensorReportTemplate.mjml +23 -0
  44. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/SpeedingReportTemplate.mjml +80 -0
  45. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/TripReportTemplate.mjml +97 -0
  46. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/ZoneReportTemplate.mjml +52 -0
  47. package/.claude/worktrees/eager-stonebraker-c08ac7/src/templates/report.mjml +96 -0
  48. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/activity.test.js +99 -0
  49. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/carbone.test.js +20 -0
  50. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/events.test.js +30 -0
  51. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/fuel.test.js +35 -0
  52. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/idle.test.js +16 -0
  53. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/index.js +24 -0
  54. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/index.test.js +203 -0
  55. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/kms.test.js +74 -0
  56. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/performance.test.js +36 -0
  57. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/positions.test.js +67 -0
  58. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/speeding.test.js +120 -0
  59. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/translations.test.js +21 -0
  60. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/trips.test.js +140 -0
  61. package/.claude/worktrees/eager-stonebraker-c08ac7/src/tests/zones.test.js +162 -0
  62. package/.claude/worktrees/eager-stonebraker-c08ac7/src/trip-report.js +641 -0
  63. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/device.js +13 -0
  64. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/driver.js +90 -0
  65. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/fuel.js +102 -0
  66. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/geofence.js +52 -0
  67. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/odoo.js +18 -0
  68. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/parallel.js +64 -0
  69. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/pdfDocument.js +140 -0
  70. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/route.js +78 -0
  71. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/timetable.js +30 -0
  72. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/traccar.js +213 -0
  73. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/traccar.worker.js +49 -0
  74. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/trips.js +270 -0
  75. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/utils.js +358 -0
  76. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/worker.js +14 -0
  77. package/.claude/worktrees/eager-stonebraker-c08ac7/src/util/xpert.js +58 -0
  78. package/.claude/worktrees/eager-stonebraker-c08ac7/src/word/index.js +95 -0
  79. package/.claude/worktrees/eager-stonebraker-c08ac7/src/zone-report.js +904 -0
  80. package/lang/enGB.js +18 -0
  81. package/lang/esCL.js +18 -0
  82. package/lang/frFR.json +7 -1
  83. package/lang/ptBR.js +18 -0
  84. package/lang/ptPT.js +18 -0
  85. package/package.json +1 -1
  86. package/src/partnerReports/driverranking-report.js +149 -49
  87. package/src/util/pdfDocument.js +2 -1
@@ -0,0 +1,128 @@
1
+ const automaticReports = require('./automaticReports')
2
+ const { devicesToProcess } = require('./util/device')
3
+ const { getTranslations } = require('./util/utils')
4
+ const traccarHelper = require('./util/traccar')
5
+
6
+ function calculateFuelDrop (position, positions) {
7
+ const index = positions.indexOf(position)
8
+ if (index > 0) {
9
+ let backIndex = 1
10
+ while ((backIndex < 10) && index - backIndex >= 0) {
11
+ const positionBefore = positions[index - backIndex]
12
+ const diff = positionBefore.attributes.fuel - position.attributes.fuel
13
+ if (diff > 0) {
14
+ return diff
15
+ }
16
+ backIndex++
17
+ }
18
+ }
19
+ return 0
20
+ }
21
+
22
+ async function createFuelDropReport (from, to, userData, traccar) {
23
+ console.log('Create FuelDropReport')
24
+
25
+ const allData = {
26
+ devices: [],
27
+ from,
28
+ to
29
+ }
30
+
31
+ const reportData = []
32
+ reportData.push(allData)
33
+
34
+ const devices = devicesToProcess(userData)
35
+
36
+ const arrayOfArrays = automaticReports.sliceArray(devices, 50)
37
+ let data = []
38
+
39
+ const types = ['deviceFuelDrop']
40
+ for (const a of arrayOfArrays) {
41
+ const response = await traccar.reports.reportsEventsGet(from, to, a.map(d => d.id), null, types)
42
+ data = data.concat(response.data)
43
+ }
44
+ console.log('FuelDrop Alerts:' + data.length)
45
+ allData.totalDevices = 0
46
+ allData.totalFuelDrops = 0
47
+ allData.totalFuelDropLiters = 0
48
+
49
+ for (const d of devices) {
50
+ const alerts = data.filter(t => t.deviceId === d.id)
51
+ if (alerts.length > 0) {
52
+ const allInOne = await traccarHelper.getAllInOne(traccar, from, to, [d], true, false, false, false, 1, devices.length)
53
+ const route = allInOne.route.filter(t => t.deviceId === d.id)
54
+
55
+ for (const a of alerts) {
56
+ a.fuelDropLiters = 0
57
+ if (a.positionId > 0) {
58
+ const position = route.find(p => p.id === a.positionId)
59
+ if (position) {
60
+ a.position = position
61
+ const diff = calculateFuelDrop(position, route)
62
+ a.fuelDropLiters = Math.round(diff * d.attributes.fuel_tank_capacity / 100)
63
+ }
64
+ }
65
+ }
66
+
67
+ // Filter wrong fuel drop values
68
+ const filteredAlerts = alerts.filter(a => a.fuelDropLiters < d.attributes.fuel_tank_capacity && a.fuelDropLiters > 0)
69
+
70
+ if (filteredAlerts.length > 0) {
71
+ allData.devices.push({
72
+ device: d,
73
+ alerts: filteredAlerts
74
+ })
75
+ }
76
+ }
77
+ }
78
+
79
+ return reportData
80
+ }
81
+
82
+ function exportFuelDropReportToExcel (userData, reportData) {
83
+ console.log('Export to Excel')
84
+ const translations = getTranslations(userData)
85
+
86
+ const settings = {
87
+ sheetName: 'FuelDropReport', // The name of the sheet
88
+ fileName: 'FuelDropReport' // The name of the spreadsheet
89
+ }
90
+ const headers = [
91
+ { label: translations.report.name, value: 'name' },
92
+ { label: translations.report.driver, value: 'driver' },
93
+ { label: translations.report.date, value: 'fixTime' },
94
+ { label: translations.report.address, value: 'address' },
95
+ { label: translations.report.fueldrop, value: 'fuelDropLiters' }
96
+ ]
97
+ let data = []
98
+ if (reportData.devices) {
99
+ reportData.devices.forEach(d => {
100
+ data = data.concat(d.alerts.map(r => {
101
+ return {
102
+ driver: r.driverName,
103
+ fuelDropLiters: r.fuelDropLiters,
104
+ fixTime: new Date(r.position.fixTime).toLocaleString(),
105
+ name: d.device.name,
106
+ address: r.geofenceName || r.position.address
107
+ }
108
+ }))
109
+ })
110
+ }
111
+
112
+ return {
113
+ headers,
114
+ data,
115
+ settings
116
+ }
117
+ }
118
+
119
+ function exportFuelDropReportToPDF (userData, reportData) {
120
+ console.log('Export to PDF')
121
+ }
122
+
123
+ exports.createFuelDropReport = createFuelDropReport
124
+ exports.exportFuelDropReportToPDF = exportFuelDropReportToPDF
125
+ exports.exportFuelDropReportToExcel = exportFuelDropReportToExcel
126
+ exports.create = createFuelDropReport
127
+ exports.exportToPDF = exportFuelDropReportToPDF
128
+ exports.exportToExcel = exportFuelDropReportToExcel
@@ -0,0 +1,172 @@
1
+ const axios = require('axios')
2
+
3
+ const appId = 'apiKey=' + process.env.HERE_API_KEY
4
+
5
+ const calcRoute = 'https://fleet.ls.hereapi.com/2/calculateroute.json?&detailedTollCosts=1&' +
6
+ appId +
7
+ '&routeMatch=1&filetype=CSV&mode=car&tollVehicleType=1&rollups=none,country;tollsys&' +
8
+ 'attributes=SPEED_LIMITS_FCn(*),ROAD_NAME_FCn(*),ROAD_ADMIN_FCn(*),DISTANCE_MARKERS_FCn(*)' +
9
+ ',ROAD_GEOM_FCn(*),TOLL_LINK_FCn(*),TOLL_BOOTH_FCn(*),TRUCK_SPEED_LIMITS_FCn(*),TRUCK_RESTR_FCn(*)'
10
+
11
+ const util = require('util')
12
+
13
+ function mpsToKmh (mps) {
14
+ return mps * 3.6
15
+ }
16
+
17
+ function getSpeedLimit (li) {
18
+ if (li && li.attributes && li.attributes.SPEED_LIMITS_FCN) {
19
+ const from = li.attributes.SPEED_LIMITS_FCN[0].FROM_REF_SPEED_LIMIT
20
+ const to = li.attributes.SPEED_LIMITS_FCN[0].TO_REF_SPEED_LIMIT
21
+ if (from > to) {
22
+ return from
23
+ }
24
+ return to
25
+ }
26
+ return 999
27
+ }
28
+
29
+ function getRoadName (li) {
30
+ if (li.attributes.ROAD_GEOM_FCN) {
31
+ const result = li.attributes.ROAD_GEOM_FCN[0].NAME
32
+ if (result && result.length > 0) { return result }
33
+ }
34
+ return ''
35
+ }
36
+
37
+ function getTruckSpeedLimits (li) {
38
+ if (li.attributes.TRUCK_SPEED_LIMITS_FCN) {
39
+ return li.attributes.TRUCK_SPEED_LIMITS_FCN
40
+ }
41
+ return null
42
+ }
43
+
44
+ function getDistanceMarkers (li) {
45
+ if (li.attributes.DISTANCE_MARKERS_FCN) {
46
+ const dMarkers = li.attributes.DISTANCE_MARKERS_FCN
47
+ return util.format('km %d', dMarkers[0].DISTANCE_VALUE)
48
+ }
49
+ return ''
50
+ }
51
+
52
+ function generateCSV (rows) {
53
+ const route = []
54
+ route.push('latitude, longitude, speed_kmh, heading, timestamp')
55
+ for (let i = 0; i < rows.length; i++) {
56
+ const r = rows[i]
57
+ route.push(r.latitude + ',' + r.longitude + ',' + r.speed * 1.852 + ',' + r.course + ',' + r.fixTime)
58
+ }
59
+ return route.join('\n')
60
+ }
61
+ /*
62
+ function generateCoordsCSV(rows) {
63
+ const route = []
64
+ for (let i = 0; i < rows.length; i++) {
65
+ const r = rows[i]
66
+ route.push('id=' + i + '&prox=' + r.latMatched + ',' + r.lonMatched + ',20')
67
+ }
68
+ return route.join('\n')
69
+ }
70
+
71
+ function fillGeocoding(rows) {
72
+ return new Promise((resolve) => {
73
+ const csv = generateCoordsCSV(rows.slice(0, 300))
74
+ const body = {
75
+ report: 'reverseGeocode',
76
+ rows: csv
77
+ }
78
+ axios.post(rServerUrl,
79
+ body,
80
+ {
81
+ headers: { 'Content-Type': 'application/json' },
82
+ timeout: 60000 // Maximum timeour for the Lambda API Gateway
83
+ }
84
+ ).then((response) => {
85
+ if (response.data) {
86
+ const hereData = response.data
87
+ for (let i = 0; i < hereData.Response.Item.length; i++) {
88
+ rows[i].geocoding = hereData.Response.Item[i].Result[0].Location.Address.Label
89
+ }
90
+ resolve(rows)
91
+ }
92
+ }).catch((e) => {
93
+ console.error(e)
94
+ })
95
+ })
96
+ } */
97
+
98
+ exports.routeMatch = async (rows) => {
99
+ const csv = generateCSV(rows)
100
+ const response = await axios.post(calcRoute, csv, { headers: { 'Content-Type': 'application/binary' } })
101
+ const hereData = response.data
102
+ const route = hereData.response.route[0]
103
+ const results = []
104
+ const wayPoints = route.waypoint
105
+ const links = route.leg[0].link
106
+ if (wayPoints && wayPoints.length > 0) {
107
+ for (let i = 0; i < wayPoints.length; i++) {
108
+ const tp = wayPoints[i]
109
+ const li = links[tp.routeLinkSeqNrMatched]
110
+ results.push({
111
+ linkId: tp.routeLinkSeqNrMatched,
112
+ timestamp: tp.timestamp,
113
+ currentSpeedKmh: mpsToKmh(tp.speedMps),
114
+ latitude: tp.originalPosition.latitude,
115
+ longitude: tp.originalPosition.longitude,
116
+ latMatched: tp.mappedPosition.latitude,
117
+ lonMatched: tp.mappedPosition.longitude,
118
+ heading: tp.headingDegreeNorthClockwise,
119
+ headingMatched: tp.headingMatched,
120
+ speedLimit: getSpeedLimit(li),
121
+ roadName: getRoadName(li),
122
+ truckSpeedLimits: getTruckSpeedLimits(li),
123
+ distanceMarkers: getDistanceMarkers(li),
124
+ geocoding: ''
125
+ })
126
+ }
127
+ }
128
+ return results
129
+ }
130
+
131
+ exports.tollsMatch = async (rows, result) => {
132
+ const csv = generateCSV(rows)
133
+ axios.post(calcRoute, csv, { headers: { 'Content-Type': 'application/binary' } }).then((response) => {
134
+ const hereData = response.data
135
+ const results = []
136
+ const tollData = hereData.response.route[0].tollCost.routeTollItems
137
+ if (tollData) {
138
+ let it = 0
139
+ const wayPoints = hereData.response.route[0].waypoint
140
+ const links = hereData.response.route[0].leg[0].link
141
+ for (let ti = 0; ti < tollData.length; ti++) {
142
+ const i = tollData[ti]
143
+ if (i.tollCostAlternatives) {
144
+ for (let il = 0; il < links.length; il++) {
145
+ if (parseInt(links[il].linkId) === parseInt(i.tollStructures[0].linkIds[0])) {
146
+ for (; it < wayPoints.length; it++) {
147
+ if (wayPoints[it].routeLinkSeqNrMatched >= il) {
148
+ const li = wayPoints[it]
149
+ results.push({
150
+ country: i.country,
151
+ road: i.tollSystem[0].name,
152
+ amount: i.tollCostAlternatives[0].amount,
153
+ currency: i.tollCostAlternatives[0].currency,
154
+ toll: i.tollStructures[0].name,
155
+ timestamp: li ? li.timestamp : '',
156
+ speed: li ? mpsToKmh(li.speedMps) : ''
157
+ })
158
+ break
159
+ }
160
+ }
161
+ break
162
+ }
163
+ }
164
+ }
165
+ }
166
+ result(results)
167
+ } else { result([]) }
168
+ }).catch(e => {
169
+ console.error(e)
170
+ result([])
171
+ })
172
+ }
@@ -0,0 +1,403 @@
1
+ const jsPDF = require('jspdf')
2
+ const { headerFromUser, AmiriRegular } = require('./util/pdfDocument')
3
+ const {
4
+ convertToLocaleString, convertMS, getTranslations, getLanguage, convertToLocaleDateString, convertToLocaleTimeString,
5
+ convertPositionToFeature,
6
+ convertToFeature
7
+ } = require('./util/utils')
8
+ const { getStyle } = require('./reportStyle')
9
+ const { getUserPartner } = require('fleetmap-partners')
10
+ const { devicesToProcess } = require('./util/device')
11
+ const automaticReports = require('./automaticReports')
12
+ const traccarHelper = require('./util/traccar')
13
+ const { getIdleEvents } = require('./util/route')
14
+ const { reportByDriver, getDriverData } = require('./util/driver')
15
+ const { getNearestPOIs, checkGeofenceIn, loadGroupsData } = require('./util/geofence')
16
+
17
+ const fileName = 'IdleReport'
18
+
19
+ function getDriver (idleEvent, drivers) {
20
+ if (idleEvent && idleEvent.position && idleEvent.position.attributes.driverUniqueId) {
21
+ const driver = drivers.find(d => d.uniqueId === idleEvent.position.attributes.driverUniqueId)
22
+ return driver ? driver.name : idleEvent.position.attributes.driverUniqueId
23
+ }
24
+ return ''
25
+ }
26
+
27
+ async function createIdleReport (from, to, userData, traccarInstance) {
28
+ console.log('Create IdleReport')
29
+ const reportData = []
30
+
31
+ if (userData.byDriver) {
32
+ console.log('ByDriver')
33
+ const report = await createIdleReportByDriver(from, to, userData, traccarInstance)
34
+ reportData.push(report)
35
+ } else if (userData.byGroup) {
36
+ console.log('ByGroup')
37
+ const allData = await createIdleReportByGroup(from, to, userData, traccarInstance)
38
+ reportData.push(...allData)
39
+ } else {
40
+ console.log('ByDevice')
41
+ const report = await createIdleReportByDevice(from, to, userData, traccarInstance)
42
+ reportData.push(report)
43
+ }
44
+
45
+ return reportData
46
+ }
47
+
48
+ async function createIdleReportByDriver (from, to, userData, traccar) {
49
+ const devices = await traccar.devices.devicesGet().then(d => d.data)
50
+
51
+ if (!devices.length) {
52
+ // Empty report
53
+ return { drivers: [] }
54
+ }
55
+
56
+ await loadGroupsData(userData, traccar)
57
+
58
+ const results = await reportByDriver(devices, traccar, from, to, userData, processDrivers)
59
+
60
+ const report = {
61
+ drivers: [],
62
+ from,
63
+ to
64
+ }
65
+
66
+ results.drivers.forEach(result => {
67
+ const driver = report.drivers.find(d => d.driver.id === result.driver.id)
68
+ if (driver) {
69
+ driver.idleEvents.push(...result.idleEvents)
70
+ } else {
71
+ report.drivers.push(result)
72
+ }
73
+ })
74
+
75
+ report.drivers.forEach(d => {
76
+ d.idleEvents = d.idleEvents.sort((a, b) => (new Date(a.position.fixTime) > new Date(b.position.fixTime)) ? 1 : -1)
77
+ })
78
+
79
+ return report
80
+ }
81
+
82
+ async function createIdleReportByGroup (from, to, userData, traccarInstance) {
83
+ const reportData = []
84
+ for (const g of userData.groups) {
85
+ const devices = userData.devices.filter(d => d.groupId === g.id)
86
+ console.log(g.name + ' devices:' + devices.length)
87
+ if (devices.length > 0) {
88
+ const groupData = {
89
+ devices: [],
90
+ group: g,
91
+ from,
92
+ to
93
+ }
94
+
95
+ const response = await traccarInstance.reports.reportsRouteGet(from, to, null, [g.id])
96
+ const routes = response.data
97
+
98
+ devices.sort((a, b) => (a.name > b.name) ? 1 : -1)
99
+
100
+ if (routes.length > 0) {
101
+ console.log('Routes:' + routes.length)
102
+ groupData.devices = processDevices(from, to, devices, routes, userData)
103
+
104
+ reportData.push(groupData)
105
+ }
106
+ }
107
+ }
108
+
109
+ const groupIds = userData.groups.map(g => g.id)
110
+ const withoutGroupDevices = userData.devices.filter(d => !groupIds.includes(d.groupId))
111
+
112
+ const allData = await createIdleReportByDevice(from, to, withoutGroupDevices, userData, traccarInstance)
113
+ reportData.push(allData)
114
+
115
+ return reportData
116
+ }
117
+
118
+ async function createIdleReportByDevice (from, to, userData, traccarInstance) {
119
+ const allData = {
120
+ devices: [],
121
+ from,
122
+ to
123
+ }
124
+
125
+ await loadGroupsData(userData, traccarInstance)
126
+
127
+ const devices = devicesToProcess(userData)
128
+ const sliced = automaticReports.sliceArray(devices, 5)
129
+
130
+ let deviceCount = 0
131
+ for (const slice of sliced) {
132
+ const { route } = await traccarHelper.getAllInOne(traccarInstance, from, to, slice, true, false, false, false, deviceCount, devices.length)
133
+
134
+ if (route.length > 0) {
135
+ allData.devices.push(...await processDevices(from, to, slice, route, userData))
136
+ }
137
+
138
+ deviceCount = deviceCount + slice.length
139
+ }
140
+
141
+ return allData
142
+ }
143
+
144
+ async function processDrivers (from, to, userData, data) {
145
+ const driversResult = []
146
+ for (const d of userData.drivers) {
147
+ const { route } = await getDriverData(d, data, userData)
148
+
149
+ if (route.length > 0) {
150
+ const idleEvents = getIdleEvents(data.route, d, userData.idleSpeedThreshold / 1.85200)
151
+ const filteredEvents = idleEvents.filter(e => userData.minimumIdleMinutes ? e.idleTime > userData.minimumIdleMinutes * 60 * 1000 : true)
152
+
153
+ if (filteredEvents.length) {
154
+ filteredEvents.forEach(e => {
155
+ findPOIData(e, userData)
156
+ findGeofenceData(e, userData)
157
+ const device = userData.devices.find(d => d.id === e.position.deviceId)
158
+ e.deviceName = device ? device.name : ''
159
+ })
160
+
161
+ const driverData = {
162
+ driver: d,
163
+ idleEvents: filteredEvents
164
+ }
165
+ driversResult.push(driverData)
166
+ }
167
+ }
168
+ }
169
+
170
+ return driversResult
171
+ }
172
+
173
+ async function processDevices (from, to, devices, routes, userData) {
174
+ const devicesResult = []
175
+ for (const d of devices) {
176
+ const route = routes.filter(p => p.deviceId === d.id)
177
+
178
+ const idleEvents = getIdleEvents(route, undefined, userData.idleSpeedThreshold / 1.85200)
179
+ const filteredEvents = idleEvents.filter(e => e.idleTime > (userData.minimumIdleMinutes ? userData.minimumIdleMinutes * 60 * 1000 : 0))
180
+
181
+ if (filteredEvents.length) {
182
+ for (const e of filteredEvents) {
183
+ findPOIData(e, userData)
184
+ findGeofenceData(e, userData)
185
+
186
+ const driver = userData.drivers.find(d => d.id === e.position.attributes.driverUniqueId)
187
+ e.driver = driver ? driver.name : ''
188
+ }
189
+
190
+ const deviceData = {
191
+ device: d,
192
+ idleEvents: filteredEvents
193
+ }
194
+ devicesResult.push(deviceData)
195
+ }
196
+ }
197
+
198
+ return devicesResult
199
+ }
200
+
201
+ function findPOIData (event, userData) {
202
+ const nearestPOIs = getNearestPOIs(event.position.longitude, event.position.latitude, userData.geofences)
203
+ if (nearestPOIs.length > 0) {
204
+ event.endPOIName = nearestPOIs[0].p.name
205
+ }
206
+ }
207
+
208
+ function findGeofenceData (event, userData) {
209
+ const geofence = userData.geofences.filter(g => !g.area.startsWith('CIRCLE')).find(g =>
210
+ checkGeofenceIn(convertPositionToFeature(event.position), convertToFeature(g)))
211
+ if (geofence) {
212
+ const group = userData.groups.find(g => g.geofenceIds && g.geofenceIds.includes(geofence.id))
213
+ event.geofenceName = geofence.name
214
+ event.geofenceGroup = (group && group.name) || ''
215
+ }
216
+ }
217
+
218
+ async function exportIdleReportToPDF (userData, reportData) {
219
+ console.log('Export to PDF')
220
+
221
+ const timezone = userData.user.attributes.timezone
222
+ const lang = getLanguage(userData)
223
+ const translations = getTranslations(userData)
224
+
225
+ const idleData = userData.byDriver ? reportData.drivers : reportData.devices
226
+
227
+ const headers = [
228
+ translations.report.date,
229
+ translations.report.start,
230
+ translations.report.end,
231
+ translations.report.address,
232
+ translations.report.duration,
233
+ translations.report.zone
234
+ ]
235
+
236
+ if (userData.byDriver) {
237
+ headers.push(translations.report.vehicle)
238
+ } else {
239
+ headers.push(translations.report.driver)
240
+ }
241
+
242
+ if (idleData) {
243
+ let first = true
244
+ // eslint-disable-next-line new-cap
245
+ const doc = new jsPDF.jsPDF('l')
246
+ await headerFromUser(doc, translations.report.titleIdleReport, userData.user)
247
+
248
+ idleData.forEach(d => {
249
+ try {
250
+ const data = []
251
+
252
+ const name = userData.byDriver ? d.driver.name : deviceName(d.device)
253
+ const group = userData.byDriver ? userData.groups.find(g => g.drivers.includes(d.id)) : userData.groups.find(g => d.device.groupId === g.id)
254
+
255
+ let space = 0
256
+ if (!first) {
257
+ doc.addPage()
258
+ } else {
259
+ first = false
260
+ space = 10
261
+ }
262
+ doc.setFontSize(13)
263
+ doc.text(name, 20, space + 20)
264
+ doc.setFontSize(11)
265
+ doc.text(group ? translations.report.group + ': ' + group.name : '', 150, space + 20)
266
+ doc.text(convertToLocaleString(reportData.from, lang, timezone) + ' - ' + convertToLocaleString(reportData.to, lang, timezone), 20, space + 25)
267
+ doc.text(translations.report.minimumMinutes + ': ' + userData.minimumIdleMinutes + ' ' + translations.report.minutes, 20, space + 35)
268
+ doc.text(translations.report.maximumSpeed + ': ' + userData.idleSpeedThreshold + ' Km/h', 100, space + 35)
269
+
270
+ d.idleEvents.forEach(a => {
271
+ const temp = [
272
+ getIdleEventDate(a.position, userData.user),
273
+ getIdleEventTime(a.position, userData.user),
274
+ getIdleEventTime(a.lastPosition, userData.user),
275
+ a.endPOIName || a.position.address,
276
+ convertMS(a.idleTime, true),
277
+ (a.geofenceName || '') + (a.geofenceGroup ? ' (' + a.geofenceGroup + ')' : ''),
278
+ userData.byDriver ? a.deviceName : getDriver(a, userData.drivers)
279
+ ]
280
+ data.push(temp)
281
+ })
282
+
283
+ const footValues = [
284
+ 'Total:' + d.idleEvents.length,
285
+ '', '', '',
286
+ convertMS(d.idleEvents.reduce((a, b) => a + b.idleTime, 0), true)
287
+ ]
288
+
289
+ if (userData.roadSpeedLimits) {
290
+ footValues.splice(2, 0, '')
291
+ }
292
+
293
+ if (timezone === 'Asia/Qatar') {
294
+ doc.addFileToVFS('Amiri-Regular.ttf', AmiriRegular())
295
+ doc.addFont('Amiri-Regular.ttf', 'Amiri', 'normal')
296
+ }
297
+
298
+ const style = getStyle(getUserPartner(userData.user))
299
+ doc.autoTable({
300
+ head: [headers],
301
+ body: data,
302
+ foot: [footValues],
303
+ showFoot: 'lastPage',
304
+ headStyles: {
305
+ fillColor: style.pdfHeaderColor,
306
+ textColor: style.pdfHeaderTextColor,
307
+ fontSize: 10
308
+ },
309
+ bodyStyles: {
310
+ fillColor: style.pdfBodyColor,
311
+ textColor: style.pdfBodyTextColor,
312
+ fontSize: 8,
313
+ font: timezone === 'Asia/Qatar' ? 'Amiri' : ''
314
+
315
+ },
316
+ footStyles: {
317
+ fillColor: style.pdfFooterColor,
318
+ textColor: style.pdfFooterTextColor,
319
+ fontSize: 9
320
+ },
321
+ startY: space + 45
322
+ })
323
+ } catch (e) {
324
+ console.error(e, 'moving on...')
325
+ }
326
+ })
327
+
328
+ return doc
329
+ }
330
+ }
331
+
332
+ function exportIdleReportToExcel (userData, reportData) {
333
+ console.log('Export to Excel')
334
+
335
+ const translations = getTranslations(userData)
336
+
337
+ const idleData = userData.byDriver ? reportData.drivers : reportData.devices
338
+
339
+ const settings = {
340
+ sheetName: translations.report.titleIdleReport.substring(0, 31), // The name of the sheet
341
+ fileName // The name of the spreadsheet
342
+ }
343
+ const headers = [
344
+ { label: userData.byDriver ? translations.report.driver : translations.report.vehicle, value: 'name' },
345
+ { label: translations.report.date, value: 'date' },
346
+ { label: translations.report.start, value: 'start' },
347
+ { label: translations.report.end, value: 'end' },
348
+ { label: translations.report.address, value: 'address' },
349
+ { label: translations.report.duration, value: 'duration' },
350
+ { label: translations.report.zone, value: 'zone' },
351
+ { label: userData.byDriver ? translations.report.vehicle : translations.report.driver, value: 'subname' }
352
+ ]
353
+
354
+ let data = []
355
+ if (idleData) {
356
+ idleData.forEach(d => {
357
+ data = data.concat([{}])
358
+ data = data.concat(d.idleEvents.map(a => {
359
+ return {
360
+ name: userData.byDriver ? d.driver.name : d.device.name,
361
+ duration: convertMS(a.idleTime, true),
362
+ date: getIdleEventDate(a.position, userData.user),
363
+ start: getIdleEventTime(a.position, userData.user),
364
+ end: getIdleEventTime(a.lastPosition, userData.user),
365
+ subname: userData.byDriver ? a.deviceName : getDriver(a, userData.drivers),
366
+ address: a.endPOIName || a.position.address,
367
+ zone: (a.geofenceName || '') + (a.geofenceGroup ? ' (' + a.geofenceGroup + ')' : '')
368
+ }
369
+ }))
370
+ // Totals
371
+ data = data.concat([{
372
+ name: d.idleEvents.length,
373
+ duration: convertMS(d.idleEvents.reduce((a, b) => a + b.idleTime, 0), true)
374
+ }])
375
+ data = data.concat([{}])
376
+ })
377
+ console.log(data)
378
+ return {
379
+ headers,
380
+ data,
381
+ settings
382
+ }
383
+ }
384
+ }
385
+
386
+ function deviceName (device) {
387
+ return device.name + (device.attributes.license_plate ? ', ' + device.attributes.license_plate : '') + (device.model ? ', ' + device.model : '')
388
+ }
389
+
390
+ function getIdleEventDate (position, user) {
391
+ return convertToLocaleDateString(position.fixTime, user.attributes.lang, user.attributes.timezone)
392
+ }
393
+
394
+ function getIdleEventTime (position, user) {
395
+ return convertToLocaleTimeString(position.fixTime, user.attributes.lang, user.attributes.timezone)
396
+ }
397
+
398
+ exports.createIdleReport = createIdleReport
399
+ exports.exportIdleReportToPDF = exportIdleReportToPDF
400
+ exports.exportIdleReportToExcel = exportIdleReportToExcel
401
+ exports.create = createIdleReport
402
+ exports.exportToPDF = exportIdleReportToPDF
403
+ exports.exportToExcel = exportIdleReportToExcel