Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
171 changes: 120 additions & 51 deletions frontend/src/pages/dashboard/Company/ShipmentsMap/ShipmentsMapWidget.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -57,57 +57,126 @@ const getMarkerColor = (shipment: ShipmentWithGps): MarkerColor => {
};

const ShipmentsMapWidget: React.FC = () => {
const [shipments, setShipments] = useState<ShipmentWithGps[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);

const icons = useMemo(() => ({
blue: buildMarkerIcon('blue'),
orange: buildMarkerIcon('orange'),
red: buildMarkerIcon('red'),
}), []);

useEffect(() => {
fixLeafletIcon();
let cancelled = false;

const run = async () => {
try {
setIsLoading(true);
setHasError(false);
const response = await shipmentApi.getAllInTransitWithGps();
if (cancelled) return;
setShipments(response.data);
} catch {
if (cancelled) return;
setHasError(true);
} finally {
if (!cancelled) setIsLoading(false);
}
};

void run();

return () => {
cancelled = true;
};
}, []);

const bounds = useMemo(() => {
const latLngs = shipments
.filter((shipment): shipment is ShipmentWithGps => typeof shipment.lat === 'number' && typeof shipment.lng === 'number')
.map((shipment) => [shipment.lat as number, shipment.lng as number] as [number, number]);

if (!latLngs.length) return undefined;
return L.latLngBounds(latLngs as L.LatLngExpression[]);
}, [shipments]);

return (
<div className="bg-[#14171e] border border-[#1e293b] rounded-xl p-4 max-md:p-3">
<div className="flex items-center justify-between gap-4 mb-3">
<div>
<h3 className="m-0 text-sm font-semibold">Active Shipments</h3>
<p className="m-0 text-xs text-[#94a3b8]">World overview of in-transit shipments</p>
const [shipments, setShipments] = useState<ShipmentWithGps[]>([]);
const [isLoading, setIsLoading] = useState(true);
const [hasError, setHasError] = useState(false);

const icons = useMemo(() => {
return {
blue: buildMarkerIcon('blue'),
orange: buildMarkerIcon('orange'),
red: buildMarkerIcon('red'),
} as const;
}, []);

useEffect(() => {
fixLeafletIcon();
let cancelled = false;

const run = async () => {
try {
setIsLoading(true);
setHasError(false);
const res = await shipmentApi.getAllInTransitWithGps();
if (cancelled) return;
setShipments(res.data);
} catch {
if (cancelled) return;
setHasError(true);
} finally {
if (!cancelled) setIsLoading(false);
}
};

void run();

return () => {
cancelled = true;
};
}, []);

const bounds = useMemo(() => {
const latLngs: Array<[number, number]> = shipments
.filter((s: ShipmentWithGps) => typeof s.lat === 'number' && typeof s.lng === 'number')
.map((s: ShipmentWithGps) => [s.lat as number, s.lng as number]);


if (!latLngs.length) return undefined;

const b = L.latLngBounds(latLngs as L.LatLngExpression[]);
return b;
}, [shipments]);

return (
<div className="bg-[#14171e] border border-[#1e293b] rounded-xl p-4 max-md:p-3">
<div className="flex items-center justify-between gap-4 mb-3">
<div>
<h3 className="m-0 text-sm font-semibold">Active Shipments</h3>
<p className="m-0 text-xs text-[#94a3b8]">World overview of in-transit shipments</p>
</div>
{hasError ? (
<div className="text-xs text-[#ef4444] font-medium">Failed to load</div>
) : isLoading ? (
<div className="text-xs text-[#94a3b8] font-medium">Loading…</div>
) : (
<div className="text-xs text-[#94a3b8] font-medium">{shipments.length} active</div>
)}
</div>

<div className="h-[400px] max-md:h-[250px] rounded-lg overflow-hidden">
<MapContainer
style={{ height: '100%', width: '100%' }}
center={[20, 0]}
zoom={2}
scrollWheelZoom={true}
worldCopyJump={true}
// If we have bounds, zoom to them; otherwise keep default world view.
bounds={bounds as L.LatLngBoundsExpression}
boundsOptions={{ padding: [20, 20] }}
>
<TileLayer
attribution='&copy; <a href="https://www.openstreetmap.org/copyright">OpenStreetMap</a> contributors'
url="https://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png"
/>

<MarkerClusterGroup
chunkedLoading
showCoverageOnHover={false}
>
{shipments
.filter((s) => typeof s.lat === 'number' && typeof s.lng === 'number')
.map((s) => {
const color = getMarkerColor(s);
const icon = icons[color];

return (
<Marker key={s.id} position={[s.lat as number, s.lng as number]} icon={icon}>
<Popup>
<div className="min-w-[240px]">
<div className="text-xs uppercase tracking-[0.05em] text-[#94a3b8] font-semibold mb-2">
Shipment
</div>
<div className="text-sm font-semibold mb-1">{s.trackingNumber ?? s.id}</div>
<div className="text-xs text-[#94a3b8] mb-3">
{s.origin} → {s.destination}
</div>
<div className="flex items-center justify-between gap-3">
<div className="text-xs text-[#94a3b8]">Status: {s.status}</div>
<Link
to={`/shipments/${encodeURIComponent(String(s.id))}`}
className="text-xs font-semibold text-[#3b82f6] no-underline hover:underline"
>
View
</Link>
</div>
</div>
</Popup>
</Marker>
);
})}
</MarkerClusterGroup>
</MapContainer>
</div>
</div>
{hasError ? (
<div className="text-xs text-[#ef4444] font-medium">Failed to load</div>
Expand Down
1 change: 1 addition & 0 deletions frontend/src/pages/dashboard/Company/ShipmentsMap/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1 @@
export { default as ShipmentsMapWidget } from './ShipmentsMapWidget';
Loading