diff --git a/initialize_container.sh b/initialize_container.sh index 1a025ddb0..bdfb124ec 100755 --- a/initialize_container.sh +++ b/initialize_container.sh @@ -8,7 +8,7 @@ RESULT='window.env = {' RESULT+='"API_URL": "'$API_URL RESULT+='", "OAUTH_CLIENT_ID": "'$OAUTH_CLIENT_ID RESULT+='", "OAUTH_TOKEN_URL": "'$OAUTH_TOKEN_URL -RESULT+='", "MAP_API_URL": "'$MAP_API_URL +RESULT+='", "MAP_API_KEY": "'$MAP_API_KEY RESULT+='", "GEO_CODE_API": "'$GEO_CODE_API RESULT+='", "ALERT_SOCKET_URL": "'$ALERT_SOCKET_URL RESULT+='", "CUSTODIAN_URL": "'$CUSTODIAN_URL diff --git a/package.json b/package.json index a909719a0..ca729a52a 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "buildly-react-template", - "version": "1.4.0", + "version": "1.4.1", "description": "Frontend Template from Buildly built using the React framework", "main": "src/index.js", "private": true, @@ -21,6 +21,7 @@ "@mui/material": "^5.4.2", "@mui/styles": "^5.4.2", "@mui/x-date-pickers": "5.0.0-beta.2", + "@react-google-maps/api": "^2.19.3", "@types/react": "^16.8.18", "@types/react-dom": "^16.8.4", "chart.js": "^2.9.4", @@ -154,4 +155,4 @@ ], "author": "Buildly", "license": "ISC" -} +} \ No newline at end of file diff --git a/public/index.html b/public/index.html index 2360767b1..688524d4a 100644 --- a/public/index.html +++ b/public/index.html @@ -4,6 +4,9 @@ + + + Transparent Path diff --git a/src/components/MapComponent/MapComponent.js b/src/components/MapComponent/MapComponent.js index e773c6d4e..9c6ce35e8 100644 --- a/src/components/MapComponent/MapComponent.js +++ b/src/components/MapComponent/MapComponent.js @@ -1,16 +1,17 @@ +/* eslint-disable no-console */ import React, { useState, useEffect } from 'react'; -import Geocode from 'react-geocode'; import { - withScriptjs, - withGoogleMap, GoogleMap, Marker, InfoWindow, Polyline, Polygon, Circle, -} from 'react-google-maps'; -import MarkerClusterer from 'react-google-maps/lib/components/addons/MarkerClusterer'; + MarkerClusterer, + useJsApiLoader, + LoadScript, +} from '@react-google-maps/api'; +import Geocode from 'react-geocode'; import _ from 'lodash'; import { Grid, useTheme } from '@mui/material'; import { @@ -20,30 +21,39 @@ import { Battery50 as Battery50Icon, CalendarToday as CalendarIcon, } from '@mui/icons-material'; -import { - MARKER_DATA, - getIcon, -} from '@utils/constants'; +import { MARKER_DATA, getIcon } from '@utils/constants'; + +const libraries = ['places', 'geometry', 'drawing']; export const MapComponent = (props) => { const { + isMarkerShown, + showPath, + screenshotMapCenter, + noInitialInfo, markers, + zoom, setSelectedMarker, - geofence, unitOfMeasure, allMarkers, - zoom, - screenshotMapCenter, - noInitialInfo, + geofence, + containerStyle, + setSelectedCluster, + selectedCluster, } = props; - const [center, setCenter] = useState({ - lat: 47.606209, - lng: -122.332069, - }); + + const [center, setCenter] = useState({ lat: 47.606209, lng: -122.332069 }); const [mapZoom, setMapZoom] = useState(zoom); const [showInfoIndex, setShowInfoIndex] = useState({}); const [polygon, setPolygon] = useState({}); + const theme = useTheme(); + + const { isLoaded } = useJsApiLoader({ + googleMapsApiKey: window.env.MAP_API_KEY, + libraries, + }); + useEffect(() => { if (screenshotMapCenter) { const meanCenter = { lat: _.mean(_.map(markers, 'lat')), lng: _.mean(_.map(markers, 'lng')) }; @@ -68,7 +78,13 @@ export const MapComponent = (props) => { } } if (_.isEmpty(markers)) { - if (!_.isEmpty(allMarkers)) { + if (!_.isEmpty(selectedCluster)) { + setCenter({ + lat: selectedCluster.lat, + lng: selectedCluster.lng, + }); + setMapZoom(18); + } else if (!_.isEmpty(allMarkers)) { const allMarkerItems = [].concat(...allMarkers); const countries = allMarkerItems.map((item) => item && item.country); const uniqueCountries = [...new Set(countries)]; @@ -116,7 +132,6 @@ export const MapComponent = (props) => { setCenter({ lat, lng }); }, (error) => { - // eslint-disable-next-line no-console console.error(error); }, ); @@ -147,36 +162,30 @@ export const MapComponent = (props) => { const overlapCounts = groupMarkersByLocation(_.flatten(allMarkers)); + if (!isLoaded) return
Loading Map...
; + return ( - - ); -}; - -const RenderedMap = withScriptjs( - withGoogleMap((props) => ( - - {!props.isMarkerShown && props.allMarkers && !_.isEmpty(props.allMarkers) - && _.map(props.allMarkers, (shipMarkers, idx) => ( + zoom={mapZoom} + > + {!isMarkerShown && allMarkers && !_.isEmpty(allMarkers) + && _.map(allMarkers, (shipMarkers, idx) => ( { - props.clusterClick(!_.isEmpty(shipMarkers) && _.first(shipMarkers).shipment, true); + setSelectedCluster(!_.isEmpty(shipMarkers) && _.first(shipMarkers)); + setCenter({ + lat: !_.isEmpty(shipMarkers) && _.first(shipMarkers).lat, + lng: !_.isEmpty(shipMarkers) && _.first(shipMarkers).lng, + }); + setMapZoom(18); }} styles={[ { @@ -216,13 +225,18 @@ const RenderedMap = withScriptjs( }, ]} > - {_.map(shipMarkers, (marker, inx) => ( - + {(clusterer) => _.map(shipMarkers, (marker, inx) => ( + ))} ))} - {props.isMarkerShown && props.markers && _.map( - props.markers, + {isMarkerShown && markers && _.map( + markers, (mark, index) => (mark.label ? ( props.onMarkerSelect(mark)} + onClick={() => onMarkerSelect(mark)} > - {_.isEqual(props.showInfoIndex, mark) && ( - props.onMarkerSelect(null)}> + {_.isEqual(showInfoIndex, mark) && ( + onMarkerSelect(null)}> {_.isEqual(mark.label, 'Clustered') ? ( - {_.map(MARKER_DATA(props.unitOfMeasure), (item, idx) => ( + {_.map(MARKER_DATA(unitOfMeasure), (item, idx) => ( ))} - + -
{mark.date}
+
{mark.date}
-
{mark.time}
+
{mark.time}
{mark.battery && _.gte(_.toNumber(mark.battery), 90) && ( - + )} {mark.battery && _.lt(_.toNumber(mark.battery), 90) && _.gte(_.toNumber(mark.battery), 60) && ( - + )} {mark.battery && _.lt(_.toNumber(mark.battery), 60) && ( - + )} {!mark.battery && ( @@ -307,7 +321,7 @@ const RenderedMap = withScriptjs(
) : ( -
+
{mark.label}
)} @@ -325,30 +339,30 @@ const RenderedMap = withScriptjs( position={ mark.lat && mark.lng ? { lat: mark.lat, lng: mark.lng } - : props.center + : center } onDragEnd={(e) => { - props.onMarkerDrag(e, mark.onMarkerDrag); + onMarkerDrag(e, mark.onMarkerDrag); }} /> )), )} - {props.isMarkerShown && !_.isEmpty(props.markers) && props.showPath && ( + {isMarkerShown && markers && !_.isEmpty(markers) && showPath && ( ({ + path={_.map(markers, (marker) => ({ lat: marker.lat, lng: marker.lng, }))} geodesic options={{ - strokeColor: props.theme.palette.background.dark, + strokeColor: theme.palette.background.dark, strokeOpacity: 0.75, strokeWeight: 1, }} /> )} - {props.isMarkerShown && props.markers && !_.isEmpty(props.polygon) && _.map( - props.markers, + {isMarkerShown && markers && !_.isEmpty(polygon) && _.map( + markers, (mark, index) => (mark.radius ? ( -
+
{`Geofence of ${mark.radius} ${_.toLower( - _.find(props.unitOfMeasure, (unit) => (_.toLower(unit.unit_of_measure_for) === 'distance')) - ? _.find(props.unitOfMeasure, (unit) => (_.toLower(unit.unit_of_measure_for) === 'distance')).unit_of_measure + _.find(unitOfMeasure, (unit) => (_.toLower(unit.unit_of_measure_for) === 'distance')) + ? _.find(unitOfMeasure, (unit) => (_.toLower(unit.unit_of_measure_for) === 'distance')).unit_of_measure : '', )}`}
@@ -396,33 +410,33 @@ const RenderedMap = withScriptjs( position={ mark.lat && mark.lng ? { lat: mark.lat, lng: mark.lng } - : props.center + : center } > -
+
Configure radius for geofence
)), )} - {!_.isEmpty(props.polygon) && ( + {!_.isEmpty(polygon) && ( )} - )), -); + ); +}; export default MapComponent; diff --git a/src/index.html b/src/index.html index f3c877dab..e4d9ed843 100644 --- a/src/index.html +++ b/src/index.html @@ -4,6 +4,9 @@ + + + { const location = useLocation(); const theme = useTheme(); const organization = getUser().organization.organization_uuid; + const { options: tzOptions } = useTimezoneSelect({ labelStyle: 'original', timezones: allTimezones }); const [locShipmentID, setLocShipmentID] = useState(''); const [shipmentFilter, setShipmentFilter] = useState('Active'); @@ -250,8 +254,8 @@ const Reporting = () => { }, [selectedShipment, markers, allGraphs, reports]); const getShipmentValue = (value) => { - let returnValue; - if (selectedShipment[value] !== null) { + let returnValue = ''; + if (!_.isEqual(selectedShipment[value], null)) { if (moment(selectedShipment[value], true).isValid()) { returnValue = moment(selectedShipment[value]) .tz(timeZone).format(`${dateFormat} ${timeFormat}`); @@ -265,7 +269,7 @@ const Reporting = () => { } } } else { - returnValue = 'NA'; + returnValue = 'N/A'; } return returnValue; }; @@ -302,7 +306,8 @@ const Reporting = () => { if (col.label === 'Date Time') { const timeArray = _.split(timeFormat, ' '); const timePeriod = _.size(timeArray) === 1 ? '24-hour' : '12-hour'; - const formattedLabel = `${col.label} (${getTimezone(new Date(), timeZone)}) (${dateFormat} ${timePeriod})`; + const filteredTimeZone = _.find(tzOptions, (option) => option.value === timeZone); + const formattedLabel = `${col.label} (${filteredTimeZone.abbrev}) (${dateFormat} ${timePeriod})`; return escapeCSV(formattedLabel); } if (col.name === 'temperature') { @@ -315,24 +320,31 @@ const Reporting = () => { }).join(','); const dateTimeColumnIndex = columns.findIndex((col) => col.label === 'Date Time'); - const departureTime = moment(selectedShipment.actual_time_of_departure).unix(); - const arrivalTime = moment(selectedShipment.actual_time_of_arrival).unix(); + const departureTime = moment(selectedShipment.actual_time_of_departure).tz(timeZone).format(`${dateFormat} ${timeFormat}`); + const arrivalTime = moment(selectedShipment.actual_time_of_arrival).isValid() + ? moment(selectedShipment.actual_time_of_arrival).tz(timeZone).format(`${dateFormat} ${timeFormat}`) + : null; + const formattedDepartureTime = moment(departureTime).unix(); + const formattedArrivalTime = arrivalTime ? moment(arrivalTime).unix() : null; const rowsWithinTimeRange = rows.filter((row) => { const dateTimeValue = moment(row[columns[dateTimeColumnIndex].name]).unix(); - return dateTimeValue >= departureTime && dateTimeValue <= arrivalTime; + if (formattedArrivalTime) { + return dateTimeValue >= formattedDepartureTime && dateTimeValue <= formattedArrivalTime; + } else { + return dateTimeValue >= formattedDepartureTime; + } }); if (_.size(rowsWithinTimeRange) > 0) { const firstRowIndex = rows.findIndex((row) => row === rowsWithinTimeRange[0]); const lastRowIndex = rows.findIndex((row) => row === rowsWithinTimeRange[_.size(rowsWithinTimeRange) - 1]); - rows[firstRowIndex].allAlerts.push({ title: 'Arrived', color: '#000' }); + if (formattedArrivalTime) { + rows[firstRowIndex].allAlerts.push({ title: 'Arrived', color: '#000' }); + } rows[lastRowIndex].allAlerts.push({ title: 'En route', color: '#000' }); } const csvBody = rows.map((row) => columns.map((col, colIndex) => { let cell = row[col.name]; - if (col.label === 'Date Time' && !_.isEmpty(cell)) { - return escapeCSV(moment(cell).tz(timeZone).format(`${dateFormat} ${timeFormat}`)); - } if (!row.location || row.location === 'Error retrieving address') { row.location = 'N/A'; } @@ -554,7 +566,8 @@ const Reporting = () => { if (col.label === 'Date Time') { const timeArray = _.split(timeFormat, ' '); const timePeriod = _.size(timeArray) === 1 ? '24-hour' : '12-hour'; - const formattedLabel = `Date Time (${getTimezone(new Date(), timeZone)}) (${dateFormat} ${timePeriod})`; + const filteredTimeZone = _.find(tzOptions, (option) => option.value === timeZone); + const formattedLabel = `Date Time (${filteredTimeZone.abbrev}) (${dateFormat} ${timePeriod})`; return formattedLabel; } if (col.name === 'battery') { @@ -579,8 +592,12 @@ const Reporting = () => { }); const dateTimeColIndex = columns.findIndex((col) => col.label === 'Date Time') + 1; - const departureTime = moment(selectedShipment.actual_time_of_departure).unix(); - const arrivalTime = moment(selectedShipment.actual_time_of_arrival).unix(); + const departureTime = moment(selectedShipment.actual_time_of_departure).tz(timeZone).format(`${dateFormat} ${timeFormat}`); + const arrivalTime = moment(selectedShipment.actual_time_of_arrival).isValid() + ? moment(selectedShipment.actual_time_of_arrival).tz(timeZone).format(`${dateFormat} ${timeFormat}`) + : null; + const formattedDepartureTime = moment(departureTime).unix(); + const formattedArrivalTime = arrivalTime ? moment(arrivalTime).unix() : null; const greyRows = []; rows.forEach((row, rowIndex) => { @@ -733,17 +750,32 @@ const Reporting = () => { }; const dateValue = moment(row[columns[dateTimeColIndex - 1].name]).unix(); - if (dateValue >= departureTime && dateValue <= arrivalTime) { - rowRef.eachCell((cell, colNumber) => { - if (!cell.fill) { - cell.fill = { - type: 'pattern', - pattern: 'solid', - fgColor: { argb: theme.palette.background.light6.replace('#', '') }, - }; - } - }); - greyRows.push(rowRef.number); + if (formattedArrivalTime) { + if (dateValue >= formattedDepartureTime && dateValue <= formattedArrivalTime) { + rowRef.eachCell((cell, colNumber) => { + if (!cell.fill) { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: theme.palette.background.light6.replace('#', '') }, + }; + } + }); + greyRows.push(rowRef.number); + } + } else { + if (dateValue >= formattedDepartureTime) { + rowRef.eachCell((cell, colNumber) => { + if (!cell.fill) { + cell.fill = { + type: 'pattern', + pattern: 'solid', + fgColor: { argb: theme.palette.background.light6.replace('#', '') }, + }; + } + }); + greyRows.push(rowRef.number); + } } }); @@ -754,18 +786,20 @@ const Reporting = () => { let firstGreyRowRichText = firstGreyRow.getCell(1).value.richText; let lastGreyRowRichText = lastGreyRow.getCell(1).value.richText; - if (_.size(firstGreyRowRichText) > 0) { - firstGreyRowRichText = [ - ...firstGreyRowRichText, - { - text: ', Arrived', - font: { color: { argb: theme.palette.background.black2.replace('#', '') } }, - }, - ]; - } else { - firstGreyRowRichText = [ - { text: 'Arrived', font: { color: { argb: theme.palette.background.black2.replace('#', '') } } }, - ]; + if (formattedArrivalTime) { + if (_.size(firstGreyRowRichText) > 0) { + firstGreyRowRichText = [ + ...firstGreyRowRichText, + { + text: ', Arrived', + font: { color: { argb: theme.palette.background.black2.replace('#', '') } }, + }, + ]; + } else { + firstGreyRowRichText = [ + { text: 'Arrived', font: { color: { argb: theme.palette.background.black2.replace('#', '') } } }, + ]; + } } if (_.size(lastGreyRowRichText) > 0) { @@ -1002,12 +1036,9 @@ const Reporting = () => { screenshotMapCenter noInitialInfo markers={markers} - googleMapURL={window.env.MAP_API_URL} zoom={4} setSelectedMarker={setSelectedMarker} - loadingElement={
} - containerElement={
} - mapElement={
} + containerStyle={{ height: '625px' }} unitOfMeasure={unitData} /> @@ -1072,10 +1103,6 @@ const Reporting = () => { - } - containerElement={( -
- )} - mapElement={ -
- } + containerStyle={{ + height: '200px', + marginTop: isMobile() ? '10px' : '30px', + marginBottom: '14px', + }} markers={[ { lat: last_known_location diff --git a/src/pages/Shipment/CreateShipment.js b/src/pages/Shipment/CreateShipment.js index 4a8c8afd2..e1c1f26ae 100644 --- a/src/pages/Shipment/CreateShipment.js +++ b/src/pages/Shipment/CreateShipment.js @@ -1181,11 +1181,8 @@ const CreateShipment = ({ history, location }) => { } - containerElement={
} - mapElement={
} + containerStyle={{ height: '300px', marginTop: '10px' }} markers={[ { lat: startingLocation && _.includes(startingLocation, ',') && parseFloat(startingLocation.split(',')[0]), @@ -1255,11 +1252,8 @@ const CreateShipment = ({ history, location }) => { } - containerElement={
} - mapElement={
} + containerStyle={{ height: '300px', marginTop: '10px' }} markers={[ { lat: endingLocation && _.includes(endingLocation, ',') && parseFloat(endingLocation.split(',')[0]), diff --git a/src/pages/Shipment/Shipment.js b/src/pages/Shipment/Shipment.js index 3cec70ae2..962295071 100644 --- a/src/pages/Shipment/Shipment.js +++ b/src/pages/Shipment/Shipment.js @@ -1,3 +1,4 @@ +/* eslint-disable array-callback-return */ /* eslint-disable no-nested-ternary */ import React, { useEffect, useState } from 'react'; import _ from 'lodash'; @@ -41,6 +42,7 @@ import useAlert from '@hooks/useAlert'; import { useStore } from '@zustand/timezone/timezoneStore'; import './ShipmentStyles.css'; import { TIVE_GATEWAY_TIMES } from '@utils/mock'; +import { calculateLatLngBounds } from '@utils/utilMethods'; const Shipment = ({ history }) => { const muiTheme = useTheme(); @@ -62,6 +64,8 @@ const Shipment = ({ history }) => { const [expandedRows, setExpandedRows] = useState([]); const [steps, setSteps] = useState([]); const [isLoading, setLoading] = useState(false); + const [selectedCluster, setSelectedCluster] = useState({}); + const [zoom, setZoom] = useState(4); const { data: shipmentData, isLoading: isLoadingShipments, isFetching: isFetchingShipments } = useQuery( ['shipments', shipmentFilter, organization], @@ -153,11 +157,21 @@ const Shipment = ({ history }) => { sensorReportData, ); const filteredRows = _.filter(formattedRows, { type: shipmentFilter }); - setRows(filteredRows); + if (_.isEmpty(selectedCluster)) { + setRows(filteredRows); + } setAllMarkers(_.map(filteredRows, 'allMarkers')); }, [shipmentFilter, shipmentData, custodianData, custodyData, itemData, allGatewayData, sensorAlertData, sensorReportData]); + useEffect(() => { + if (!_.isEmpty(markers) || !_.isEmpty(selectedCluster)) { + setZoom(12); + } else { + setZoom(4); + } + }, [markers, selectedCluster]); + useEffect(() => { if (selectedShipment) { processMarkers(selectedShipment, true); @@ -176,6 +190,22 @@ const Shipment = ({ history }) => { } }, [selectedShipment, expandedRows]); + useEffect(() => { + if (!_.isEmpty(selectedCluster) && !_.isEmpty(rows)) { + const { lat, lng } = selectedCluster; + const { radius } = user.organization; + const values = calculateLatLngBounds(lat, lng, radius); + const filteredRows = rows.filter((obj) => !_.isEmpty(obj.allMarkers)); + const clusterFilteredRows = filteredRows.filter((obj) => { + const firstMarker = _.first(obj.allMarkers); + const isLatInRange = firstMarker.lat >= values.minLat && firstMarker.lat <= values.maxLat; + const isLngInRange = firstMarker.lng >= values.minLng && firstMarker.lng <= values.maxLng; + return isLatInRange && isLngInRange; + }); + setRows(clusterFilteredRows); + } + }, [selectedCluster]); + const processMarkers = (shipment, setExpanded = false) => { const dateFormat = !_.isEmpty(unitData) && _.find(unitData, (unit) => (_.toLower(unit.unit_of_measure_for) === 'date')).unit_of_measure; const timeFormat = !_.isEmpty(unitData) && _.find(unitData, (unit) => (_.toLower(unit.unit_of_measure_for) === 'time')).unit_of_measure; @@ -537,6 +567,7 @@ const Shipment = ({ history }) => { setSelectedMarker({}); setExpandedRows([]); setSteps([]); + setSelectedCluster({}); }; const renderSensorData = (marker) => { @@ -673,6 +704,36 @@ const Shipment = ({ history }) => { + {!_.isEmpty(selectedCluster) && ( + + )}
@@ -687,20 +748,12 @@ const Shipment = ({ history }) => { isMarkerShown={!_.isEmpty(markers)} showPath markers={markers} - googleMapURL={window.env.MAP_API_URL} - zoom={_.isEmpty(markers) ? 2.5 : 12} + zoom={zoom} setSelectedMarker={setSelectedMarker} - loadingElement={ -
- } - containerElement={ -
- } - mapElement={ -
- } - clusterClick={processMarkers} + containerStyle={{ height: '600px' }} unitOfMeasure={unitData} + setSelectedCluster={setSelectedCluster} + selectedCluster={selectedCluster} /> diff --git a/src/pages/Shipment/ShipmentStyles.css b/src/pages/Shipment/ShipmentStyles.css index da6aa0b70..a1fc7b283 100644 --- a/src/pages/Shipment/ShipmentStyles.css +++ b/src/pages/Shipment/ShipmentStyles.css @@ -27,6 +27,17 @@ background-color: var(--color-palette-primary-main); } +.shipmentGoBackButton { + margin-bottom: 24px; + margin-left: 16px; + background-color: var(--color-palette-primary-main); + color: var(--color-palette-background-default); +} + +.shipmentGoBackButton:hover { + background-color: var(--color-palette-primary-main); +} + .shipmentTab { color: var(--color-palette-background-default); } diff --git a/src/utils/utilMethods.js b/src/utils/utilMethods.js index da90c6e21..3ea82aa1e 100644 --- a/src/utils/utilMethods.js +++ b/src/utils/utilMethods.js @@ -93,3 +93,18 @@ export const dateDifference = (initialDate, finalDate) => { const dateString = `${days} days, ${hours} hrs., ${minutes} min.`; return dateString; }; + +export const calculateLatLngBounds = (lat, lng, miles) => { + const milesToLatDegree = miles / 69; + const maxLat = lat + milesToLatDegree; + const minLat = lat - milesToLatDegree; + const milesToLngDegree = miles / (69 * Math.cos(lat * (Math.PI / 180))); + const maxLng = lng + milesToLngDegree; + const minLng = lng - milesToLngDegree; + return { + maxLat, + minLat, + maxLng, + minLng, + }; +};