kios-webapp/astro/src/components/KiosMap.tsx

302 lines
8.0 KiB
TypeScript

import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, CircleMarker, Popup, Polyline, LayerGroup, GeoJSON, } from 'react-leaflet';
import 'leaflet/dist/leaflet.css';
import L, { LatLngBounds } from 'leaflet';
import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/react-query";
import axios from "axios";
//Todo: Move types to own file
interface User {
name: string;
id: string;
email: string;
phoneNumber: string;
}
interface Node {
name: string;
id: string;
}
interface Product extends Node {
id: string;
name: string;
weight?: number;
picture: string;
createdAt: string;
updatedAt: string;
};
// interface Location = {
// latitude: number;
// longitude: number;
// }
interface Maker extends Node {
email: string;
phoneNumber?: string;
location: [number, number];
stock: Product[];
createdAt: string;
updatedAt: string;
};
interface Retailer extends Node {
email: string;
phoneNumber?: string;
location: [number, number];
stock: Product[];
createdAt: string;
updatedAt: string;
};
const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const;
type DispatchStatus = typeof DISPATCH_STATUS[number];
interface Dispatch {
id: string;
dispatchesCode?: string; //Human readable id
createdAt: string;
updatedAt: string;
maker?: Maker;
retailer?: Retailer;
products: Product[];
courier?: User;
timeSensitive: boolean;
status: DispatchStatus;
departureDate: string;
arrivalDate: string;
weightAllowance: number;
}
//Todo: update fetch url endpoints
//Todo: Move queryclient and hooks to own file
//Todo: Move axios stuff
const API_URL = "http://localhost:3001"
const headers = {
"Content-Type": "application/json",
}
const getMakers = async () => {
const url = `${API_URL}/api/makers`
console.log("Fetching url:", url)
const response = await axios.get(url);
const makers: Maker[] = response.data.docs;
console.log(`Fetch result from ${url}`, makers)
return makers;
}
const useGetMakers = () => {
return useQuery<Maker[]>({
queryFn: () => getMakers(),
queryKey: ['makers'],
enabled: true
})
}
const getRetailers = async () => {
const url = `${API_URL}/api/retailers`
console.log("Fetching url:", url)
const response = await axios.get(url);
const retailers: Retailer[] = response.data.docs;
console.log(`Fetch result from ${url}`, retailers)
return retailers;
}
const useGetRetailers = () => {
return useQuery<Retailer[]>({
queryFn: () => getRetailers(),
queryKey: ['retailers'],
enabled: true
})
}
const getDispatches = async () => {
const url = `${API_URL}/api/dispatches`
console.log("Fetching url:", url)
const response = await axios.get(url);
const dispatches: Dispatch[] = response.data.docs;
console.log(`Fetch result from ${url}`, dispatches)
return dispatches;
}
const useGetDispatches = () => {
return useQuery<Dispatch[]>({
queryFn: () => getDispatches(),
queryKey: ['dispatches'],
enabled: true
})
}
//Payload longitude and latitude are mislabeled in payload (lol)
const locationSwitcharoo = (location: number[]) => {
if (location.length === 2) {
const correctedLocation = [location[1], location[0]]
return correctedLocation;
}
console.error("locationSwitcharoo: Location array malformed")
return location
}
const dashArrays: Record<DispatchStatus, string> = {
requested: '20, 10',
accepted: '0, 0',
archived: '1, 5',
}
const dashColors: Record<DispatchStatus, string> = {
requested: '#000',
accepted: '#000',
archived: '#000',
}
const dashColorSelected: string = '#f87171' //same as tw red 400
const dashOpacities: Record<DispatchStatus, number> = {
requested: 0.7,
accepted: 0.7,
archived: 0.5
}
export const KiosMap = () => {
const { data: makers, isLoading: isLoadingMakers } = useGetMakers();
const { data: retailers, isLoading: isLoadingRetailers } = useGetRetailers();
const { data: dispatches, isLoading: isLoadingDispatches } = useGetDispatches();
const [selectedNodeId, setSelectedNodeId] = useState<string>('')
const handleSelectNode = (nodeId: string) => {
setSelectedNodeId(nodeId)
console.log("set id:", nodeId)
}
const blackDotIcon = L.divIcon({
className: 'bg-gray-950 rounded-full',
iconSize: [20, 20],
iconAnchor: [10, 10]
});
const selectedDotIcon = L.divIcon({
className: 'bg-red-400 rounded-full ring-offset-2 ring-4 ring-red-400 ring-dashed',
iconSize: [20, 20],
iconAnchor: [10, 10]
});
const bounds = new LatLngBounds(
[-90, -180], // Southwest corner
[90, 180] // Northeast corner
);
return (
<div className='w-full flex justify-center align-middle'>
<div className=''>
<div></div>
</div>
<MapContainer
id="map"
center={[-6.1815, 106.8228]}
zoom={3}
maxBounds={bounds} // Restrict panning beyond these bounds
maxBoundsViscosity={0.9} // How strongly to snap the map's bounds to the restricted area
style={{ height: '800px', width: '100%' }}
>
<TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
maxZoom={19}
attribution='&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
/>
{(makers && !isLoadingMakers) &&
<LayerGroup>
{makers.map((maker: any, index: number) => (
<Marker
eventHandlers={{
click: () => handleSelectNode(maker.id)
}}
key={maker.id}
position={[locationSwitcharoo(maker.location)[0], locationSwitcharoo(maker.location)[1]]}
icon={selectedNodeId === maker.id ? selectedDotIcon : blackDotIcon}
>
{/* <Popup>{maker.name}</Popup> */}
</Marker>
))}
</LayerGroup>
}
{(retailers && !isLoadingRetailers) &&
<LayerGroup>
{retailers.map((retailer: any, index: number) => (
<Marker
eventHandlers={{
click: () => handleSelectNode(retailer.id)
}}
key={retailer.id}
position={[locationSwitcharoo(retailer.location)[0], locationSwitcharoo(retailer.location)[1]]}
icon={selectedNodeId === retailer.id ? selectedDotIcon : blackDotIcon}
>
{/* <Popup>{retailer.name}</Popup> */}
</Marker>
))}
</LayerGroup>
}
{(dispatches && !isLoadingDispatches) &&
<LayerGroup>
{dispatches.map((dispatch: any, index: number) => {
if (dispatch.maker && dispatch.retailer) {
const start = locationSwitcharoo(dispatch.maker.location);
const end = locationSwitcharoo(dispatch.retailer.location);
let productsString = '';
dispatch.products.forEach((product: any, i: number) => {
productsString += product.productTitle + (i + 1 < dispatch.products.length ? ', ' : '');
});
//status type should already be inferred when list of dispatches is created, weird that is is required
const status: DispatchStatus = dispatch.status;
const dashArray: string = dashArrays[status]
const dashColor: string = dashColors[status]
const dashOpacity: number = dashOpacities[status]
return (
<Polyline
eventHandlers={{
click: () => handleSelectNode(dispatch.id)
}}
key={dispatch.id}
positions={[[start[0], start[1]], [end[0], end[1]]]}
pathOptions={{color: selectedNodeId === dispatch.id ? dashColorSelected : dashColor}}
opacity={dashOpacity}
dashArray={dashArray} />
);
}
})}
</LayerGroup>
}
</MapContainer >
</div>
);
};