From d3c7705bedebd65c94f9eea691aaf2fe03b0cafe Mon Sep 17 00:00:00 2001 From: Anton Tananaev Date: Sat, 19 Aug 2023 13:58:45 -0700 Subject: Move to Vite --- modern/src/reports/ChartReportPage.js | 138 --------------- modern/src/reports/ChartReportPage.jsx | 138 +++++++++++++++ modern/src/reports/CombinedReportPage.js | 105 ----------- modern/src/reports/CombinedReportPage.jsx | 105 +++++++++++ modern/src/reports/EventReportPage.js | 232 ------------------------- modern/src/reports/EventReportPage.jsx | 232 +++++++++++++++++++++++++ modern/src/reports/RouteReportPage.js | 173 ------------------ modern/src/reports/RouteReportPage.jsx | 173 ++++++++++++++++++ modern/src/reports/ScheduledPage.js | 106 ----------- modern/src/reports/ScheduledPage.jsx | 106 +++++++++++ modern/src/reports/StatisticsPage.js | 85 --------- modern/src/reports/StatisticsPage.jsx | 85 +++++++++ modern/src/reports/StopReportPage.js | 172 ------------------ modern/src/reports/StopReportPage.jsx | 172 ++++++++++++++++++ modern/src/reports/SummaryReportPage.js | 152 ---------------- modern/src/reports/SummaryReportPage.jsx | 152 ++++++++++++++++ modern/src/reports/TripReportPage.js | 216 ----------------------- modern/src/reports/TripReportPage.jsx | 216 +++++++++++++++++++++++ modern/src/reports/components/ColumnSelect.js | 34 ---- modern/src/reports/components/ColumnSelect.jsx | 34 ++++ modern/src/reports/components/ReportFilter.js | 225 ------------------------ modern/src/reports/components/ReportFilter.jsx | 225 ++++++++++++++++++++++++ modern/src/reports/components/ReportsMenu.js | 110 ------------ modern/src/reports/components/ReportsMenu.jsx | 110 ++++++++++++ 24 files changed, 1748 insertions(+), 1748 deletions(-) delete mode 100644 modern/src/reports/ChartReportPage.js create mode 100644 modern/src/reports/ChartReportPage.jsx delete mode 100644 modern/src/reports/CombinedReportPage.js create mode 100644 modern/src/reports/CombinedReportPage.jsx delete mode 100644 modern/src/reports/EventReportPage.js create mode 100644 modern/src/reports/EventReportPage.jsx delete mode 100644 modern/src/reports/RouteReportPage.js create mode 100644 modern/src/reports/RouteReportPage.jsx delete mode 100644 modern/src/reports/ScheduledPage.js create mode 100644 modern/src/reports/ScheduledPage.jsx delete mode 100644 modern/src/reports/StatisticsPage.js create mode 100644 modern/src/reports/StatisticsPage.jsx delete mode 100644 modern/src/reports/StopReportPage.js create mode 100644 modern/src/reports/StopReportPage.jsx delete mode 100644 modern/src/reports/SummaryReportPage.js create mode 100644 modern/src/reports/SummaryReportPage.jsx delete mode 100644 modern/src/reports/TripReportPage.js create mode 100644 modern/src/reports/TripReportPage.jsx delete mode 100644 modern/src/reports/components/ColumnSelect.js create mode 100644 modern/src/reports/components/ColumnSelect.jsx delete mode 100644 modern/src/reports/components/ReportFilter.js create mode 100644 modern/src/reports/components/ReportFilter.jsx delete mode 100644 modern/src/reports/components/ReportsMenu.js create mode 100644 modern/src/reports/components/ReportsMenu.jsx (limited to 'modern/src/reports') diff --git a/modern/src/reports/ChartReportPage.js b/modern/src/reports/ChartReportPage.js deleted file mode 100644 index 2c33f0f5..00000000 --- a/modern/src/reports/ChartReportPage.js +++ /dev/null @@ -1,138 +0,0 @@ -import React, { useState } from 'react'; -import { - FormControl, InputLabel, Select, MenuItem, -} from '@mui/material'; -import { - CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, -} from 'recharts'; -import ReportFilter from './components/ReportFilter'; -import { formatTime } from '../common/util/formatter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import { useCatch } from '../reactHelper'; -import { useAttributePreference, usePreference } from '../common/util/preferences'; -import { - altitudeFromMeters, distanceFromMeters, speedFromKnots, volumeFromLiters, -} from '../common/util/converter'; -import useReportStyles from './common/useReportStyles'; - -const ChartReportPage = () => { - const classes = useReportStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const distanceUnit = useAttributePreference('distanceUnit'); - const altitudeUnit = useAttributePreference('altitudeUnit'); - const speedUnit = useAttributePreference('speedUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - const hours12 = usePreference('twelveHourFormat'); - - const [items, setItems] = useState([]); - const [types, setTypes] = useState(['speed']); - const [type, setType] = useState('speed'); - - const values = items.map((it) => it[type]); - const minValue = Math.min(...values); - const maxValue = Math.max(...values); - const valueRange = maxValue - minValue; - - const handleSubmit = useCatch(async ({ deviceId, from, to }) => { - const query = new URLSearchParams({ deviceId, from, to }); - const response = await fetch(`/api/reports/route?${query.toString()}`, { - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - const positions = await response.json(); - const keySet = new Set(); - const keyList = []; - const formattedPositions = positions.map((position) => { - const data = { ...position, ...position.attributes }; - const formatted = {}; - formatted.fixTime = formatTime(position.fixTime, 'time', hours12); - Object.keys(data).filter((key) => !['id', 'deviceId'].includes(key)).forEach((key) => { - const value = data[key]; - if (typeof value === 'number') { - keySet.add(key); - const definition = positionAttributes[key] || {}; - switch (definition.dataType) { - case 'speed': - formatted[key] = speedFromKnots(value, speedUnit).toFixed(2); - break; - case 'altitude': - formatted[key] = altitudeFromMeters(value, altitudeUnit).toFixed(2); - break; - case 'distance': - formatted[key] = distanceFromMeters(value, distanceUnit).toFixed(2); - break; - case 'volume': - formatted[key] = volumeFromLiters(value, volumeUnit).toFixed(2); - break; - case 'hours': - formatted[key] = (value / 1000).toFixed(2); - break; - default: - formatted[key] = value; - break; - } - } - }); - return formatted; - }); - Object.keys(positionAttributes).forEach((key) => { - if (keySet.has(key)) { - keyList.push(key); - keySet.delete(key); - } - }); - setTypes([...keyList, ...keySet]); - setItems(formattedPositions); - } else { - throw Error(await response.text()); - } - }); - - return ( - } breadcrumbs={['reportTitle', 'reportChart']}> - -
- - {t('reportChartType')} - - -
-
- {items.length > 0 && ( -
- - - - value.toFixed(2)} domain={[minValue - valueRange / 5, maxValue + valueRange / 5]} /> - - [value, positionAttributes[key]?.name || key]} /> - - - -
- )} -
- ); -}; - -export default ChartReportPage; diff --git a/modern/src/reports/ChartReportPage.jsx b/modern/src/reports/ChartReportPage.jsx new file mode 100644 index 00000000..2c33f0f5 --- /dev/null +++ b/modern/src/reports/ChartReportPage.jsx @@ -0,0 +1,138 @@ +import React, { useState } from 'react'; +import { + FormControl, InputLabel, Select, MenuItem, +} from '@mui/material'; +import { + CartesianGrid, Line, LineChart, ResponsiveContainer, Tooltip, XAxis, YAxis, +} from 'recharts'; +import ReportFilter from './components/ReportFilter'; +import { formatTime } from '../common/util/formatter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; +import { useCatch } from '../reactHelper'; +import { useAttributePreference, usePreference } from '../common/util/preferences'; +import { + altitudeFromMeters, distanceFromMeters, speedFromKnots, volumeFromLiters, +} from '../common/util/converter'; +import useReportStyles from './common/useReportStyles'; + +const ChartReportPage = () => { + const classes = useReportStyles(); + const t = useTranslation(); + + const positionAttributes = usePositionAttributes(t); + + const distanceUnit = useAttributePreference('distanceUnit'); + const altitudeUnit = useAttributePreference('altitudeUnit'); + const speedUnit = useAttributePreference('speedUnit'); + const volumeUnit = useAttributePreference('volumeUnit'); + const hours12 = usePreference('twelveHourFormat'); + + const [items, setItems] = useState([]); + const [types, setTypes] = useState(['speed']); + const [type, setType] = useState('speed'); + + const values = items.map((it) => it[type]); + const minValue = Math.min(...values); + const maxValue = Math.max(...values); + const valueRange = maxValue - minValue; + + const handleSubmit = useCatch(async ({ deviceId, from, to }) => { + const query = new URLSearchParams({ deviceId, from, to }); + const response = await fetch(`/api/reports/route?${query.toString()}`, { + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + const positions = await response.json(); + const keySet = new Set(); + const keyList = []; + const formattedPositions = positions.map((position) => { + const data = { ...position, ...position.attributes }; + const formatted = {}; + formatted.fixTime = formatTime(position.fixTime, 'time', hours12); + Object.keys(data).filter((key) => !['id', 'deviceId'].includes(key)).forEach((key) => { + const value = data[key]; + if (typeof value === 'number') { + keySet.add(key); + const definition = positionAttributes[key] || {}; + switch (definition.dataType) { + case 'speed': + formatted[key] = speedFromKnots(value, speedUnit).toFixed(2); + break; + case 'altitude': + formatted[key] = altitudeFromMeters(value, altitudeUnit).toFixed(2); + break; + case 'distance': + formatted[key] = distanceFromMeters(value, distanceUnit).toFixed(2); + break; + case 'volume': + formatted[key] = volumeFromLiters(value, volumeUnit).toFixed(2); + break; + case 'hours': + formatted[key] = (value / 1000).toFixed(2); + break; + default: + formatted[key] = value; + break; + } + } + }); + return formatted; + }); + Object.keys(positionAttributes).forEach((key) => { + if (keySet.has(key)) { + keyList.push(key); + keySet.delete(key); + } + }); + setTypes([...keyList, ...keySet]); + setItems(formattedPositions); + } else { + throw Error(await response.text()); + } + }); + + return ( + } breadcrumbs={['reportTitle', 'reportChart']}> + +
+ + {t('reportChartType')} + + +
+
+ {items.length > 0 && ( +
+ + + + value.toFixed(2)} domain={[minValue - valueRange / 5, maxValue + valueRange / 5]} /> + + [value, positionAttributes[key]?.name || key]} /> + + + +
+ )} +
+ ); +}; + +export default ChartReportPage; diff --git a/modern/src/reports/CombinedReportPage.js b/modern/src/reports/CombinedReportPage.js deleted file mode 100644 index a5000839..00000000 --- a/modern/src/reports/CombinedReportPage.js +++ /dev/null @@ -1,105 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - Table, TableBody, TableCell, TableHead, TableRow, -} from '@mui/material'; -import ReportFilter from './components/ReportFilter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import { useCatch } from '../reactHelper'; -import MapView from '../map/core/MapView'; -import MapRoutePath from '../map/MapRoutePath'; -import useReportStyles from './common/useReportStyles'; -import TableShimmer from '../common/components/TableShimmer'; -import MapCamera from '../map/MapCamera'; -import MapGeofence from '../map/MapGeofence'; -import { formatTime } from '../common/util/formatter'; -import { usePreference } from '../common/util/preferences'; -import { prefixString } from '../common/util/stringUtils'; -import MapMarkers from '../map/MapMarkers'; - -const CombinedReportPage = () => { - const classes = useReportStyles(); - const t = useTranslation(); - - const devices = useSelector((state) => state.devices.items); - - const hours12 = usePreference('twelveHourFormat'); - - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - - const createMarkers = () => items.flatMap((item) => item.events - .map((event) => item.positions.find((p) => event.positionId === p.id)) - .filter((position) => position != null) - .map((position) => ({ - latitude: position.latitude, - longitude: position.longitude, - }))); - - const handleSubmit = useCatch(async ({ deviceIds, groupIds, from, to }) => { - const query = new URLSearchParams({ from, to }); - deviceIds.forEach((deviceId) => query.append('deviceId', deviceId)); - groupIds.forEach((groupId) => query.append('groupId', groupId)); - setLoading(true); - try { - const response = await fetch(`/api/reports/combined?${query.toString()}`); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }); - - return ( - } breadcrumbs={['reportTitle', 'reportCombined']}> -
- {Boolean(items.length) && ( -
- - - {items.map((item) => ( - - ))} - - - item.route)} /> -
- )} -
-
- -
- - - - {t('sharedDevice')} - {t('positionFixTime')} - {t('sharedType')} - - - - {!loading ? items.flatMap((item) => item.events.map((event, index) => ( - - {index ? '' : devices[item.deviceId].name} - {formatTime(event.eventTime, 'seconds', hours12)} - {t(prefixString('event', event.type))} - - ))) : ()} - -
-
-
-
- ); -}; - -export default CombinedReportPage; diff --git a/modern/src/reports/CombinedReportPage.jsx b/modern/src/reports/CombinedReportPage.jsx new file mode 100644 index 00000000..a5000839 --- /dev/null +++ b/modern/src/reports/CombinedReportPage.jsx @@ -0,0 +1,105 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + Table, TableBody, TableCell, TableHead, TableRow, +} from '@mui/material'; +import ReportFilter from './components/ReportFilter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import { useCatch } from '../reactHelper'; +import MapView from '../map/core/MapView'; +import MapRoutePath from '../map/MapRoutePath'; +import useReportStyles from './common/useReportStyles'; +import TableShimmer from '../common/components/TableShimmer'; +import MapCamera from '../map/MapCamera'; +import MapGeofence from '../map/MapGeofence'; +import { formatTime } from '../common/util/formatter'; +import { usePreference } from '../common/util/preferences'; +import { prefixString } from '../common/util/stringUtils'; +import MapMarkers from '../map/MapMarkers'; + +const CombinedReportPage = () => { + const classes = useReportStyles(); + const t = useTranslation(); + + const devices = useSelector((state) => state.devices.items); + + const hours12 = usePreference('twelveHourFormat'); + + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + + const createMarkers = () => items.flatMap((item) => item.events + .map((event) => item.positions.find((p) => event.positionId === p.id)) + .filter((position) => position != null) + .map((position) => ({ + latitude: position.latitude, + longitude: position.longitude, + }))); + + const handleSubmit = useCatch(async ({ deviceIds, groupIds, from, to }) => { + const query = new URLSearchParams({ from, to }); + deviceIds.forEach((deviceId) => query.append('deviceId', deviceId)); + groupIds.forEach((groupId) => query.append('groupId', groupId)); + setLoading(true); + try { + const response = await fetch(`/api/reports/combined?${query.toString()}`); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }); + + return ( + } breadcrumbs={['reportTitle', 'reportCombined']}> +
+ {Boolean(items.length) && ( +
+ + + {items.map((item) => ( + + ))} + + + item.route)} /> +
+ )} +
+
+ +
+ + + + {t('sharedDevice')} + {t('positionFixTime')} + {t('sharedType')} + + + + {!loading ? items.flatMap((item) => item.events.map((event, index) => ( + + {index ? '' : devices[item.deviceId].name} + {formatTime(event.eventTime, 'seconds', hours12)} + {t(prefixString('event', event.type))} + + ))) : ()} + +
+
+
+
+ ); +}; + +export default CombinedReportPage; diff --git a/modern/src/reports/EventReportPage.js b/modern/src/reports/EventReportPage.js deleted file mode 100644 index 10b539ab..00000000 --- a/modern/src/reports/EventReportPage.js +++ /dev/null @@ -1,232 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody, Link, IconButton, -} from '@mui/material'; -import GpsFixedIcon from '@mui/icons-material/GpsFixed'; -import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; -import { useSelector } from 'react-redux'; -import { formatSpeed, formatTime } from '../common/util/formatter'; -import ReportFilter from './components/ReportFilter'; -import { prefixString } from '../common/util/stringUtils'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import usePersistedState from '../common/util/usePersistedState'; -import ColumnSelect from './components/ColumnSelect'; -import { useCatch, useEffectAsync } from '../reactHelper'; -import useReportStyles from './common/useReportStyles'; -import TableShimmer from '../common/components/TableShimmer'; -import { useAttributePreference, usePreference } from '../common/util/preferences'; -import MapView from '../map/core/MapView'; -import MapGeofence from '../map/MapGeofence'; -import MapPositions from '../map/MapPositions'; -import MapCamera from '../map/MapCamera'; -import scheduleReport from './common/scheduleReport'; - -const columnsArray = [ - ['eventTime', 'positionFixTime'], - ['type', 'sharedType'], - ['geofenceId', 'sharedGeofence'], - ['maintenanceId', 'sharedMaintenance'], - ['attributes', 'commandData'], -]; -const columnsMap = new Map(columnsArray); - -const EventReportPage = () => { - const navigate = useNavigate(); - const classes = useReportStyles(); - const t = useTranslation(); - - const devices = useSelector((state) => state.devices.items); - const geofences = useSelector((state) => state.geofences.items); - - const speedUnit = useAttributePreference('speedUnit'); - const hours12 = usePreference('twelveHourFormat'); - - const [allEventTypes, setAllEventTypes] = useState([['allEvents', 'eventAll']]); - - const [columns, setColumns] = usePersistedState('eventColumns', ['eventTime', 'type', 'attributes']); - const [eventTypes, setEventTypes] = useState(['allEvents']); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedItem, setSelectedItem] = useState(null); - const [position, setPosition] = useState(null); - - useEffectAsync(async () => { - if (selectedItem) { - const response = await fetch(`/api/positions?id=${selectedItem.positionId}`); - if (response.ok) { - const positions = await response.json(); - if (positions.length > 0) { - setPosition(positions[0]); - } - } else { - throw Error(await response.text()); - } - } else { - setPosition(null); - } - }, [selectedItem]); - - useEffectAsync(async () => { - const response = await fetch('/api/notifications/types'); - if (response.ok) { - const types = await response.json(); - setAllEventTypes([...allEventTypes, ...types.map((it) => [it.type, prefixString('event', it.type)])]); - } else { - throw Error(await response.text()); - } - }, []); - - const handleSubmit = useCatch(async ({ deviceId, from, to, type }) => { - const query = new URLSearchParams({ deviceId, from, to }); - eventTypes.forEach((it) => query.append('type', it)); - if (type === 'export') { - window.location.assign(`/api/reports/events/xlsx?${query.toString()}`); - } else if (type === 'mail') { - const response = await fetch(`/api/reports/events/mail?${query.toString()}`); - if (!response.ok) { - throw Error(await response.text()); - } - } else { - setLoading(true); - try { - const response = await fetch(`/api/reports/events?${query.toString()}`, { - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - } - }); - - const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { - report.type = 'events'; - if (eventTypes[0] !== 'allEvents') { - report.attributes.types = eventTypes.join(','); - } - const error = await scheduleReport(deviceIds, groupIds, report); - if (error) { - throw Error(error); - } else { - navigate('/reports/scheduled'); - } - }); - - const formatValue = (item, key) => { - switch (key) { - case 'eventTime': - return formatTime(item[key], 'seconds', hours12); - case 'type': - return t(prefixString('event', item[key])); - case 'geofenceId': - if (item[key] > 0) { - const geofence = geofences[item[key]]; - return geofence && geofence.name; - } - return null; - case 'maintenanceId': - return item[key] > 0 ? item[key] > 0 : null; - case 'attributes': - switch (item.type) { - case 'alarm': - return t(prefixString('alarm', item.attributes.alarm)); - case 'deviceOverspeed': - return formatSpeed(item.attributes.speed, speedUnit, t); - case 'driverChanged': - return item.attributes.driverUniqueId; - case 'media': - return ({item.attributes.file}); - case 'commandResult': - return item.attributes.result; - default: - return ''; - } - default: - return item[key]; - } - }; - - return ( - } breadcrumbs={['reportTitle', 'reportEvents']}> -
- {selectedItem && ( -
- - - {position && } - - {position && } -
- )} -
-
- -
- - {t('reportEventTypes')} - - -
- -
-
- - - - - {columns.map((key) => ({t(columnsMap.get(key))}))} - - - - {!loading ? items.map((item) => ( - - - {item.positionId ? selectedItem === item ? ( - setSelectedItem(null)}> - - - ) : ( - setSelectedItem(item)}> - - - ) : ''} - - {columns.map((key) => ( - - {formatValue(item, key)} - - ))} - - )) : ()} - -
-
-
-
- ); -}; - -export default EventReportPage; diff --git a/modern/src/reports/EventReportPage.jsx b/modern/src/reports/EventReportPage.jsx new file mode 100644 index 00000000..10b539ab --- /dev/null +++ b/modern/src/reports/EventReportPage.jsx @@ -0,0 +1,232 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableCell, TableBody, Link, IconButton, +} from '@mui/material'; +import GpsFixedIcon from '@mui/icons-material/GpsFixed'; +import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; +import { useSelector } from 'react-redux'; +import { formatSpeed, formatTime } from '../common/util/formatter'; +import ReportFilter from './components/ReportFilter'; +import { prefixString } from '../common/util/stringUtils'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import usePersistedState from '../common/util/usePersistedState'; +import ColumnSelect from './components/ColumnSelect'; +import { useCatch, useEffectAsync } from '../reactHelper'; +import useReportStyles from './common/useReportStyles'; +import TableShimmer from '../common/components/TableShimmer'; +import { useAttributePreference, usePreference } from '../common/util/preferences'; +import MapView from '../map/core/MapView'; +import MapGeofence from '../map/MapGeofence'; +import MapPositions from '../map/MapPositions'; +import MapCamera from '../map/MapCamera'; +import scheduleReport from './common/scheduleReport'; + +const columnsArray = [ + ['eventTime', 'positionFixTime'], + ['type', 'sharedType'], + ['geofenceId', 'sharedGeofence'], + ['maintenanceId', 'sharedMaintenance'], + ['attributes', 'commandData'], +]; +const columnsMap = new Map(columnsArray); + +const EventReportPage = () => { + const navigate = useNavigate(); + const classes = useReportStyles(); + const t = useTranslation(); + + const devices = useSelector((state) => state.devices.items); + const geofences = useSelector((state) => state.geofences.items); + + const speedUnit = useAttributePreference('speedUnit'); + const hours12 = usePreference('twelveHourFormat'); + + const [allEventTypes, setAllEventTypes] = useState([['allEvents', 'eventAll']]); + + const [columns, setColumns] = usePersistedState('eventColumns', ['eventTime', 'type', 'attributes']); + const [eventTypes, setEventTypes] = useState(['allEvents']); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [position, setPosition] = useState(null); + + useEffectAsync(async () => { + if (selectedItem) { + const response = await fetch(`/api/positions?id=${selectedItem.positionId}`); + if (response.ok) { + const positions = await response.json(); + if (positions.length > 0) { + setPosition(positions[0]); + } + } else { + throw Error(await response.text()); + } + } else { + setPosition(null); + } + }, [selectedItem]); + + useEffectAsync(async () => { + const response = await fetch('/api/notifications/types'); + if (response.ok) { + const types = await response.json(); + setAllEventTypes([...allEventTypes, ...types.map((it) => [it.type, prefixString('event', it.type)])]); + } else { + throw Error(await response.text()); + } + }, []); + + const handleSubmit = useCatch(async ({ deviceId, from, to, type }) => { + const query = new URLSearchParams({ deviceId, from, to }); + eventTypes.forEach((it) => query.append('type', it)); + if (type === 'export') { + window.location.assign(`/api/reports/events/xlsx?${query.toString()}`); + } else if (type === 'mail') { + const response = await fetch(`/api/reports/events/mail?${query.toString()}`); + if (!response.ok) { + throw Error(await response.text()); + } + } else { + setLoading(true); + try { + const response = await fetch(`/api/reports/events?${query.toString()}`, { + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + } + }); + + const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { + report.type = 'events'; + if (eventTypes[0] !== 'allEvents') { + report.attributes.types = eventTypes.join(','); + } + const error = await scheduleReport(deviceIds, groupIds, report); + if (error) { + throw Error(error); + } else { + navigate('/reports/scheduled'); + } + }); + + const formatValue = (item, key) => { + switch (key) { + case 'eventTime': + return formatTime(item[key], 'seconds', hours12); + case 'type': + return t(prefixString('event', item[key])); + case 'geofenceId': + if (item[key] > 0) { + const geofence = geofences[item[key]]; + return geofence && geofence.name; + } + return null; + case 'maintenanceId': + return item[key] > 0 ? item[key] > 0 : null; + case 'attributes': + switch (item.type) { + case 'alarm': + return t(prefixString('alarm', item.attributes.alarm)); + case 'deviceOverspeed': + return formatSpeed(item.attributes.speed, speedUnit, t); + case 'driverChanged': + return item.attributes.driverUniqueId; + case 'media': + return ({item.attributes.file}); + case 'commandResult': + return item.attributes.result; + default: + return ''; + } + default: + return item[key]; + } + }; + + return ( + } breadcrumbs={['reportTitle', 'reportEvents']}> +
+ {selectedItem && ( +
+ + + {position && } + + {position && } +
+ )} +
+
+ +
+ + {t('reportEventTypes')} + + +
+ +
+
+ + + + + {columns.map((key) => ({t(columnsMap.get(key))}))} + + + + {!loading ? items.map((item) => ( + + + {item.positionId ? selectedItem === item ? ( + setSelectedItem(null)}> + + + ) : ( + setSelectedItem(item)}> + + + ) : ''} + + {columns.map((key) => ( + + {formatValue(item, key)} + + ))} + + )) : ()} + +
+
+
+
+ ); +}; + +export default EventReportPage; diff --git a/modern/src/reports/RouteReportPage.js b/modern/src/reports/RouteReportPage.js deleted file mode 100644 index 5003ff31..00000000 --- a/modern/src/reports/RouteReportPage.js +++ /dev/null @@ -1,173 +0,0 @@ -import React, { Fragment, useCallback, useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; -import { - IconButton, Table, TableBody, TableCell, TableHead, TableRow, -} from '@mui/material'; -import GpsFixedIcon from '@mui/icons-material/GpsFixed'; -import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; -import ReportFilter from './components/ReportFilter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import PositionValue from '../common/components/PositionValue'; -import ColumnSelect from './components/ColumnSelect'; -import usePositionAttributes from '../common/attributes/usePositionAttributes'; -import { useCatch } from '../reactHelper'; -import MapView from '../map/core/MapView'; -import MapRoutePath from '../map/MapRoutePath'; -import MapRoutePoints from '../map/MapRoutePoints'; -import MapPositions from '../map/MapPositions'; -import useReportStyles from './common/useReportStyles'; -import TableShimmer from '../common/components/TableShimmer'; -import MapCamera from '../map/MapCamera'; -import MapGeofence from '../map/MapGeofence'; -import scheduleReport from './common/scheduleReport'; - -const RouteReportPage = () => { - const navigate = useNavigate(); - const classes = useReportStyles(); - const t = useTranslation(); - - const positionAttributes = usePositionAttributes(t); - - const devices = useSelector((state) => state.devices.items); - - const [available, setAvailable] = useState([]); - const [columns, setColumns] = useState(['fixTime', 'latitude', 'longitude', 'speed', 'address']); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedItem, setSelectedItem] = useState(null); - - const onMapPointClick = useCallback((positionId) => { - setSelectedItem(items.find((it) => it.id === positionId)); - }, [items, setSelectedItem]); - - const handleSubmit = useCatch(async ({ deviceIds, from, to, type }) => { - const query = new URLSearchParams({ from, to }); - deviceIds.forEach((deviceId) => query.append('deviceId', deviceId)); - if (type === 'export') { - window.location.assign(`/api/reports/route/xlsx?${query.toString()}`); - } else if (type === 'mail') { - const response = await fetch(`/api/reports/route/mail?${query.toString()}`); - if (!response.ok) { - throw Error(await response.text()); - } - } else { - setLoading(true); - try { - const response = await fetch(`/api/reports/route?${query.toString()}`, { - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - const data = await response.json(); - const keySet = new Set(); - const keyList = []; - data.forEach((position) => { - Object.keys(position).forEach((it) => keySet.add(it)); - Object.keys(position.attributes).forEach((it) => keySet.add(it)); - }); - ['id', 'deviceId', 'outdated', 'network', 'attributes'].forEach((key) => keySet.delete(key)); - Object.keys(positionAttributes).forEach((key) => { - if (keySet.has(key)) { - keyList.push(key); - keySet.delete(key); - } - }); - setAvailable([...keyList, ...keySet].map((key) => [key, positionAttributes[key]?.name || key])); - setItems(data); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - } - }); - - const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { - report.type = 'route'; - const error = await scheduleReport(deviceIds, groupIds, report); - if (error) { - throw Error(error); - } else { - navigate('/reports/scheduled'); - } - }); - - return ( - } breadcrumbs={['reportTitle', 'reportRoute']}> -
- {selectedItem && ( -
- - - {[...new Set(items.map((it) => it.deviceId))].map((deviceId) => { - const positions = items.filter((position) => position.deviceId === deviceId); - return ( - - - - - ); - })} - - - -
- )} -
-
- - - -
- - - - - {t('sharedDevice')} - {columns.map((key) => ({positionAttributes[key]?.name || key}))} - - - - {!loading ? items.slice(0, 4000).map((item) => ( - - - {selectedItem === item ? ( - setSelectedItem(null)}> - - - ) : ( - setSelectedItem(item)}> - - - )} - - {devices[item.deviceId].name} - {columns.map((key) => ( - - - - ))} - - )) : ()} - -
-
-
-
- ); -}; - -export default RouteReportPage; diff --git a/modern/src/reports/RouteReportPage.jsx b/modern/src/reports/RouteReportPage.jsx new file mode 100644 index 00000000..5003ff31 --- /dev/null +++ b/modern/src/reports/RouteReportPage.jsx @@ -0,0 +1,173 @@ +import React, { Fragment, useCallback, useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { + IconButton, Table, TableBody, TableCell, TableHead, TableRow, +} from '@mui/material'; +import GpsFixedIcon from '@mui/icons-material/GpsFixed'; +import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; +import ReportFilter from './components/ReportFilter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import PositionValue from '../common/components/PositionValue'; +import ColumnSelect from './components/ColumnSelect'; +import usePositionAttributes from '../common/attributes/usePositionAttributes'; +import { useCatch } from '../reactHelper'; +import MapView from '../map/core/MapView'; +import MapRoutePath from '../map/MapRoutePath'; +import MapRoutePoints from '../map/MapRoutePoints'; +import MapPositions from '../map/MapPositions'; +import useReportStyles from './common/useReportStyles'; +import TableShimmer from '../common/components/TableShimmer'; +import MapCamera from '../map/MapCamera'; +import MapGeofence from '../map/MapGeofence'; +import scheduleReport from './common/scheduleReport'; + +const RouteReportPage = () => { + const navigate = useNavigate(); + const classes = useReportStyles(); + const t = useTranslation(); + + const positionAttributes = usePositionAttributes(t); + + const devices = useSelector((state) => state.devices.items); + + const [available, setAvailable] = useState([]); + const [columns, setColumns] = useState(['fixTime', 'latitude', 'longitude', 'speed', 'address']); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + + const onMapPointClick = useCallback((positionId) => { + setSelectedItem(items.find((it) => it.id === positionId)); + }, [items, setSelectedItem]); + + const handleSubmit = useCatch(async ({ deviceIds, from, to, type }) => { + const query = new URLSearchParams({ from, to }); + deviceIds.forEach((deviceId) => query.append('deviceId', deviceId)); + if (type === 'export') { + window.location.assign(`/api/reports/route/xlsx?${query.toString()}`); + } else if (type === 'mail') { + const response = await fetch(`/api/reports/route/mail?${query.toString()}`); + if (!response.ok) { + throw Error(await response.text()); + } + } else { + setLoading(true); + try { + const response = await fetch(`/api/reports/route?${query.toString()}`, { + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + const data = await response.json(); + const keySet = new Set(); + const keyList = []; + data.forEach((position) => { + Object.keys(position).forEach((it) => keySet.add(it)); + Object.keys(position.attributes).forEach((it) => keySet.add(it)); + }); + ['id', 'deviceId', 'outdated', 'network', 'attributes'].forEach((key) => keySet.delete(key)); + Object.keys(positionAttributes).forEach((key) => { + if (keySet.has(key)) { + keyList.push(key); + keySet.delete(key); + } + }); + setAvailable([...keyList, ...keySet].map((key) => [key, positionAttributes[key]?.name || key])); + setItems(data); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + } + }); + + const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { + report.type = 'route'; + const error = await scheduleReport(deviceIds, groupIds, report); + if (error) { + throw Error(error); + } else { + navigate('/reports/scheduled'); + } + }); + + return ( + } breadcrumbs={['reportTitle', 'reportRoute']}> +
+ {selectedItem && ( +
+ + + {[...new Set(items.map((it) => it.deviceId))].map((deviceId) => { + const positions = items.filter((position) => position.deviceId === deviceId); + return ( + + + + + ); + })} + + + +
+ )} +
+
+ + + +
+ + + + + {t('sharedDevice')} + {columns.map((key) => ({positionAttributes[key]?.name || key}))} + + + + {!loading ? items.slice(0, 4000).map((item) => ( + + + {selectedItem === item ? ( + setSelectedItem(null)}> + + + ) : ( + setSelectedItem(item)}> + + + )} + + {devices[item.deviceId].name} + {columns.map((key) => ( + + + + ))} + + )) : ()} + +
+
+
+
+ ); +}; + +export default RouteReportPage; diff --git a/modern/src/reports/ScheduledPage.js b/modern/src/reports/ScheduledPage.js deleted file mode 100644 index 50e335d5..00000000 --- a/modern/src/reports/ScheduledPage.js +++ /dev/null @@ -1,106 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { - Table, TableRow, TableCell, TableHead, TableBody, IconButton, -} from '@mui/material'; -import makeStyles from '@mui/styles/makeStyles'; -import DeleteIcon from '@mui/icons-material/Delete'; -import { useEffectAsync } from '../reactHelper'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import TableShimmer from '../common/components/TableShimmer'; -import RemoveDialog from '../common/components/RemoveDialog'; - -const useStyles = makeStyles((theme) => ({ - columnAction: { - width: '1%', - paddingRight: theme.spacing(1), - }, -})); - -const ScheduledPage = () => { - const classes = useStyles(); - const t = useTranslation(); - - const calendars = useSelector((state) => state.calendars.items); - - const [timestamp, setTimestamp] = useState(Date.now()); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - const [removingId, setRemovingId] = useState(); - - useEffectAsync(async () => { - setLoading(true); - try { - const response = await fetch('/api/reports'); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }, [timestamp]); - - const formatType = (type) => { - switch (type) { - case 'events': - return t('reportEvents'); - case 'route': - return t('reportRoute'); - case 'summary': - return t('reportSummary'); - case 'trips': - return t('reportTrips'); - case 'stops': - return t('reportStops'); - default: - return type; - } - }; - - return ( - } breadcrumbs={['settingsTitle', 'reportScheduled']}> - - - - {t('sharedType')} - {t('sharedDescription')} - {t('sharedCalendar')} - - - - - {!loading ? items.map((item) => ( - - {formatType(item.type)} - {item.description} - {calendars[item.calendarId].name} - - setRemovingId(item.id)}> - - - - - )) : ()} - -
- { - setRemovingId(null); - if (removed) { - setTimestamp(Date.now()); - } - }} - /> -
- ); -}; - -export default ScheduledPage; diff --git a/modern/src/reports/ScheduledPage.jsx b/modern/src/reports/ScheduledPage.jsx new file mode 100644 index 00000000..50e335d5 --- /dev/null +++ b/modern/src/reports/ScheduledPage.jsx @@ -0,0 +1,106 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { + Table, TableRow, TableCell, TableHead, TableBody, IconButton, +} from '@mui/material'; +import makeStyles from '@mui/styles/makeStyles'; +import DeleteIcon from '@mui/icons-material/Delete'; +import { useEffectAsync } from '../reactHelper'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import TableShimmer from '../common/components/TableShimmer'; +import RemoveDialog from '../common/components/RemoveDialog'; + +const useStyles = makeStyles((theme) => ({ + columnAction: { + width: '1%', + paddingRight: theme.spacing(1), + }, +})); + +const ScheduledPage = () => { + const classes = useStyles(); + const t = useTranslation(); + + const calendars = useSelector((state) => state.calendars.items); + + const [timestamp, setTimestamp] = useState(Date.now()); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [removingId, setRemovingId] = useState(); + + useEffectAsync(async () => { + setLoading(true); + try { + const response = await fetch('/api/reports'); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }, [timestamp]); + + const formatType = (type) => { + switch (type) { + case 'events': + return t('reportEvents'); + case 'route': + return t('reportRoute'); + case 'summary': + return t('reportSummary'); + case 'trips': + return t('reportTrips'); + case 'stops': + return t('reportStops'); + default: + return type; + } + }; + + return ( + } breadcrumbs={['settingsTitle', 'reportScheduled']}> + + + + {t('sharedType')} + {t('sharedDescription')} + {t('sharedCalendar')} + + + + + {!loading ? items.map((item) => ( + + {formatType(item.type)} + {item.description} + {calendars[item.calendarId].name} + + setRemovingId(item.id)}> + + + + + )) : ()} + +
+ { + setRemovingId(null); + if (removed) { + setTimestamp(Date.now()); + } + }} + /> +
+ ); +}; + +export default ScheduledPage; diff --git a/modern/src/reports/StatisticsPage.js b/modern/src/reports/StatisticsPage.js deleted file mode 100644 index 7b3f2879..00000000 --- a/modern/src/reports/StatisticsPage.js +++ /dev/null @@ -1,85 +0,0 @@ -import React, { useState } from 'react'; -import { - Table, TableRow, TableCell, TableHead, TableBody, -} from '@mui/material'; -import { formatTime } from '../common/util/formatter'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import ReportFilter from './components/ReportFilter'; -import usePersistedState from '../common/util/usePersistedState'; -import ColumnSelect from './components/ColumnSelect'; -import { useCatch } from '../reactHelper'; -import useReportStyles from './common/useReportStyles'; -import TableShimmer from '../common/components/TableShimmer'; -import { usePreference } from '../common/util/preferences'; - -const columnsArray = [ - ['captureTime', 'statisticsCaptureTime'], - ['activeUsers', 'statisticsActiveUsers'], - ['activeDevices', 'statisticsActiveDevices'], - ['requests', 'statisticsRequests'], - ['messagesReceived', 'statisticsMessagesReceived'], - ['messagesStored', 'statisticsMessagesStored'], - ['mailSent', 'notificatorMail'], - ['smsSent', 'notificatorSms'], - ['geocoderRequests', 'statisticsGeocoder'], - ['geolocationRequests', 'statisticsGeolocation'], -]; -const columnsMap = new Map(columnsArray); - -const StatisticsPage = () => { - const classes = useReportStyles(); - const t = useTranslation(); - - const hours12 = usePreference('twelveHourFormat'); - - const [columns, setColumns] = usePersistedState('statisticsColumns', ['captureTime', 'activeUsers', 'activeDevices', 'messagesStored']); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - - const handleSubmit = useCatch(async ({ from, to }) => { - setLoading(true); - try { - const query = new URLSearchParams({ from, to }); - const response = await fetch(`/api/statistics?${query.toString()}`); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - }); - - return ( - } breadcrumbs={['reportTitle', 'statisticsTitle']}> -
- - - -
- - - - {columns.map((key) => ({t(columnsMap.get(key))}))} - - - - {!loading ? items.map((item) => ( - - {columns.map((key) => ( - - {key === 'captureTime' ? formatTime(item[key], 'date', hours12) : item[key]} - - ))} - - )) : ()} - -
-
- ); -}; - -export default StatisticsPage; diff --git a/modern/src/reports/StatisticsPage.jsx b/modern/src/reports/StatisticsPage.jsx new file mode 100644 index 00000000..7b3f2879 --- /dev/null +++ b/modern/src/reports/StatisticsPage.jsx @@ -0,0 +1,85 @@ +import React, { useState } from 'react'; +import { + Table, TableRow, TableCell, TableHead, TableBody, +} from '@mui/material'; +import { formatTime } from '../common/util/formatter'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import ReportFilter from './components/ReportFilter'; +import usePersistedState from '../common/util/usePersistedState'; +import ColumnSelect from './components/ColumnSelect'; +import { useCatch } from '../reactHelper'; +import useReportStyles from './common/useReportStyles'; +import TableShimmer from '../common/components/TableShimmer'; +import { usePreference } from '../common/util/preferences'; + +const columnsArray = [ + ['captureTime', 'statisticsCaptureTime'], + ['activeUsers', 'statisticsActiveUsers'], + ['activeDevices', 'statisticsActiveDevices'], + ['requests', 'statisticsRequests'], + ['messagesReceived', 'statisticsMessagesReceived'], + ['messagesStored', 'statisticsMessagesStored'], + ['mailSent', 'notificatorMail'], + ['smsSent', 'notificatorSms'], + ['geocoderRequests', 'statisticsGeocoder'], + ['geolocationRequests', 'statisticsGeolocation'], +]; +const columnsMap = new Map(columnsArray); + +const StatisticsPage = () => { + const classes = useReportStyles(); + const t = useTranslation(); + + const hours12 = usePreference('twelveHourFormat'); + + const [columns, setColumns] = usePersistedState('statisticsColumns', ['captureTime', 'activeUsers', 'activeDevices', 'messagesStored']); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + + const handleSubmit = useCatch(async ({ from, to }) => { + setLoading(true); + try { + const query = new URLSearchParams({ from, to }); + const response = await fetch(`/api/statistics?${query.toString()}`); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + }); + + return ( + } breadcrumbs={['reportTitle', 'statisticsTitle']}> +
+ + + +
+ + + + {columns.map((key) => ({t(columnsMap.get(key))}))} + + + + {!loading ? items.map((item) => ( + + {columns.map((key) => ( + + {key === 'captureTime' ? formatTime(item[key], 'date', hours12) : item[key]} + + ))} + + )) : ()} + +
+
+ ); +}; + +export default StatisticsPage; diff --git a/modern/src/reports/StopReportPage.js b/modern/src/reports/StopReportPage.js deleted file mode 100644 index 3a6ee18d..00000000 --- a/modern/src/reports/StopReportPage.js +++ /dev/null @@ -1,172 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - IconButton, - Table, TableBody, TableCell, TableHead, TableRow, -} from '@mui/material'; -import GpsFixedIcon from '@mui/icons-material/GpsFixed'; -import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; -import { - formatDistance, formatHours, formatVolume, formatTime, -} from '../common/util/formatter'; -import ReportFilter from './components/ReportFilter'; -import { useAttributePreference, usePreference } from '../common/util/preferences'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import ColumnSelect from './components/ColumnSelect'; -import usePersistedState from '../common/util/usePersistedState'; -import { useCatch } from '../reactHelper'; -import useReportStyles from './common/useReportStyles'; -import MapPositions from '../map/MapPositions'; -import MapView from '../map/core/MapView'; -import MapCamera from '../map/MapCamera'; -import AddressValue from '../common/components/AddressValue'; -import TableShimmer from '../common/components/TableShimmer'; -import MapGeofence from '../map/MapGeofence'; -import scheduleReport from './common/scheduleReport'; - -const columnsArray = [ - ['startTime', 'reportStartTime'], - ['startOdometer', 'positionOdometer'], - ['address', 'positionAddress'], - ['endTime', 'reportEndTime'], - ['duration', 'reportDuration'], - ['engineHours', 'reportEngineHours'], - ['spentFuel', 'reportSpentFuel'], -]; -const columnsMap = new Map(columnsArray); - -const StopReportPage = () => { - const navigate = useNavigate(); - const classes = useReportStyles(); - const t = useTranslation(); - - const distanceUnit = useAttributePreference('distanceUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - const hours12 = usePreference('twelveHourFormat'); - - const [columns, setColumns] = usePersistedState('stopColumns', ['startTime', 'endTime', 'startOdometer', 'address']); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedItem, setSelectedItem] = useState(null); - - const handleSubmit = useCatch(async ({ deviceId, from, to, type }) => { - const query = new URLSearchParams({ deviceId, from, to }); - if (type === 'export') { - window.location.assign(`/api/reports/stops/xlsx?${query.toString()}`); - } else if (type === 'mail') { - const response = await fetch(`/api/reports/stops/mail?${query.toString()}`); - if (!response.ok) { - throw Error(await response.text()); - } - } else { - setLoading(true); - try { - const response = await fetch(`/api/reports/stops?${query.toString()}`, { - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - } - }); - - const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { - report.type = 'stops'; - const error = await scheduleReport(deviceIds, groupIds, report); - if (error) { - throw Error(error); - } else { - navigate('/reports/scheduled'); - } - }); - - const formatValue = (item, key) => { - switch (key) { - case 'startTime': - case 'endTime': - return formatTime(item[key], 'minutes', hours12); - case 'startOdometer': - return formatDistance(item[key], distanceUnit, t); - case 'duration': - return formatHours(item[key]); - case 'engineHours': - return formatHours(item[key]); - case 'spentFuel': - return formatVolume(item[key], volumeUnit, t); - case 'address': - return (); - default: - return item[key]; - } - }; - - return ( - } breadcrumbs={['reportTitle', 'reportStops']}> -
- {selectedItem && ( -
- - - - - -
- )} -
-
- - - -
- - - - - {columns.map((key) => ({t(columnsMap.get(key))}))} - - - - {!loading ? items.map((item) => ( - - - {selectedItem === item ? ( - setSelectedItem(null)}> - - - ) : ( - setSelectedItem(item)}> - - - )} - - {columns.map((key) => ( - - {formatValue(item, key)} - - ))} - - )) : ()} - -
-
-
-
- ); -}; - -export default StopReportPage; diff --git a/modern/src/reports/StopReportPage.jsx b/modern/src/reports/StopReportPage.jsx new file mode 100644 index 00000000..3a6ee18d --- /dev/null +++ b/modern/src/reports/StopReportPage.jsx @@ -0,0 +1,172 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + IconButton, + Table, TableBody, TableCell, TableHead, TableRow, +} from '@mui/material'; +import GpsFixedIcon from '@mui/icons-material/GpsFixed'; +import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; +import { + formatDistance, formatHours, formatVolume, formatTime, +} from '../common/util/formatter'; +import ReportFilter from './components/ReportFilter'; +import { useAttributePreference, usePreference } from '../common/util/preferences'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import ColumnSelect from './components/ColumnSelect'; +import usePersistedState from '../common/util/usePersistedState'; +import { useCatch } from '../reactHelper'; +import useReportStyles from './common/useReportStyles'; +import MapPositions from '../map/MapPositions'; +import MapView from '../map/core/MapView'; +import MapCamera from '../map/MapCamera'; +import AddressValue from '../common/components/AddressValue'; +import TableShimmer from '../common/components/TableShimmer'; +import MapGeofence from '../map/MapGeofence'; +import scheduleReport from './common/scheduleReport'; + +const columnsArray = [ + ['startTime', 'reportStartTime'], + ['startOdometer', 'positionOdometer'], + ['address', 'positionAddress'], + ['endTime', 'reportEndTime'], + ['duration', 'reportDuration'], + ['engineHours', 'reportEngineHours'], + ['spentFuel', 'reportSpentFuel'], +]; +const columnsMap = new Map(columnsArray); + +const StopReportPage = () => { + const navigate = useNavigate(); + const classes = useReportStyles(); + const t = useTranslation(); + + const distanceUnit = useAttributePreference('distanceUnit'); + const volumeUnit = useAttributePreference('volumeUnit'); + const hours12 = usePreference('twelveHourFormat'); + + const [columns, setColumns] = usePersistedState('stopColumns', ['startTime', 'endTime', 'startOdometer', 'address']); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + + const handleSubmit = useCatch(async ({ deviceId, from, to, type }) => { + const query = new URLSearchParams({ deviceId, from, to }); + if (type === 'export') { + window.location.assign(`/api/reports/stops/xlsx?${query.toString()}`); + } else if (type === 'mail') { + const response = await fetch(`/api/reports/stops/mail?${query.toString()}`); + if (!response.ok) { + throw Error(await response.text()); + } + } else { + setLoading(true); + try { + const response = await fetch(`/api/reports/stops?${query.toString()}`, { + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + } + }); + + const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { + report.type = 'stops'; + const error = await scheduleReport(deviceIds, groupIds, report); + if (error) { + throw Error(error); + } else { + navigate('/reports/scheduled'); + } + }); + + const formatValue = (item, key) => { + switch (key) { + case 'startTime': + case 'endTime': + return formatTime(item[key], 'minutes', hours12); + case 'startOdometer': + return formatDistance(item[key], distanceUnit, t); + case 'duration': + return formatHours(item[key]); + case 'engineHours': + return formatHours(item[key]); + case 'spentFuel': + return formatVolume(item[key], volumeUnit, t); + case 'address': + return (); + default: + return item[key]; + } + }; + + return ( + } breadcrumbs={['reportTitle', 'reportStops']}> +
+ {selectedItem && ( +
+ + + + + +
+ )} +
+
+ + + +
+ + + + + {columns.map((key) => ({t(columnsMap.get(key))}))} + + + + {!loading ? items.map((item) => ( + + + {selectedItem === item ? ( + setSelectedItem(null)}> + + + ) : ( + setSelectedItem(item)}> + + + )} + + {columns.map((key) => ( + + {formatValue(item, key)} + + ))} + + )) : ()} + +
+
+
+
+ ); +}; + +export default StopReportPage; diff --git a/modern/src/reports/SummaryReportPage.js b/modern/src/reports/SummaryReportPage.js deleted file mode 100644 index ec5395f1..00000000 --- a/modern/src/reports/SummaryReportPage.js +++ /dev/null @@ -1,152 +0,0 @@ -import React, { useState } from 'react'; -import { useSelector } from 'react-redux'; -import { useNavigate } from 'react-router-dom'; -import { - FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableBody, TableCell, -} from '@mui/material'; -import { - formatDistance, formatHours, formatSpeed, formatVolume, formatTime, -} from '../common/util/formatter'; -import ReportFilter from './components/ReportFilter'; -import { useAttributePreference, usePreference } from '../common/util/preferences'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import usePersistedState from '../common/util/usePersistedState'; -import ColumnSelect from './components/ColumnSelect'; -import { useCatch } from '../reactHelper'; -import useReportStyles from './common/useReportStyles'; -import TableShimmer from '../common/components/TableShimmer'; -import scheduleReport from './common/scheduleReport'; - -const columnsArray = [ - ['startTime', 'reportStartDate'], - ['distance', 'sharedDistance'], - ['startOdometer', 'reportStartOdometer'], - ['endOdometer', 'reportEndOdometer'], - ['averageSpeed', 'reportAverageSpeed'], - ['maxSpeed', 'reportMaximumSpeed'], - ['engineHours', 'reportEngineHours'], - ['spentFuel', 'reportSpentFuel'], -]; -const columnsMap = new Map(columnsArray); - -const SummaryReportPage = () => { - const navigate = useNavigate(); - const classes = useReportStyles(); - const t = useTranslation(); - - const devices = useSelector((state) => state.devices.items); - - const distanceUnit = useAttributePreference('distanceUnit'); - const speedUnit = useAttributePreference('speedUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - const hours12 = usePreference('twelveHourFormat'); - - const [columns, setColumns] = usePersistedState('summaryColumns', ['startTime', 'distance', 'averageSpeed']); - const [daily, setDaily] = useState(false); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - - const handleSubmit = useCatch(async ({ deviceIds, groupIds, from, to, type }) => { - const query = new URLSearchParams({ from, to, daily }); - deviceIds.forEach((deviceId) => query.append('deviceId', deviceId)); - groupIds.forEach((groupId) => query.append('groupId', groupId)); - if (type === 'export') { - window.location.assign(`/api/reports/summary/xlsx?${query.toString()}`); - } else if (type === 'mail') { - const response = await fetch(`/api/reports/summary/mail?${query.toString()}`); - if (!response.ok) { - throw Error(await response.text()); - } - } else { - setLoading(true); - try { - const response = await fetch(`/api/reports/summary?${query.toString()}`, { - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - } - }); - - const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { - report.type = 'summary'; - report.attributes.daily = daily; - const error = await scheduleReport(deviceIds, groupIds, report); - if (error) { - throw Error(error); - } else { - navigate('/reports/scheduled'); - } - }); - - const formatValue = (item, key) => { - switch (key) { - case 'deviceId': - return devices[item[key]].name; - case 'startTime': - return formatTime(item[key], 'date', hours12); - case 'startOdometer': - case 'endOdometer': - case 'distance': - return formatDistance(item[key], distanceUnit, t); - case 'averageSpeed': - case 'maxSpeed': - return formatSpeed(item[key], speedUnit, t); - case 'engineHours': - return formatHours(item[key]); - case 'spentFuel': - return formatVolume(item[key], volumeUnit, t); - default: - return item[key]; - } - }; - - return ( - } breadcrumbs={['reportTitle', 'reportSummary']}> -
- -
- - {t('sharedType')} - - -
- -
-
- - - - {t('sharedDevice')} - {columns.map((key) => ({t(columnsMap.get(key))}))} - - - - {!loading ? items.map((item) => ( - - {devices[item.deviceId].name} - {columns.map((key) => ( - - {formatValue(item, key)} - - ))} - - )) : ()} - -
-
- ); -}; - -export default SummaryReportPage; diff --git a/modern/src/reports/SummaryReportPage.jsx b/modern/src/reports/SummaryReportPage.jsx new file mode 100644 index 00000000..ec5395f1 --- /dev/null +++ b/modern/src/reports/SummaryReportPage.jsx @@ -0,0 +1,152 @@ +import React, { useState } from 'react'; +import { useSelector } from 'react-redux'; +import { useNavigate } from 'react-router-dom'; +import { + FormControl, InputLabel, Select, MenuItem, Table, TableHead, TableRow, TableBody, TableCell, +} from '@mui/material'; +import { + formatDistance, formatHours, formatSpeed, formatVolume, formatTime, +} from '../common/util/formatter'; +import ReportFilter from './components/ReportFilter'; +import { useAttributePreference, usePreference } from '../common/util/preferences'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import usePersistedState from '../common/util/usePersistedState'; +import ColumnSelect from './components/ColumnSelect'; +import { useCatch } from '../reactHelper'; +import useReportStyles from './common/useReportStyles'; +import TableShimmer from '../common/components/TableShimmer'; +import scheduleReport from './common/scheduleReport'; + +const columnsArray = [ + ['startTime', 'reportStartDate'], + ['distance', 'sharedDistance'], + ['startOdometer', 'reportStartOdometer'], + ['endOdometer', 'reportEndOdometer'], + ['averageSpeed', 'reportAverageSpeed'], + ['maxSpeed', 'reportMaximumSpeed'], + ['engineHours', 'reportEngineHours'], + ['spentFuel', 'reportSpentFuel'], +]; +const columnsMap = new Map(columnsArray); + +const SummaryReportPage = () => { + const navigate = useNavigate(); + const classes = useReportStyles(); + const t = useTranslation(); + + const devices = useSelector((state) => state.devices.items); + + const distanceUnit = useAttributePreference('distanceUnit'); + const speedUnit = useAttributePreference('speedUnit'); + const volumeUnit = useAttributePreference('volumeUnit'); + const hours12 = usePreference('twelveHourFormat'); + + const [columns, setColumns] = usePersistedState('summaryColumns', ['startTime', 'distance', 'averageSpeed']); + const [daily, setDaily] = useState(false); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + + const handleSubmit = useCatch(async ({ deviceIds, groupIds, from, to, type }) => { + const query = new URLSearchParams({ from, to, daily }); + deviceIds.forEach((deviceId) => query.append('deviceId', deviceId)); + groupIds.forEach((groupId) => query.append('groupId', groupId)); + if (type === 'export') { + window.location.assign(`/api/reports/summary/xlsx?${query.toString()}`); + } else if (type === 'mail') { + const response = await fetch(`/api/reports/summary/mail?${query.toString()}`); + if (!response.ok) { + throw Error(await response.text()); + } + } else { + setLoading(true); + try { + const response = await fetch(`/api/reports/summary?${query.toString()}`, { + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + } + }); + + const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { + report.type = 'summary'; + report.attributes.daily = daily; + const error = await scheduleReport(deviceIds, groupIds, report); + if (error) { + throw Error(error); + } else { + navigate('/reports/scheduled'); + } + }); + + const formatValue = (item, key) => { + switch (key) { + case 'deviceId': + return devices[item[key]].name; + case 'startTime': + return formatTime(item[key], 'date', hours12); + case 'startOdometer': + case 'endOdometer': + case 'distance': + return formatDistance(item[key], distanceUnit, t); + case 'averageSpeed': + case 'maxSpeed': + return formatSpeed(item[key], speedUnit, t); + case 'engineHours': + return formatHours(item[key]); + case 'spentFuel': + return formatVolume(item[key], volumeUnit, t); + default: + return item[key]; + } + }; + + return ( + } breadcrumbs={['reportTitle', 'reportSummary']}> +
+ +
+ + {t('sharedType')} + + +
+ +
+
+ + + + {t('sharedDevice')} + {columns.map((key) => ({t(columnsMap.get(key))}))} + + + + {!loading ? items.map((item) => ( + + {devices[item.deviceId].name} + {columns.map((key) => ( + + {formatValue(item, key)} + + ))} + + )) : ()} + +
+
+ ); +}; + +export default SummaryReportPage; diff --git a/modern/src/reports/TripReportPage.js b/modern/src/reports/TripReportPage.js deleted file mode 100644 index 606b08c5..00000000 --- a/modern/src/reports/TripReportPage.js +++ /dev/null @@ -1,216 +0,0 @@ -import React, { useState } from 'react'; -import { useNavigate } from 'react-router-dom'; -import { - IconButton, Table, TableBody, TableCell, TableHead, TableRow, -} from '@mui/material'; -import GpsFixedIcon from '@mui/icons-material/GpsFixed'; -import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; -import { - formatDistance, formatSpeed, formatHours, formatVolume, formatTime, -} from '../common/util/formatter'; -import ReportFilter from './components/ReportFilter'; -import { useAttributePreference, usePreference } from '../common/util/preferences'; -import { useTranslation } from '../common/components/LocalizationProvider'; -import PageLayout from '../common/components/PageLayout'; -import ReportsMenu from './components/ReportsMenu'; -import ColumnSelect from './components/ColumnSelect'; -import usePersistedState from '../common/util/usePersistedState'; -import { useCatch, useEffectAsync } from '../reactHelper'; -import useReportStyles from './common/useReportStyles'; -import MapView from '../map/core/MapView'; -import MapRoutePath from '../map/MapRoutePath'; -import AddressValue from '../common/components/AddressValue'; -import TableShimmer from '../common/components/TableShimmer'; -import MapMarkers from '../map/MapMarkers'; -import MapCamera from '../map/MapCamera'; -import MapGeofence from '../map/MapGeofence'; -import scheduleReport from './common/scheduleReport'; - -const columnsArray = [ - ['startTime', 'reportStartTime'], - ['startOdometer', 'reportStartOdometer'], - ['startAddress', 'reportStartAddress'], - ['endTime', 'reportEndTime'], - ['endOdometer', 'reportEndOdometer'], - ['endAddress', 'reportEndAddress'], - ['distance', 'sharedDistance'], - ['averageSpeed', 'reportAverageSpeed'], - ['maxSpeed', 'reportMaximumSpeed'], - ['duration', 'reportDuration'], - ['spentFuel', 'reportSpentFuel'], - ['driverName', 'sharedDriver'], -]; -const columnsMap = new Map(columnsArray); - -const TripReportPage = () => { - const navigate = useNavigate(); - const classes = useReportStyles(); - const t = useTranslation(); - - const distanceUnit = useAttributePreference('distanceUnit'); - const speedUnit = useAttributePreference('speedUnit'); - const volumeUnit = useAttributePreference('volumeUnit'); - const hours12 = usePreference('twelveHourFormat'); - - const [columns, setColumns] = usePersistedState('tripColumns', ['startTime', 'endTime', 'distance', 'averageSpeed']); - const [items, setItems] = useState([]); - const [loading, setLoading] = useState(false); - const [selectedItem, setSelectedItem] = useState(null); - const [route, setRoute] = useState(null); - - const createMarkers = () => ([ - { - latitude: selectedItem.startLat, - longitude: selectedItem.startLon, - image: 'default-negative', - }, - { - latitude: selectedItem.endLat, - longitude: selectedItem.endLon, - image: 'default-positive', - }, - ]); - - useEffectAsync(async () => { - if (selectedItem) { - const query = new URLSearchParams({ - deviceId: selectedItem.deviceId, - from: selectedItem.startTime, - to: selectedItem.endTime, - }); - const response = await fetch(`/api/reports/route?${query.toString()}`, { - headers: { - Accept: 'application/json', - }, - }); - if (response.ok) { - setRoute(await response.json()); - } else { - throw Error(await response.text()); - } - } else { - setRoute(null); - } - }, [selectedItem]); - - const handleSubmit = useCatch(async ({ deviceId, from, to, type }) => { - const query = new URLSearchParams({ deviceId, from, to }); - if (type === 'export') { - window.location.assign(`/api/reports/trips/xlsx?${query.toString()}`); - } else if (type === 'mail') { - const response = await fetch(`/api/reports/trips/mail?${query.toString()}`); - if (!response.ok) { - throw Error(await response.text()); - } - } else { - setLoading(true); - try { - const response = await fetch(`/api/reports/trips?${query.toString()}`, { - headers: { Accept: 'application/json' }, - }); - if (response.ok) { - setItems(await response.json()); - } else { - throw Error(await response.text()); - } - } finally { - setLoading(false); - } - } - }); - - const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { - report.type = 'trips'; - const error = await scheduleReport(deviceIds, groupIds, report); - if (error) { - throw Error(error); - } else { - navigate('/reports/scheduled'); - } - }); - - const formatValue = (item, key) => { - switch (key) { - case 'startTime': - case 'endTime': - return formatTime(item[key], 'minutes', hours12); - case 'startOdometer': - case 'endOdometer': - case 'distance': - return formatDistance(item[key], distanceUnit, t); - case 'averageSpeed': - case 'maxSpeed': - return formatSpeed(item[key], speedUnit, t); - case 'duration': - return formatHours(item[key]); - case 'spentFuel': - return formatVolume(item[key], volumeUnit, t); - case 'startAddress': - return (); - case 'endAddress': - return (); - default: - return item[key]; - } - }; - - return ( - } breadcrumbs={['reportTitle', 'reportTrips']}> -
- {selectedItem && ( -
- - - {route && ( - <> - - - - - )} - -
- )} -
-
- - - -
- - - - - {columns.map((key) => ({t(columnsMap.get(key))}))} - - - - {!loading ? items.map((item) => ( - - - {selectedItem === item ? ( - setSelectedItem(null)}> - - - ) : ( - setSelectedItem(item)}> - - - )} - - {columns.map((key) => ( - - {formatValue(item, key)} - - ))} - - )) : ()} - -
-
-
-
- ); -}; - -export default TripReportPage; diff --git a/modern/src/reports/TripReportPage.jsx b/modern/src/reports/TripReportPage.jsx new file mode 100644 index 00000000..606b08c5 --- /dev/null +++ b/modern/src/reports/TripReportPage.jsx @@ -0,0 +1,216 @@ +import React, { useState } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { + IconButton, Table, TableBody, TableCell, TableHead, TableRow, +} from '@mui/material'; +import GpsFixedIcon from '@mui/icons-material/GpsFixed'; +import LocationSearchingIcon from '@mui/icons-material/LocationSearching'; +import { + formatDistance, formatSpeed, formatHours, formatVolume, formatTime, +} from '../common/util/formatter'; +import ReportFilter from './components/ReportFilter'; +import { useAttributePreference, usePreference } from '../common/util/preferences'; +import { useTranslation } from '../common/components/LocalizationProvider'; +import PageLayout from '../common/components/PageLayout'; +import ReportsMenu from './components/ReportsMenu'; +import ColumnSelect from './components/ColumnSelect'; +import usePersistedState from '../common/util/usePersistedState'; +import { useCatch, useEffectAsync } from '../reactHelper'; +import useReportStyles from './common/useReportStyles'; +import MapView from '../map/core/MapView'; +import MapRoutePath from '../map/MapRoutePath'; +import AddressValue from '../common/components/AddressValue'; +import TableShimmer from '../common/components/TableShimmer'; +import MapMarkers from '../map/MapMarkers'; +import MapCamera from '../map/MapCamera'; +import MapGeofence from '../map/MapGeofence'; +import scheduleReport from './common/scheduleReport'; + +const columnsArray = [ + ['startTime', 'reportStartTime'], + ['startOdometer', 'reportStartOdometer'], + ['startAddress', 'reportStartAddress'], + ['endTime', 'reportEndTime'], + ['endOdometer', 'reportEndOdometer'], + ['endAddress', 'reportEndAddress'], + ['distance', 'sharedDistance'], + ['averageSpeed', 'reportAverageSpeed'], + ['maxSpeed', 'reportMaximumSpeed'], + ['duration', 'reportDuration'], + ['spentFuel', 'reportSpentFuel'], + ['driverName', 'sharedDriver'], +]; +const columnsMap = new Map(columnsArray); + +const TripReportPage = () => { + const navigate = useNavigate(); + const classes = useReportStyles(); + const t = useTranslation(); + + const distanceUnit = useAttributePreference('distanceUnit'); + const speedUnit = useAttributePreference('speedUnit'); + const volumeUnit = useAttributePreference('volumeUnit'); + const hours12 = usePreference('twelveHourFormat'); + + const [columns, setColumns] = usePersistedState('tripColumns', ['startTime', 'endTime', 'distance', 'averageSpeed']); + const [items, setItems] = useState([]); + const [loading, setLoading] = useState(false); + const [selectedItem, setSelectedItem] = useState(null); + const [route, setRoute] = useState(null); + + const createMarkers = () => ([ + { + latitude: selectedItem.startLat, + longitude: selectedItem.startLon, + image: 'default-negative', + }, + { + latitude: selectedItem.endLat, + longitude: selectedItem.endLon, + image: 'default-positive', + }, + ]); + + useEffectAsync(async () => { + if (selectedItem) { + const query = new URLSearchParams({ + deviceId: selectedItem.deviceId, + from: selectedItem.startTime, + to: selectedItem.endTime, + }); + const response = await fetch(`/api/reports/route?${query.toString()}`, { + headers: { + Accept: 'application/json', + }, + }); + if (response.ok) { + setRoute(await response.json()); + } else { + throw Error(await response.text()); + } + } else { + setRoute(null); + } + }, [selectedItem]); + + const handleSubmit = useCatch(async ({ deviceId, from, to, type }) => { + const query = new URLSearchParams({ deviceId, from, to }); + if (type === 'export') { + window.location.assign(`/api/reports/trips/xlsx?${query.toString()}`); + } else if (type === 'mail') { + const response = await fetch(`/api/reports/trips/mail?${query.toString()}`); + if (!response.ok) { + throw Error(await response.text()); + } + } else { + setLoading(true); + try { + const response = await fetch(`/api/reports/trips?${query.toString()}`, { + headers: { Accept: 'application/json' }, + }); + if (response.ok) { + setItems(await response.json()); + } else { + throw Error(await response.text()); + } + } finally { + setLoading(false); + } + } + }); + + const handleSchedule = useCatch(async (deviceIds, groupIds, report) => { + report.type = 'trips'; + const error = await scheduleReport(deviceIds, groupIds, report); + if (error) { + throw Error(error); + } else { + navigate('/reports/scheduled'); + } + }); + + const formatValue = (item, key) => { + switch (key) { + case 'startTime': + case 'endTime': + return formatTime(item[key], 'minutes', hours12); + case 'startOdometer': + case 'endOdometer': + case 'distance': + return formatDistance(item[key], distanceUnit, t); + case 'averageSpeed': + case 'maxSpeed': + return formatSpeed(item[key], speedUnit, t); + case 'duration': + return formatHours(item[key]); + case 'spentFuel': + return formatVolume(item[key], volumeUnit, t); + case 'startAddress': + return (); + case 'endAddress': + return (); + default: + return item[key]; + } + }; + + return ( + } breadcrumbs={['reportTitle', 'reportTrips']}> +
+ {selectedItem && ( +
+ + + {route && ( + <> + + + + + )} + +
+ )} +
+
+ + + +
+ + + + + {columns.map((key) => ({t(columnsMap.get(key))}))} + + + + {!loading ? items.map((item) => ( + + + {selectedItem === item ? ( + setSelectedItem(null)}> + + + ) : ( + setSelectedItem(item)}> + + + )} + + {columns.map((key) => ( + + {formatValue(item, key)} + + ))} + + )) : ()} + +
+
+
+
+ ); +}; + +export default TripReportPage; diff --git a/modern/src/reports/components/ColumnSelect.js b/modern/src/reports/components/ColumnSelect.js deleted file mode 100644 index d08394ea..00000000 --- a/modern/src/reports/components/ColumnSelect.js +++ /dev/null @@ -1,34 +0,0 @@ -import React from 'react'; -import { - FormControl, InputLabel, MenuItem, Select, -} from '@mui/material'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import useReportStyles from '../common/useReportStyles'; - -const ColumnSelect = ({ - columns, setColumns, columnsArray, rawValues, disabled, -}) => { - const classes = useReportStyles(); - const t = useTranslation(); - - return ( -
- - {t('sharedColumns')} - - -
- ); -}; - -export default ColumnSelect; diff --git a/modern/src/reports/components/ColumnSelect.jsx b/modern/src/reports/components/ColumnSelect.jsx new file mode 100644 index 00000000..d08394ea --- /dev/null +++ b/modern/src/reports/components/ColumnSelect.jsx @@ -0,0 +1,34 @@ +import React from 'react'; +import { + FormControl, InputLabel, MenuItem, Select, +} from '@mui/material'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import useReportStyles from '../common/useReportStyles'; + +const ColumnSelect = ({ + columns, setColumns, columnsArray, rawValues, disabled, +}) => { + const classes = useReportStyles(); + const t = useTranslation(); + + return ( +
+ + {t('sharedColumns')} + + +
+ ); +}; + +export default ColumnSelect; diff --git a/modern/src/reports/components/ReportFilter.js b/modern/src/reports/components/ReportFilter.js deleted file mode 100644 index ff0dc146..00000000 --- a/modern/src/reports/components/ReportFilter.js +++ /dev/null @@ -1,225 +0,0 @@ -import React, { useState } from 'react'; -import { - FormControl, InputLabel, Select, MenuItem, Button, TextField, Typography, -} from '@mui/material'; -import { useDispatch, useSelector } from 'react-redux'; -import moment from 'moment'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import useReportStyles from '../common/useReportStyles'; -import { devicesActions, reportsActions } from '../../store'; -import SplitButton from '../../common/components/SplitButton'; -import SelectField from '../../common/components/SelectField'; -import { useRestriction } from '../../common/util/permissions'; - -const ReportFilter = ({ children, handleSubmit, handleSchedule, showOnly, ignoreDevice, multiDevice, includeGroups }) => { - const classes = useReportStyles(); - const dispatch = useDispatch(); - const t = useTranslation(); - - const readonly = useRestriction('readonly'); - - const devices = useSelector((state) => state.devices.items); - const groups = useSelector((state) => state.groups.items); - - const deviceId = useSelector((state) => state.devices.selectedId); - const deviceIds = useSelector((state) => state.devices.selectedIds); - const groupIds = useSelector((state) => state.reports.groupIds); - const period = useSelector((state) => state.reports.period); - const from = useSelector((state) => state.reports.from); - const to = useSelector((state) => state.reports.to); - const [button, setButton] = useState('json'); - - const [description, setDescription] = useState(); - const [calendarId, setCalendarId] = useState(); - - const scheduleDisabled = button === 'schedule' && (!description || !calendarId); - const disabled = (!ignoreDevice && !deviceId && !deviceIds.length && !groupIds.length) || scheduleDisabled; - - const handleClick = (type) => { - if (type === 'schedule') { - handleSchedule(deviceIds, groupIds, { - description, - calendarId, - attributes: {}, - }); - } else { - let selectedFrom; - let selectedTo; - switch (period) { - case 'today': - selectedFrom = moment().startOf('day'); - selectedTo = moment().endOf('day'); - break; - case 'yesterday': - selectedFrom = moment().subtract(1, 'day').startOf('day'); - selectedTo = moment().subtract(1, 'day').endOf('day'); - break; - case 'thisWeek': - selectedFrom = moment().startOf('week'); - selectedTo = moment().endOf('week'); - break; - case 'previousWeek': - selectedFrom = moment().subtract(1, 'week').startOf('week'); - selectedTo = moment().subtract(1, 'week').endOf('week'); - break; - case 'thisMonth': - selectedFrom = moment().startOf('month'); - selectedTo = moment().endOf('month'); - break; - case 'previousMonth': - selectedFrom = moment().subtract(1, 'month').startOf('month'); - selectedTo = moment().subtract(1, 'month').endOf('month'); - break; - default: - selectedFrom = moment(from, moment.HTML5_FMT.DATETIME_LOCAL); - selectedTo = moment(to, moment.HTML5_FMT.DATETIME_LOCAL); - break; - } - - handleSubmit({ - deviceId, - deviceIds, - groupIds, - from: selectedFrom.toISOString(), - to: selectedTo.toISOString(), - calendarId, - type, - }); - } - }; - - return ( -
- {!ignoreDevice && ( -
- - {t(multiDevice ? 'deviceTitle' : 'reportDevice')} - - -
- )} - {includeGroups && ( -
- - {t('settingsGroups')} - - -
- )} - {button !== 'schedule' ? ( - <> -
- - {t('reportPeriod')} - - -
- {period === 'custom' && ( -
- dispatch(reportsActions.updateFrom(e.target.value))} - fullWidth - /> -
- )} - {period === 'custom' && ( -
- dispatch(reportsActions.updateTo(e.target.value))} - fullWidth - /> -
- )} - - ) : ( - <> -
- setDescription(event.target.value)} - label={t('sharedDescription')} - fullWidth - /> -
-
- setCalendarId(Number(event.target.value))} - endpoint="/api/calendars" - label={t('sharedCalendar')} - fullWidth - /> -
- - )} - {children} -
- {showOnly ? ( - - ) : ( - setButton(value)} - options={readonly ? { - json: t('reportShow'), - export: t('reportExport'), - mail: t('reportEmail'), - } : { - json: t('reportShow'), - export: t('reportExport'), - mail: t('reportEmail'), - schedule: t('reportSchedule'), - }} - /> - )} -
-
- ); -}; - -export default ReportFilter; diff --git a/modern/src/reports/components/ReportFilter.jsx b/modern/src/reports/components/ReportFilter.jsx new file mode 100644 index 00000000..ff0dc146 --- /dev/null +++ b/modern/src/reports/components/ReportFilter.jsx @@ -0,0 +1,225 @@ +import React, { useState } from 'react'; +import { + FormControl, InputLabel, Select, MenuItem, Button, TextField, Typography, +} from '@mui/material'; +import { useDispatch, useSelector } from 'react-redux'; +import moment from 'moment'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import useReportStyles from '../common/useReportStyles'; +import { devicesActions, reportsActions } from '../../store'; +import SplitButton from '../../common/components/SplitButton'; +import SelectField from '../../common/components/SelectField'; +import { useRestriction } from '../../common/util/permissions'; + +const ReportFilter = ({ children, handleSubmit, handleSchedule, showOnly, ignoreDevice, multiDevice, includeGroups }) => { + const classes = useReportStyles(); + const dispatch = useDispatch(); + const t = useTranslation(); + + const readonly = useRestriction('readonly'); + + const devices = useSelector((state) => state.devices.items); + const groups = useSelector((state) => state.groups.items); + + const deviceId = useSelector((state) => state.devices.selectedId); + const deviceIds = useSelector((state) => state.devices.selectedIds); + const groupIds = useSelector((state) => state.reports.groupIds); + const period = useSelector((state) => state.reports.period); + const from = useSelector((state) => state.reports.from); + const to = useSelector((state) => state.reports.to); + const [button, setButton] = useState('json'); + + const [description, setDescription] = useState(); + const [calendarId, setCalendarId] = useState(); + + const scheduleDisabled = button === 'schedule' && (!description || !calendarId); + const disabled = (!ignoreDevice && !deviceId && !deviceIds.length && !groupIds.length) || scheduleDisabled; + + const handleClick = (type) => { + if (type === 'schedule') { + handleSchedule(deviceIds, groupIds, { + description, + calendarId, + attributes: {}, + }); + } else { + let selectedFrom; + let selectedTo; + switch (period) { + case 'today': + selectedFrom = moment().startOf('day'); + selectedTo = moment().endOf('day'); + break; + case 'yesterday': + selectedFrom = moment().subtract(1, 'day').startOf('day'); + selectedTo = moment().subtract(1, 'day').endOf('day'); + break; + case 'thisWeek': + selectedFrom = moment().startOf('week'); + selectedTo = moment().endOf('week'); + break; + case 'previousWeek': + selectedFrom = moment().subtract(1, 'week').startOf('week'); + selectedTo = moment().subtract(1, 'week').endOf('week'); + break; + case 'thisMonth': + selectedFrom = moment().startOf('month'); + selectedTo = moment().endOf('month'); + break; + case 'previousMonth': + selectedFrom = moment().subtract(1, 'month').startOf('month'); + selectedTo = moment().subtract(1, 'month').endOf('month'); + break; + default: + selectedFrom = moment(from, moment.HTML5_FMT.DATETIME_LOCAL); + selectedTo = moment(to, moment.HTML5_FMT.DATETIME_LOCAL); + break; + } + + handleSubmit({ + deviceId, + deviceIds, + groupIds, + from: selectedFrom.toISOString(), + to: selectedTo.toISOString(), + calendarId, + type, + }); + } + }; + + return ( +
+ {!ignoreDevice && ( +
+ + {t(multiDevice ? 'deviceTitle' : 'reportDevice')} + + +
+ )} + {includeGroups && ( +
+ + {t('settingsGroups')} + + +
+ )} + {button !== 'schedule' ? ( + <> +
+ + {t('reportPeriod')} + + +
+ {period === 'custom' && ( +
+ dispatch(reportsActions.updateFrom(e.target.value))} + fullWidth + /> +
+ )} + {period === 'custom' && ( +
+ dispatch(reportsActions.updateTo(e.target.value))} + fullWidth + /> +
+ )} + + ) : ( + <> +
+ setDescription(event.target.value)} + label={t('sharedDescription')} + fullWidth + /> +
+
+ setCalendarId(Number(event.target.value))} + endpoint="/api/calendars" + label={t('sharedCalendar')} + fullWidth + /> +
+ + )} + {children} +
+ {showOnly ? ( + + ) : ( + setButton(value)} + options={readonly ? { + json: t('reportShow'), + export: t('reportExport'), + mail: t('reportEmail'), + } : { + json: t('reportShow'), + export: t('reportExport'), + mail: t('reportEmail'), + schedule: t('reportSchedule'), + }} + /> + )} +
+
+ ); +}; + +export default ReportFilter; diff --git a/modern/src/reports/components/ReportsMenu.js b/modern/src/reports/components/ReportsMenu.js deleted file mode 100644 index a6c38578..00000000 --- a/modern/src/reports/components/ReportsMenu.js +++ /dev/null @@ -1,110 +0,0 @@ -import React from 'react'; -import { - Divider, List, ListItemButton, ListItemIcon, ListItemText, -} from '@mui/material'; -import StarIcon from '@mui/icons-material/Star'; -import TimelineIcon from '@mui/icons-material/Timeline'; -import PauseCircleFilledIcon from '@mui/icons-material/PauseCircleFilled'; -import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled'; -import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'; -import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; -import TrendingUpIcon from '@mui/icons-material/TrendingUp'; -import BarChartIcon from '@mui/icons-material/BarChart'; -import RouteIcon from '@mui/icons-material/Route'; -import EventRepeatIcon from '@mui/icons-material/EventRepeat'; -import { Link, useLocation } from 'react-router-dom'; -import { useTranslation } from '../../common/components/LocalizationProvider'; -import { useAdministrator, useRestriction } from '../../common/util/permissions'; - -const MenuItem = ({ - title, link, icon, selected, -}) => ( - - {icon} - - -); - -const ReportsMenu = () => { - const t = useTranslation(); - const location = useLocation(); - - const admin = useAdministrator(); - const readonly = useRestriction('readonly'); - - return ( - <> - - } - selected={location.pathname === '/reports/combined'} - /> - } - selected={location.pathname === '/reports/route'} - /> - } - selected={location.pathname === '/reports/event'} - /> - } - selected={location.pathname === '/reports/trip'} - /> - } - selected={location.pathname === '/reports/stop'} - /> - } - selected={location.pathname === '/reports/summary'} - /> - } - selected={location.pathname === '/reports/chart'} - /> - } - /> - - {(admin || !readonly) && ( - <> - - - } - /> - {admin && ( - } - selected={location.pathname === '/reports/statistics'} - /> - )} - - - )} - - ); -}; - -export default ReportsMenu; diff --git a/modern/src/reports/components/ReportsMenu.jsx b/modern/src/reports/components/ReportsMenu.jsx new file mode 100644 index 00000000..a6c38578 --- /dev/null +++ b/modern/src/reports/components/ReportsMenu.jsx @@ -0,0 +1,110 @@ +import React from 'react'; +import { + Divider, List, ListItemButton, ListItemIcon, ListItemText, +} from '@mui/material'; +import StarIcon from '@mui/icons-material/Star'; +import TimelineIcon from '@mui/icons-material/Timeline'; +import PauseCircleFilledIcon from '@mui/icons-material/PauseCircleFilled'; +import PlayCircleFilledIcon from '@mui/icons-material/PlayCircleFilled'; +import NotificationsActiveIcon from '@mui/icons-material/NotificationsActive'; +import FormatListBulletedIcon from '@mui/icons-material/FormatListBulleted'; +import TrendingUpIcon from '@mui/icons-material/TrendingUp'; +import BarChartIcon from '@mui/icons-material/BarChart'; +import RouteIcon from '@mui/icons-material/Route'; +import EventRepeatIcon from '@mui/icons-material/EventRepeat'; +import { Link, useLocation } from 'react-router-dom'; +import { useTranslation } from '../../common/components/LocalizationProvider'; +import { useAdministrator, useRestriction } from '../../common/util/permissions'; + +const MenuItem = ({ + title, link, icon, selected, +}) => ( + + {icon} + + +); + +const ReportsMenu = () => { + const t = useTranslation(); + const location = useLocation(); + + const admin = useAdministrator(); + const readonly = useRestriction('readonly'); + + return ( + <> + + } + selected={location.pathname === '/reports/combined'} + /> + } + selected={location.pathname === '/reports/route'} + /> + } + selected={location.pathname === '/reports/event'} + /> + } + selected={location.pathname === '/reports/trip'} + /> + } + selected={location.pathname === '/reports/stop'} + /> + } + selected={location.pathname === '/reports/summary'} + /> + } + selected={location.pathname === '/reports/chart'} + /> + } + /> + + {(admin || !readonly) && ( + <> + + + } + /> + {admin && ( + } + selected={location.pathname === '/reports/statistics'} + /> + )} + + + )} + + ); +}; + +export default ReportsMenu; -- cgit v1.2.3