build ok,

This commit is contained in:
louiscklaw
2025-04-14 09:26:24 +08:00
commit 6c931c1fe8
770 changed files with 63959 additions and 0 deletions

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View 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>
);
}

View File

@@ -0,0 +1,10 @@
export interface Vehicle {
id: string;
location: string;
latitude: number;
longitude: number;
temperature: number;
startedAt?: Date;
departedAt?: Date;
arrivedAt?: Date;
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}

View File

@@ -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>
);
}