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,345 @@
1
+ const helpers = require('@turf/helpers')
2
+ const automaticReports = require('./automaticReports')
3
+ const { devicesToProcess } = require('./util/device')
4
+ const distance = require('@turf/distance')
5
+ const { getTranslations, getLanguage, convertToLocaleString } = require('./util/utils')
6
+ const odoo = require('./util/odoo')
7
+ const traccarHelper = require('./util/traccar')
8
+ const { calculateFuelDiff, getRefuelingLiters } = require('./util/fuel')
9
+ const jsPDF = require('jspdf')
10
+ const { headerFromUser, addTable } = require('./util/pdfDocument')
11
+ const { getStyle } = require('./reportStyle')
12
+ const { getUserPartner } = require('fleetmap-partners')
13
+
14
+ // const positionsToCheck = 15
15
+
16
+ async function createRefuelingReport (from, to, userData, traccar) {
17
+ console.log('Create RefuelingReport')
18
+
19
+ const reportData = []
20
+
21
+ const allData = {
22
+ devices: [],
23
+ from,
24
+ to
25
+ }
26
+
27
+ const devices = devicesToProcess(userData).filter(d => automaticReports.deviceWithFuelInfo(d))
28
+
29
+ console.log('Devices:' + devices.length)
30
+
31
+ const slices = automaticReports.sliceArray(devices, 5)
32
+
33
+ allData.totalDevices = 0
34
+ allData.totalRefueling = 0
35
+ allData.totalRefuelingLiters = 0
36
+ let deviceCount = 0
37
+ for (const slice of slices) {
38
+ const allInOne = await traccarHelper.getAllInOne(traccar, from, to, slice, true, false, false, false, deviceCount, devices.length)
39
+ const route = allInOne.route
40
+
41
+ const fuelServicesData = []
42
+ if (devices.some(d => d.attributes.odooId)) {
43
+ fuelServicesData.push(...(await odoo.getOdooFuelServices(traccar, from, to)))
44
+ }
45
+
46
+ allData.devices.push(...await processDevices(from, to, slice, route, userData, fuelServicesData))
47
+ deviceCount = deviceCount + slice.length
48
+ }
49
+ const refuelingPositions = allData.devices.map(d => d.refuelings).flat()
50
+ allData.totalDevices = allData.totalDevices + 1
51
+ allData.totalRefueling = allData.totalRefueling + refuelingPositions.length
52
+ allData.totalRefuelingLiters = allData.totalRefuelingLiters + refuelingPositions.reduce((a, b) => a + b.diff, 0)
53
+
54
+ reportData.push(allData)
55
+
56
+ return reportData
57
+ }
58
+
59
+ async function processDevices (from, to, devices, data, userData, fuelServicesData) {
60
+ const devicesResult = []
61
+
62
+ for (const d of devices) {
63
+ const positions = data.filter(t => t.deviceId === d.id)
64
+ const deviceFuelServices = fuelServicesData.filter(f => Number.parseInt(d.attributes.odooId) === f.vehicle_id[0])
65
+
66
+ if (positions.length > 0) {
67
+ const refuelingPositions = await calculateRefuelingPositions(userData, d, {
68
+ route: positions,
69
+ fuelServices: deviceFuelServices
70
+ })
71
+
72
+ if (userData.drivers.length > 0) {
73
+ refuelingPositions.forEach(r => {
74
+ if (r.position) {
75
+ const geofence = findNearestPOI(r.position, userData.geofences)
76
+ if (geofence) {
77
+ r.position.geofenceName = geofence.name
78
+ }
79
+ const driver = userData.drivers.find(d => d.uniqueId === r.position.attributes.driverUniqueId)
80
+ if (driver) {
81
+ r.position.driverName = driver.name
82
+ }
83
+ }
84
+ })
85
+ }
86
+
87
+ if (refuelingPositions.length > 0) {
88
+ devicesResult.push({
89
+ device: d,
90
+ refuelings: refuelingPositions
91
+ })
92
+ }
93
+ }
94
+ }
95
+
96
+ return devicesResult
97
+ }
98
+
99
+ function findNearestPOI (position, geofences) {
100
+ if (geofences.length === 0) { return null }
101
+
102
+ if (!position) { return null }
103
+
104
+ const a = geofences.map(g => {
105
+ if (g.area && g.area.startsWith('CIRCLE (')) {
106
+ console.log('->' + g.area)
107
+ const str = g.area.substring('CIRCLE ('.length, g.area.indexOf(','))
108
+
109
+ const coord = str.trim().split(' ')
110
+ return {
111
+ id: g.id,
112
+ distance: Math.round(coordsDistance(parseFloat(coord[1]), parseFloat(coord[0]), position.longitude, position.latitude))
113
+ }
114
+ }
115
+ return {
116
+ id: g.id,
117
+ distance: Number.MAX_SAFE_INTEGER
118
+ }
119
+ }).filter(a => a.distance < 100).sort((a, b) => (a.distance > b.distance) ? 1 : -1)
120
+
121
+ if (a.length > 0) {
122
+ return a[0]
123
+ }
124
+
125
+ return null
126
+ }
127
+
128
+ function coordsDistance (lon1, lat1, lon2, lat2) {
129
+ const from = helpers.point([lon1, lat1])
130
+ const to = helpers.point([lon2, lat2])
131
+
132
+ return distance.default(from, to) * 1000
133
+ }
134
+
135
+ /* function isStoped (position) {
136
+ return !position.attributes.ignition || position.attributes.rpm === 0
137
+ } */
138
+
139
+ /* function analyseRouteRefuelingOnStops (data, d, refuelingPositions) {
140
+ let refuelingActivated = false
141
+ let currentValue = null
142
+ let positionsChecked = 0
143
+ let lastRefuelingIndex = 0
144
+ data.route.forEach(function (element, index, array) {
145
+ if (isStoped(element) && refuelingActivated && !currentValue) {
146
+ // Ignition is off and Refueling is Active and there is no current value
147
+
148
+ // Get 5 position before the ignition off to calculate the current value of the fuel tank level
149
+ // lastRefuelingIndex is used to avoid re-analyzing positions prior to a refueling
150
+ const before = (index > 5 ? array.slice((index - 6 < lastRefuelingIndex ? lastRefuelingIndex : index - 6), index - 1) : array.slice(0, index - 1)).filter(b => b.attributes.ignition && b.attributes.fuel)
151
+ currentValue = Math.round(before.reduce((a, b) => a + b.attributes.fuel, 0) / before.length)
152
+
153
+ positionsChecked = 0
154
+ }
155
+
156
+ if (element.attributes.ignition && refuelingActivated && currentValue) {
157
+ // Ignition is on and Refueling is Active and there is a current value
158
+ // Calculate the value of the fuel tank level after turning the ignition back on
159
+ const after = (array.length > index + 5 ? array.slice(index, index + 1) : array.slice(index)).filter(b => b.attributes.ignition && b.attributes.fuel)
160
+ const newValue = Math.round(after.reduce((a, b) => a + b.attributes.fuel, 0) / after.length)
161
+
162
+ const diff = calculateFuelDiff(currentValue, newValue, d, element)
163
+ if (diff > 20) {
164
+ // New refueling detected
165
+ const value = getRefuelingLiters(d, element, diff)
166
+ refuelingPositions.push({position: element, date: element.fixTime, diff: value})
167
+ lastRefuelingIndex = index
168
+ positionsChecked = positionsToCheck + 1 // to reset values
169
+ }
170
+
171
+ if (positionsChecked > positionsToCheck) {
172
+ refuelingActivated = false
173
+ currentValue = null
174
+ positionsChecked = 0
175
+ } else {
176
+ positionsChecked++
177
+ }
178
+ }
179
+
180
+ if (element.attributes.ignition && !refuelingActivated) {
181
+ refuelingActivated = true
182
+ }
183
+ })
184
+ } */
185
+
186
+ function analyseRouteRefueling (data, d, refuelingPositions) {
187
+ let startValue = 0
188
+ let currentValue = 0
189
+ let refuelingActivated = false
190
+ data.route.forEach(function (element, index, array) {
191
+ if (!refuelingActivated) {
192
+ startValue = element.attributes.fuel
193
+ }
194
+
195
+ const endValue = array.length > index + 2 ? array[index + 2].attributes.fuel : undefined
196
+ const diff = calculateFuelDiff(startValue, endValue, d, element)
197
+
198
+ if (diff > 5 && !refuelingActivated) {
199
+ refuelingActivated = true
200
+ currentValue = diff
201
+ } else if (refuelingActivated && currentValue < diff) {
202
+ currentValue = diff
203
+ } else {
204
+ if (currentValue > 20) {
205
+ // New refueling detected
206
+ const value = getRefuelingLiters(d, element, currentValue)
207
+ refuelingPositions.push({ position: element, date: element.fixTime, diff: value })
208
+ currentValue = 0
209
+ }
210
+ refuelingActivated = false
211
+ }
212
+ })
213
+ }
214
+
215
+ async function calculateRefuelingPositions (userData, d, data) {
216
+ const refuelingPositions = []
217
+
218
+ if (d.attributes.odooId && !data.route[0].attributes.fuel) {
219
+ console.log(data.fuelServices)
220
+ data.fuelServices.forEach(s => {
221
+ const serviceDate = new Date(s.date)
222
+ serviceDate.setHours(23, 59, 59, 59)
223
+
224
+ const routeBeforeService = data.route.filter(r => new Date(r.fixTime).getTime() < serviceDate.getTime())
225
+
226
+ const quantity = s.liter
227
+
228
+ if (routeBeforeService.length) {
229
+ refuelingPositions.push({ position: routeBeforeService.slice(-1)[0], date: s.date, diff: Number.parseFloat(quantity), cost: s.amount })
230
+ }
231
+ })
232
+ } else {
233
+ // analyseRouteRefuelingOnStops(data, d, refuelingPositions)
234
+ analyseRouteRefueling(data, d, refuelingPositions)
235
+ }
236
+ return refuelingPositions
237
+ }
238
+
239
+ function exportRefuelingReportToExcel (userData, reportData) {
240
+ console.log('Export to Excel')
241
+ const translations = getTranslations(userData)
242
+
243
+ const settings = {
244
+ sheetName: 'RefuelingReport', // The name of the sheet
245
+ fileName: 'RefuelingReport' // The name of the spreadsheet
246
+ }
247
+ const headers = [
248
+ { label: translations.report.vehicle, value: 'name' },
249
+ { label: translations.report.date, value: 'fixTime' },
250
+ { label: translations.report.address, value: 'address' },
251
+ { label: translations.report.refueling, value: 'diff' },
252
+ { label: translations.report.driver, value: 'driver' }
253
+ ]
254
+ let data = []
255
+ if (reportData.devices) {
256
+ reportData.devices.forEach(d => {
257
+ data = data.concat(d.refuelings.map(r => {
258
+ return {
259
+ driver: r.driverName,
260
+ diff: r.diff,
261
+ fixTime: new Date(r.position.fixTime).toLocaleString(),
262
+ name: d.device.name,
263
+ address: r.geofenceName || r.position.address
264
+ }
265
+ }))
266
+ })
267
+ }
268
+
269
+ return {
270
+ headers,
271
+ data,
272
+ settings
273
+ }
274
+ }
275
+
276
+ async function exportRefuelingReportToPDF (userData, reportData) {
277
+ console.log('Export to PDF')
278
+ const timezone = userData.user.attributes.timezone
279
+ const translations = getTranslations(userData)
280
+ const lang = getLanguage(userData)
281
+
282
+ // eslint-disable-next-line new-cap
283
+ const doc = new jsPDF.jsPDF('l')
284
+ await headerFromUser(doc, translations.report.titleRefuelingReport, userData.user)
285
+
286
+ const headers = [
287
+ translations.report.date,
288
+ translations.report.address,
289
+ translations.report.refueling,
290
+ translations.report.driver
291
+ ]
292
+
293
+ reportData.devices.forEach((d, index) => {
294
+ const data = []
295
+ const name = deviceName(d.device)
296
+ const group = userData.groups.find(g => d.device.groupId === g.id)
297
+
298
+ const space = index === 0 ? 8 : 0
299
+ if (index) {
300
+ doc.addPage()
301
+ }
302
+
303
+ doc.setFontSize(13)
304
+ doc.text(name, 20, space + 20)
305
+ doc.setFontSize(11)
306
+ doc.text(group ? translations.report.group + ': ' + group.name : '', 200, space + 20)
307
+ doc.text(convertToLocaleString(reportData.from, lang, timezone) + ' - ' + convertToLocaleString(reportData.to, lang, timezone), 20, space + 25)
308
+
309
+ d.refuelings.forEach(r =>
310
+ data.push([
311
+ new Date(r.position.fixTime).toLocaleString(),
312
+ r.geofenceName || r.position.address,
313
+ r.diff,
314
+ r.driverName
315
+ ])
316
+ )
317
+
318
+ const footValues = [
319
+ 'Total:' + d.refuelings.length,
320
+ '',
321
+ d.refuelings.reduce((a, b) => a + b.diff, 0),
322
+ ''
323
+ ]
324
+
325
+ const style = getStyle(getUserPartner(userData.user))
326
+ addTable(doc, headers, data, footValues, style, space + 40,
327
+ {
328
+ 2: { halign: 'right' }
329
+ })
330
+ })
331
+
332
+ return doc
333
+ }
334
+
335
+ function deviceName (device) {
336
+ return device.name + (device.attributes.license_plate ? ', ' + device.attributes.license_plate : '') + (device.model ? ', ' + device.model : '')
337
+ }
338
+
339
+ exports.createRefuelingReport = createRefuelingReport
340
+ exports.calculateRefuelingPositions = calculateRefuelingPositions
341
+ exports.exportRefuelingReportToPDF = exportRefuelingReportToPDF
342
+ exports.exportRefuelingReportToExcel = exportRefuelingReportToExcel
343
+ exports.create = createRefuelingReport
344
+ exports.exportToPDF = exportRefuelingReportToPDF
345
+ exports.exportToExcel = exportRefuelingReportToExcel
@@ -0,0 +1,34 @@
1
+ function getStyle(partnerData) {
2
+ const reportStyleData = {
3
+ pdfHeaderColor: [105, 105, 105],
4
+ pdfHeaderTextColor: [256, 256, 256],
5
+ pdfBodyColor: [256, 256, 256],
6
+ pdfBodyTextColor: [30, 30, 30],
7
+ pdfFooterColor: [210, 210, 210],
8
+ pdfFooterTextColor: [30, 30, 30],
9
+ imgHeight: 0,
10
+ imgWidth: 0,
11
+ }
12
+
13
+ if(partnerData && partnerData.reports){
14
+ reportStyleData.pdfHeaderColor = partnerData.reports.mainColor || [105, 105, 105]
15
+ reportStyleData.pdfFooterColor = partnerData.reports.mainColor || [210, 210, 210]
16
+ reportStyleData.pdfFooterTextColor = partnerData.reports.mainColor ? [256, 256, 256] : [30, 30, 30]
17
+ reportStyleData.imgHeight = partnerData.reports.logoHeight || 0
18
+ reportStyleData.imgWidth = partnerData.reports.logoWidth || 0
19
+ }
20
+
21
+ return reportStyleData
22
+ }
23
+
24
+ exports.getStyle = getStyle
25
+
26
+ exports.pdfHeaderColor = [105, 105, 105]
27
+ exports.pdfHeaderTextColor = [256, 256, 256]
28
+ exports.pdfBodyColor = [256, 256, 256]
29
+ exports.pdfBodyTextColor = [30, 30, 30]
30
+ exports.pdfFooterColor = [210, 210, 210]
31
+ exports.pdfFooterTextColor = [30, 30, 30]
32
+
33
+
34
+