build ok,
This commit is contained in:
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Card from '@mui/material/Card';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { WarningCircle as WarningCircleIcon } from '@phosphor-icons/react/dist/ssr/WarningCircle';
|
||||
|
||||
export interface DeviatedVehiclesProps {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function DeviatedVehicles({ amount }: DeviatedVehiclesProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Stack spacing={1} sx={{ p: 3 }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-shadows-8)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
<WarningCircleIcon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Avatar>
|
||||
<Typography variant="h5">{amount}</Typography>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Vehicles deviated from route
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
140
002_source/cms/src/components/dashboard/logistics/fleet-map.tsx
Normal file
140
002_source/cms/src/components/dashboard/logistics/fleet-map.tsx
Normal file
@@ -0,0 +1,140 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import type { FlyToOptions } from 'mapbox-gl';
|
||||
import type { MapRef, ViewState } from 'react-map-gl';
|
||||
import Mapbox, { Marker } from 'react-map-gl';
|
||||
|
||||
import { config } from '@/config';
|
||||
import { useSettings } from '@/hooks/use-settings';
|
||||
|
||||
import type { Vehicle } from './types';
|
||||
|
||||
// Map default view state
|
||||
const VIEW_STATE: Pick<ViewState, 'latitude' | 'longitude' | 'zoom'> = {
|
||||
latitude: 40.74281576586265,
|
||||
longitude: -73.99277240443942,
|
||||
zoom: 11,
|
||||
};
|
||||
|
||||
export interface FleetMapProps {
|
||||
currentVehicleId?: string;
|
||||
onVehicleSelect?: (vehicleId: string) => void;
|
||||
vehicles?: Vehicle[];
|
||||
}
|
||||
|
||||
export function FleetMap({ onVehicleSelect, currentVehicleId, vehicles = [] }: FleetMapProps): React.JSX.Element {
|
||||
const {
|
||||
settings: { colorScheme = 'light' },
|
||||
} = useSettings();
|
||||
|
||||
const mapRef = React.useRef<MapRef | null>(null);
|
||||
|
||||
const [viewState] = React.useState(() => {
|
||||
if (!currentVehicleId) {
|
||||
return VIEW_STATE;
|
||||
}
|
||||
|
||||
const currentVehicle = vehicles.find((vehicle) => vehicle.id === currentVehicleId);
|
||||
|
||||
if (!currentVehicle) {
|
||||
return VIEW_STATE;
|
||||
}
|
||||
|
||||
return { latitude: currentVehicle.latitude, longitude: currentVehicle.longitude, zoom: 13 };
|
||||
});
|
||||
|
||||
const handleRecenter = React.useCallback(() => {
|
||||
const map = mapRef.current;
|
||||
|
||||
if (!map) {
|
||||
return;
|
||||
}
|
||||
|
||||
let flyOptions: FlyToOptions;
|
||||
|
||||
const currentVehicle = vehicles.find((vehicle) => vehicle.id === currentVehicleId);
|
||||
|
||||
if (!currentVehicle) {
|
||||
flyOptions = { center: [VIEW_STATE.longitude, VIEW_STATE.latitude] };
|
||||
} else {
|
||||
flyOptions = { center: [currentVehicle.longitude, currentVehicle.latitude] };
|
||||
}
|
||||
|
||||
map.flyTo(flyOptions);
|
||||
}, [vehicles, currentVehicleId]);
|
||||
|
||||
// Recenter if vehicles or current vehicle change
|
||||
React.useEffect(() => {
|
||||
handleRecenter();
|
||||
// eslint-disable-next-line react-hooks/exhaustive-deps -- Expected
|
||||
}, [vehicles, currentVehicleId]);
|
||||
|
||||
const mapStyle = colorScheme === 'dark' ? 'mapbox://styles/mapbox/dark-v9' : 'mapbox://styles/mapbox/light-v9';
|
||||
|
||||
if (!config.mapbox.apiKey) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
alignItems: 'center',
|
||||
display: 'flex',
|
||||
flex: '1 1 auto',
|
||||
flexDirection: 'column',
|
||||
justifyContent: 'center',
|
||||
overflowY: 'auto',
|
||||
p: 3,
|
||||
}}
|
||||
>
|
||||
<Stack spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Box component="img" src="/assets/error.svg" sx={{ height: 'auto', maxWidth: '100%', width: '120px' }} />
|
||||
<Stack spacing={1}>
|
||||
<Typography sx={{ textAlign: 'center' }} variant="h5">
|
||||
Map cannot be loaded
|
||||
</Typography>
|
||||
<Typography color="text.secondary" sx={{ textAlign: 'center' }} variant="subtitle2">
|
||||
Mapbox API Key is not configured.
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Mapbox
|
||||
attributionControl={false}
|
||||
initialViewState={viewState}
|
||||
mapStyle={mapStyle}
|
||||
mapboxAccessToken={config.mapbox.apiKey}
|
||||
maxZoom={20}
|
||||
minZoom={11}
|
||||
ref={mapRef}
|
||||
>
|
||||
{vehicles.map((vehicle) => (
|
||||
<Marker
|
||||
key={vehicle.id}
|
||||
latitude={vehicle.latitude}
|
||||
longitude={vehicle.longitude}
|
||||
onClick={() => {
|
||||
onVehicleSelect?.(vehicle.id);
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{
|
||||
height: '40px',
|
||||
width: '40px',
|
||||
...(vehicle.id === currentVehicleId && {
|
||||
filter: 'drop-shadow(0px 0px 4px var(--mui-palette-primary-main))',
|
||||
}),
|
||||
}}
|
||||
>
|
||||
<Box alt="Marker" component="img" src="/assets/marker-truck.png" sx={{ height: '100%' }} />
|
||||
</Box>
|
||||
</Marker>
|
||||
))}
|
||||
</Mapbox>
|
||||
);
|
||||
}
|
@@ -0,0 +1,131 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Timeline from '@mui/lab/Timeline';
|
||||
import TimelineConnector from '@mui/lab/TimelineConnector';
|
||||
import TimelineContent from '@mui/lab/TimelineContent';
|
||||
import TimelineDot from '@mui/lab/TimelineDot';
|
||||
import TimelineItem from '@mui/lab/TimelineItem';
|
||||
import TimelineSeparator from '@mui/lab/TimelineSeparator';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Collapse from '@mui/material/Collapse';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Truck as TruckIcon } from '@phosphor-icons/react/dist/ssr/Truck';
|
||||
|
||||
import { dayjs } from '@/lib/dayjs';
|
||||
|
||||
import type { Vehicle } from './types';
|
||||
|
||||
export interface FleetVehicleProps {
|
||||
onDeselect?: () => void;
|
||||
onSelect?: (vehicleId: string) => void;
|
||||
selected?: boolean;
|
||||
vehicle: Vehicle;
|
||||
}
|
||||
|
||||
export function FleetVehicle({ onDeselect, onSelect, selected, vehicle }: FleetVehicleProps): React.JSX.Element {
|
||||
const handleToggle = React.useCallback(() => {
|
||||
if (!selected) {
|
||||
onSelect?.(vehicle.id);
|
||||
} else {
|
||||
onDeselect?.();
|
||||
}
|
||||
}, [onDeselect, onSelect, selected, vehicle]);
|
||||
|
||||
return (
|
||||
<Stack component="li">
|
||||
<Stack
|
||||
direction="row"
|
||||
onClick={handleToggle}
|
||||
onKeyUp={(event) => {
|
||||
if (event.key === 'Enter' || event.key === ' ') {
|
||||
handleToggle();
|
||||
}
|
||||
}}
|
||||
role="button"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', cursor: 'pointer', dispaly: 'flex', p: 2, textAlign: 'left', width: '100%' }}
|
||||
tabIndex={0}
|
||||
>
|
||||
<Avatar>
|
||||
<TruckIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
<div>
|
||||
<Typography variant="subtitle1">{vehicle.id}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{vehicle.location}
|
||||
</Typography>
|
||||
</div>
|
||||
</Stack>
|
||||
<Collapse in={selected}>
|
||||
<Divider />
|
||||
<Box sx={{ p: 2 }}>
|
||||
<Stack spacing={1}>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
Temperature (good)
|
||||
</Typography>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<LinearProgress sx={{ flex: '1 1 auto' }} value={vehicle.temperature} variant="determinate" />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{vehicle.temperature}°C
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
<Timeline sx={{ px: 3, '& .MuiTimelineItem-root:before': { flex: 0, p: 0 } }}>
|
||||
{vehicle.arrivedAt ? (
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot color="primary" />
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent>
|
||||
<div>
|
||||
<Typography variant="body2">Arrived</Typography>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{dayjs(vehicle.arrivedAt).format('MMM D, YYYY h:mm A')}
|
||||
</Typography>
|
||||
</div>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
) : null}
|
||||
{vehicle.departedAt ? (
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot color="primary" />
|
||||
<TimelineConnector />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent>
|
||||
<div>
|
||||
<Typography variant="body2">Out for delivery</Typography>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{dayjs(vehicle.departedAt).format('MMM D, YYYY h:mm A')}
|
||||
</Typography>
|
||||
</div>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
) : null}
|
||||
{vehicle.startedAt ? (
|
||||
<TimelineItem>
|
||||
<TimelineSeparator>
|
||||
<TimelineDot color="primary" />
|
||||
</TimelineSeparator>
|
||||
<TimelineContent>
|
||||
<div>
|
||||
<Typography variant="body2">Tracking number created</Typography>
|
||||
<Typography color="text.secondary" variant="caption">
|
||||
{dayjs(vehicle.startedAt).format('MMM D, YYYY h:mm A')}
|
||||
</Typography>
|
||||
</div>
|
||||
</TimelineContent>
|
||||
</TimelineItem>
|
||||
) : null}
|
||||
</Timeline>
|
||||
</Box>
|
||||
</Collapse>
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Stack from '@mui/material/Stack';
|
||||
|
||||
import { FleetVehicle } from './fleet-vehicle';
|
||||
import type { Vehicle } from './types';
|
||||
|
||||
export interface FleetVehiclesProps {
|
||||
currentVehicleId?: string;
|
||||
onVehicleDeselect?: () => void;
|
||||
onVehicleSelect?: (vehicleId: string) => void;
|
||||
vehicles?: Vehicle[];
|
||||
}
|
||||
|
||||
export function FleetVehicles({
|
||||
onVehicleDeselect,
|
||||
onVehicleSelect,
|
||||
currentVehicleId,
|
||||
vehicles = [],
|
||||
}: FleetVehiclesProps): React.JSX.Element {
|
||||
return (
|
||||
<Stack
|
||||
component="ul"
|
||||
divider={<Divider />}
|
||||
sx={{ borderBottom: '1px solid var(--mui-palette-divider)', listStyle: 'none', m: 0, p: 0 }}
|
||||
>
|
||||
{vehicles.map((vehicle) => {
|
||||
const selected = currentVehicleId ? currentVehicleId === vehicle.id : false;
|
||||
|
||||
return (
|
||||
<FleetVehicle
|
||||
key={vehicle.id}
|
||||
onDeselect={onVehicleDeselect}
|
||||
onSelect={onVehicleSelect}
|
||||
selected={selected}
|
||||
vehicle={vehicle}
|
||||
/>
|
||||
);
|
||||
})}
|
||||
</Stack>
|
||||
);
|
||||
}
|
@@ -0,0 +1,65 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import { DotsThree as DotsThreeIcon } from '@phosphor-icons/react/dist/ssr/DotsThree';
|
||||
import { List as ListIcon } from '@phosphor-icons/react/dist/ssr/List';
|
||||
|
||||
import { FleetMap } from './fleet-map';
|
||||
import { Sidebar } from './sidebar';
|
||||
import type { Vehicle } from './types';
|
||||
|
||||
export interface FleetViewProps {
|
||||
vehicles: Vehicle[];
|
||||
}
|
||||
|
||||
export function FleetView({ vehicles }: FleetViewProps): React.JSX.Element {
|
||||
const [openSidebar, setOpenSidebar] = React.useState<boolean>(false);
|
||||
const [currentVehicleId, setCurrentVehicleId] = React.useState<string | undefined>(vehicles[0]?.id);
|
||||
|
||||
const handleVehicleSelect = React.useCallback((vehicleId: string) => {
|
||||
setCurrentVehicleId(vehicleId);
|
||||
}, []);
|
||||
|
||||
const handleVehicleDeselect = React.useCallback(() => {
|
||||
setCurrentVehicleId(undefined);
|
||||
}, []);
|
||||
|
||||
const handleSidebarOpen = React.useCallback(() => {
|
||||
setOpenSidebar(true);
|
||||
}, []);
|
||||
|
||||
const handleSidebarClose = React.useCallback(() => {
|
||||
setOpenSidebar(false);
|
||||
}, []);
|
||||
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flex: '1 1 0', minHeight: 0 }}>
|
||||
<Sidebar
|
||||
currentVehicleId={currentVehicleId}
|
||||
onClose={handleSidebarClose}
|
||||
onVehicleDeselect={handleVehicleDeselect}
|
||||
onVehicleSelect={handleVehicleSelect}
|
||||
open={openSidebar}
|
||||
vehicles={vehicles}
|
||||
/>
|
||||
<Box sx={{ display: 'flex', flex: '1 1 auto', flexDirection: 'column', overflow: 'hidden' }}>
|
||||
<Box sx={{ borderBottom: '1px solid var(--mui-palette-divider)', display: 'flex', flex: '0 0 auto', p: 2 }}>
|
||||
<Stack direction="row" spacing={1} sx={{ flex: '1 1 auto' }}>
|
||||
<IconButton onClick={handleSidebarOpen} sx={{ display: { md: 'none' } }}>
|
||||
<ListIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Stack direction="row" spacing={1}>
|
||||
<IconButton>
|
||||
<DotsThreeIcon weight="bold" />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
</Box>
|
||||
<FleetMap currentVehicleId={currentVehicleId} onVehicleSelect={handleVehicleSelect} vehicles={vehicles} />
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Card from '@mui/material/Card';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Clock as ClockIcon } from '@phosphor-icons/react/dist/ssr/Clock';
|
||||
|
||||
export interface LateVehiclesProps {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function LateVehicles({ amount }: LateVehiclesProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Stack spacing={1} sx={{ p: 3 }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-shadows-8)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
<ClockIcon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Avatar>
|
||||
<Typography variant="h5">{amount}</Typography>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Late vehicles
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,42 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
|
||||
export interface OnRouteVehiclesProps {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function OnRouteVehicles({ amount }: OnRouteVehiclesProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Stack spacing={1} sx={{ p: 3 }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Avatar sx={{ bgcolor: 'transparent' }}>
|
||||
<Box
|
||||
sx={{
|
||||
animation: 'pulse ease 750ms infinite',
|
||||
borderRadius: '50%',
|
||||
p: '4px',
|
||||
'@keyframes pulse': {
|
||||
'0%': { boxShadow: 'none' },
|
||||
'100%': { boxShadow: '0px 0px 0px 6px rgba(240, 68, 56, 0.1)' },
|
||||
},
|
||||
}}
|
||||
>
|
||||
<Box
|
||||
sx={{ bgcolor: 'var(--mui-palette-error-main)', borderRadius: '50%', height: '18px', width: '18px' }}
|
||||
/>
|
||||
</Box>
|
||||
</Avatar>
|
||||
<Typography variant="h5">{amount}</Typography>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
On route vehicles
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
109
002_source/cms/src/components/dashboard/logistics/sidebar.tsx
Normal file
109
002_source/cms/src/components/dashboard/logistics/sidebar.tsx
Normal file
@@ -0,0 +1,109 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Box from '@mui/material/Box';
|
||||
import Button from '@mui/material/Button';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import Drawer from '@mui/material/Drawer';
|
||||
import IconButton from '@mui/material/IconButton';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Plus as PlusIcon } from '@phosphor-icons/react/dist/ssr/Plus';
|
||||
import { X as XIcon } from '@phosphor-icons/react/dist/ssr/X';
|
||||
|
||||
import { useMediaQuery } from '@/hooks/use-media-query';
|
||||
|
||||
import { FleetVehicles } from './fleet-vehicles';
|
||||
import type { Vehicle } from './types';
|
||||
|
||||
export interface SidebarProps {
|
||||
currentVehicleId?: string;
|
||||
onClose?: () => void;
|
||||
onVehicleDeselect?: () => void;
|
||||
onVehicleSelect?: (vehicleId: string) => void;
|
||||
open?: boolean;
|
||||
vehicles: Vehicle[];
|
||||
}
|
||||
|
||||
export function Sidebar({
|
||||
currentVehicleId,
|
||||
onClose,
|
||||
onVehicleDeselect,
|
||||
onVehicleSelect,
|
||||
open,
|
||||
vehicles,
|
||||
}: SidebarProps): React.JSX.Element {
|
||||
const mdUp = useMediaQuery('up', 'md');
|
||||
|
||||
const content = (
|
||||
<SidebarContent
|
||||
currentVehicleId={currentVehicleId}
|
||||
onClose={onClose}
|
||||
onVehicleDeselect={onVehicleDeselect}
|
||||
onVehicleSelect={onVehicleSelect}
|
||||
vehicles={vehicles}
|
||||
/>
|
||||
);
|
||||
|
||||
if (mdUp) {
|
||||
return (
|
||||
<Box
|
||||
sx={{
|
||||
borderRight: '1px solid var(--mui-palette-divider)',
|
||||
display: { xs: 'none', md: 'block' },
|
||||
flex: '0 0 auto',
|
||||
width: '320px',
|
||||
}}
|
||||
>
|
||||
{content}
|
||||
</Box>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Drawer PaperProps={{ sx: { maxWidth: '100%', width: '320px' } }} onClose={onClose} open={open}>
|
||||
{content}
|
||||
</Drawer>
|
||||
);
|
||||
}
|
||||
|
||||
export interface SidebarContentProps {
|
||||
currentVehicleId?: string;
|
||||
onClose?: () => void;
|
||||
onVehicleDeselect?: () => void;
|
||||
onVehicleSelect?: (vehicleId: string) => void;
|
||||
vehicles: Vehicle[];
|
||||
}
|
||||
|
||||
function SidebarContent({
|
||||
currentVehicleId,
|
||||
onClose,
|
||||
onVehicleDeselect,
|
||||
onVehicleSelect,
|
||||
vehicles,
|
||||
}: SidebarContentProps): React.JSX.Element {
|
||||
return (
|
||||
<Box sx={{ display: 'flex', flexDirection: 'column', height: '100%' }}>
|
||||
<Stack spacing={1} sx={{ flex: '0 0 auto', p: 2 }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'flex-center', justifyContent: 'space-between' }}>
|
||||
<Typography variant="h5">Fleet</Typography>
|
||||
<IconButton onClick={onClose} sx={{ display: { md: 'none' } }}>
|
||||
<XIcon />
|
||||
</IconButton>
|
||||
</Stack>
|
||||
<Button startIcon={<PlusIcon />} variant="contained">
|
||||
Add vehicle
|
||||
</Button>
|
||||
</Stack>
|
||||
<Divider />
|
||||
<Box sx={{ flex: '1 1 auto', overflowY: 'auto' }}>
|
||||
<FleetVehicles
|
||||
currentVehicleId={currentVehicleId}
|
||||
onVehicleDeselect={onVehicleDeselect}
|
||||
onVehicleSelect={onVehicleSelect}
|
||||
vehicles={vehicles}
|
||||
/>
|
||||
</Box>
|
||||
</Box>
|
||||
);
|
||||
}
|
10
002_source/cms/src/components/dashboard/logistics/types.d.ts
vendored
Normal file
10
002_source/cms/src/components/dashboard/logistics/types.d.ts
vendored
Normal file
@@ -0,0 +1,10 @@
|
||||
export interface Vehicle {
|
||||
id: string;
|
||||
location: string;
|
||||
latitude: number;
|
||||
longitude: number;
|
||||
temperature: number;
|
||||
startedAt?: Date;
|
||||
departedAt?: Date;
|
||||
arrivedAt?: Date;
|
||||
}
|
@@ -0,0 +1,135 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardContent from '@mui/material/CardContent';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import Grid from '@mui/material/Unstable_Grid2';
|
||||
import { Wrench as WrenchIcon } from '@phosphor-icons/react/dist/ssr/Wrench';
|
||||
import { Cell, Pie, PieChart } from 'recharts';
|
||||
|
||||
import { NoSsr } from '@/components/core/no-ssr';
|
||||
|
||||
export interface VehiclesConditionProps {
|
||||
bad: number;
|
||||
excellent: number;
|
||||
good: number;
|
||||
}
|
||||
|
||||
export function VehiclesCondition({ bad, excellent, good }: VehiclesConditionProps): React.JSX.Element {
|
||||
const total = excellent + good + bad;
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<WrenchIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Vehicles condition"
|
||||
/>
|
||||
<CardContent>
|
||||
<Grid container spacing={3}>
|
||||
<Grid md={4} xs={12}>
|
||||
<VehicleCondition
|
||||
amount={excellent}
|
||||
color="var(--mui-palette-success-main)"
|
||||
description="No issues"
|
||||
title="Excellent"
|
||||
total={total}
|
||||
trackColor="var(--mui-palette-success-100)"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid md={4} xs={12}>
|
||||
<VehicleCondition
|
||||
amount={good}
|
||||
color="var(--mui-palette-warning-main)"
|
||||
description="Minor issues"
|
||||
title="Good"
|
||||
total={total}
|
||||
trackColor="var(--mui-palette-warning-100)"
|
||||
/>
|
||||
</Grid>
|
||||
<Grid md={4} xs={12}>
|
||||
<VehicleCondition
|
||||
amount={bad}
|
||||
color="var(--mui-palette-error-main)"
|
||||
description="Needs attention"
|
||||
title="Bad"
|
||||
total={total}
|
||||
trackColor="var(--mui-palette-error-100)"
|
||||
/>
|
||||
</Grid>
|
||||
</Grid>
|
||||
</CardContent>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
interface ConditionChartProps {
|
||||
amount: number;
|
||||
color: string;
|
||||
description: string;
|
||||
title: string;
|
||||
total: number;
|
||||
trackColor: string;
|
||||
}
|
||||
|
||||
function VehicleCondition({
|
||||
amount,
|
||||
color,
|
||||
description,
|
||||
title,
|
||||
total,
|
||||
trackColor,
|
||||
}: ConditionChartProps): React.JSX.Element {
|
||||
const chartSize = 100;
|
||||
const chartTickness = 12;
|
||||
|
||||
const progress = Math.round((amount / total) * 100);
|
||||
|
||||
const data = [
|
||||
{ name: title, value: progress, color },
|
||||
{ name: 'Empty', value: 100 - progress, color: trackColor },
|
||||
] satisfies { name: string; value: number; color: string }[];
|
||||
|
||||
return (
|
||||
<Card variant="outlined">
|
||||
<Stack spacing={3} sx={{ alignItems: 'center', px: 2, py: 3 }}>
|
||||
<Typography variant="h6">{title}</Typography>
|
||||
<NoSsr fallback={<Box sx={{ height: `${chartSize}px`, width: `${chartSize}px` }} />}>
|
||||
<PieChart height={chartSize} margin={{ top: 0, right: 0, bottom: 0, left: 0 }} width={chartSize}>
|
||||
<Pie
|
||||
animationDuration={300}
|
||||
cx={chartSize / 2}
|
||||
cy={chartSize / 2}
|
||||
data={data}
|
||||
dataKey="value"
|
||||
innerRadius={chartSize / 2 - chartTickness}
|
||||
nameKey="name"
|
||||
outerRadius={chartSize / 2}
|
||||
strokeWidth={0}
|
||||
>
|
||||
{data.map(
|
||||
(entry): React.JSX.Element => (
|
||||
<Cell fill={entry.color} key={entry.name} />
|
||||
)
|
||||
)}
|
||||
</Pie>
|
||||
</PieChart>
|
||||
</NoSsr>
|
||||
<Box sx={{ textAlign: 'center' }}>
|
||||
<Typography variant="h6">{amount}</Typography>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{description}
|
||||
</Typography>
|
||||
</Box>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,78 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import List from '@mui/material/List';
|
||||
import ListItem from '@mui/material/ListItem';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Bell as BellIcon } from '@phosphor-icons/react/dist/ssr/Bell';
|
||||
import { RadialBar, RadialBarChart } from 'recharts';
|
||||
|
||||
import { NoSsr } from '@/components/core/no-ssr';
|
||||
|
||||
export interface VehiclesOverviewProps {
|
||||
data: { name: string; value: number; fill: string }[];
|
||||
}
|
||||
|
||||
export function VehiclesOverview({ data }: VehiclesOverviewProps): React.JSX.Element {
|
||||
const chartSize = 260;
|
||||
const chartInnerRadius = 40;
|
||||
const dataWithEmpty = [{ name: 'Empty', value: 100 }, ...data];
|
||||
|
||||
const total = data.reduce((acc, cur) => acc + cur.value, 0);
|
||||
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<BellIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="Vehicles overview"
|
||||
/>
|
||||
<Stack direction="row" spacing={3} sx={{ alignItems: 'center', flexWrap: 'wrap', p: 3 }}>
|
||||
<Box sx={{ '& .recharts-layer path[name="Empty"]': { display: 'none' } }}>
|
||||
<NoSsr fallback={<Box sx={{ height: `${chartSize}px`, width: `${chartSize}px` }} />}>
|
||||
<RadialBarChart
|
||||
barSize={10}
|
||||
data={dataWithEmpty}
|
||||
endAngle={-360}
|
||||
height={chartSize}
|
||||
innerRadius={chartInnerRadius}
|
||||
startAngle={90}
|
||||
width={chartSize}
|
||||
>
|
||||
<RadialBar animationDuration={300} background cornerRadius={8} dataKey="value" />
|
||||
</RadialBarChart>
|
||||
</NoSsr>
|
||||
</Box>
|
||||
<Stack spacing={3} sx={{ flex: '1 1 auto' }}>
|
||||
<div>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Total
|
||||
</Typography>
|
||||
<Typography variant="h5">{total}</Typography>
|
||||
</div>
|
||||
<List disablePadding>
|
||||
{data.map((entry) => (
|
||||
<ListItem disableGutters key={entry.name}>
|
||||
<Stack direction="row" spacing={1} sx={{ alignItems: 'center', flex: '1 1 auto', minWidth: 0 }}>
|
||||
<Box sx={{ bgcolor: entry.fill, borderRadius: '2px', height: '4px', width: '16px' }} />
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
{entry.name}
|
||||
</Typography>
|
||||
</Stack>
|
||||
<Typography variant="subtitle2">{entry.value}</Typography>
|
||||
</ListItem>
|
||||
))}
|
||||
</List>
|
||||
</Stack>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,95 @@
|
||||
'use client';
|
||||
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Box from '@mui/material/Box';
|
||||
import Card from '@mui/material/Card';
|
||||
import CardHeader from '@mui/material/CardHeader';
|
||||
import Chip from '@mui/material/Chip';
|
||||
import Divider from '@mui/material/Divider';
|
||||
import LinearProgress from '@mui/material/LinearProgress';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Path as PathIcon } from '@phosphor-icons/react/dist/ssr/Path';
|
||||
import { Truck as TruckIcon } from '@phosphor-icons/react/dist/ssr/Truck';
|
||||
|
||||
import { DataTable } from '@/components/core/data-table';
|
||||
import type { ColumnDef } from '@/components/core/data-table';
|
||||
|
||||
export interface Vehicle {
|
||||
id: string;
|
||||
endingRoute: string;
|
||||
startingRoute: string;
|
||||
status: 'success' | 'error' | 'warning';
|
||||
temperature: number;
|
||||
temperatureLabel: string;
|
||||
warning?: string;
|
||||
}
|
||||
|
||||
const columns = [
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Avatar sx={{ bgcolor: 'var(--mui-palette-background-level2)', color: 'var(--mui-palette-text-primary)' }}>
|
||||
<TruckIcon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Avatar>
|
||||
<Typography variant="subtitle2">{row.id}</Typography>
|
||||
</Stack>
|
||||
),
|
||||
name: 'Vehicle',
|
||||
width: '200px',
|
||||
},
|
||||
{ field: 'startingRoute', name: 'Starting Route', width: '200px' },
|
||||
{ field: 'endingRoute', name: 'Ending Route', width: '200px' },
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
<Chip color={row.status} label={row.warning ?? 'No warnings'} size="small" variant="soft" />
|
||||
),
|
||||
name: 'Warning',
|
||||
width: '200px',
|
||||
},
|
||||
{
|
||||
formatter: (row): React.JSX.Element => (
|
||||
<Stack spacing={2}>
|
||||
<LinearProgress value={row.temperature} variant="determinate" />
|
||||
<Stack
|
||||
direction="row"
|
||||
spacing={2}
|
||||
sx={{ alignItems: 'center', justifyContent: 'space-between', whiteSpace: 'nowrap' }}
|
||||
>
|
||||
<Typography variant="subtitle2">{row.temperatureLabel}</Typography>
|
||||
<Typography color="text.secondary" variant="inherit">
|
||||
{row.temperature}
|
||||
°C
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Stack>
|
||||
),
|
||||
name: 'Temperature',
|
||||
width: '200px',
|
||||
align: 'right',
|
||||
},
|
||||
] satisfies ColumnDef<Vehicle>[];
|
||||
|
||||
export interface VehiclesTableProps {
|
||||
rows: Vehicle[];
|
||||
}
|
||||
|
||||
export function VehiclesTable({ rows }: VehiclesTableProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<CardHeader
|
||||
avatar={
|
||||
<Avatar>
|
||||
<PathIcon fontSize="var(--Icon-fontSize)" />
|
||||
</Avatar>
|
||||
}
|
||||
title="On route vehicles"
|
||||
/>
|
||||
<Divider />
|
||||
<Box sx={{ overflowX: 'auto' }}>
|
||||
<DataTable<Vehicle> columns={columns} rows={rows} />
|
||||
</Box>
|
||||
</Card>
|
||||
);
|
||||
}
|
@@ -0,0 +1,34 @@
|
||||
import * as React from 'react';
|
||||
import Avatar from '@mui/material/Avatar';
|
||||
import Card from '@mui/material/Card';
|
||||
import Stack from '@mui/material/Stack';
|
||||
import Typography from '@mui/material/Typography';
|
||||
import { Warning as WarningIcon } from '@phosphor-icons/react/dist/ssr/Warning';
|
||||
|
||||
export interface VehiclesWithErrorsProps {
|
||||
amount: number;
|
||||
}
|
||||
|
||||
export function VehiclesWithErrors({ amount }: VehiclesWithErrorsProps): React.JSX.Element {
|
||||
return (
|
||||
<Card>
|
||||
<Stack spacing={1} sx={{ p: 3 }}>
|
||||
<Stack direction="row" spacing={2} sx={{ alignItems: 'center' }}>
|
||||
<Avatar
|
||||
sx={{
|
||||
bgcolor: 'var(--mui-palette-background-paper)',
|
||||
boxShadow: 'var(--mui-shadows-8)',
|
||||
color: 'var(--mui-palette-text-primary)',
|
||||
}}
|
||||
>
|
||||
<WarningIcon fontSize="var(--icon-fontSize-lg)" />
|
||||
</Avatar>
|
||||
<Typography variant="h5">{amount}</Typography>
|
||||
</Stack>
|
||||
<Typography color="text.secondary" variant="body2">
|
||||
Vehicles with errors
|
||||
</Typography>
|
||||
</Stack>
|
||||
</Card>
|
||||
);
|
||||
}
|
Reference in New Issue
Block a user