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

411 lines
16 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 Contacts from './Contacts';
import type { User, Node, Retailer, Maker, Product, Dispatch, DispatchStatus, CreateDispatch } from '../astroTypes';
import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/react-query";
import { useGetMakers, useGetDispatches, useGetRetailers, useGetUser, useGetMyself, API_URL, useGetMyRetailers, useCreateDispatch } from "../utils/hooks"
import { Button, buttonVariants } from './ui/Button';
import { humanId, poolSize, minLength, maxLength } from 'human-id'
import {
Dialog,
DialogContent,
DialogDescription,
DialogHeader,
DialogTitle,
DialogTrigger,
} from "@/components/ui/dialog"
import { LoginForm } from './LoginForm';
import { hasAuthCookie } from '@/utils/authUtils';
//Payload longitude and latitude are mislabeled/switched in payload
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
}
interface NodeSelection {
id: string,
type: "maker" | "retailer" | "dispatch" | "none"
}
export const KiosMap = () => {
const [authToken, setAuthToken] = useState<string>('')
const { data: makers, isLoading: isLoadingMakers } = useGetMakers();
const { data: retailers, isLoading: isLoadingRetailers } = useGetRetailers();
const { data: dispatches, isLoading: isLoadingDispatches } = useGetDispatches();
const { data: myself, isLoading: isLoadingMyself } = useGetMyself(authToken);
const { data: myRetailers, isLoading: isLoadingMyRetailers } = useGetMyRetailers(myself);
const createDispatchMutation = useCreateDispatch();
const [selectedNode, setSelectedNode] = useState<NodeSelection>({ id: "", type: "none" })
let selectedMaker: Maker | undefined = undefined;
let selectedRetailer: Retailer | undefined = undefined;
let selectedDispatch: Dispatch | undefined = undefined;
if (selectedNode.type === "maker" && makers) {
selectedMaker = makers.find(maker => maker.id === selectedNode.id && selectedNode.type === "maker");
}
if (selectedNode.type === "retailer" && retailers) {
selectedRetailer = retailers.find(retailer => retailer.id === selectedNode.id && selectedNode.type === "retailer");
}
if (selectedNode.type === "dispatch" && dispatches) {
selectedDispatch = dispatches.find(dispatch => dispatch.id === selectedNode.id && selectedNode.type === "dispatch");
}
const handleSelectNode = (nodeId: string, typeParam: "maker" | "retailer" | "dispatch" | "none") => {
setSelectedNode({ id: nodeId, type: typeParam })
}
//params: dispatch: Dispatch, courier: User
const handleAcceptRoute = () => {
}
const handleRequestDispatch = (products: Product[], retailer: Retailer, maker: Maker | undefined) => {
if (maker === undefined) {
console.error("Request dispatch error: Marker undefined")
return
}
const dispatch: CreateDispatch = {
code: humanId({
separator: '-',
capitalize: false,
}),
products: products.map((product) => {return product.id}),
maker: maker.id,
retailer: retailer.id,
timeSensitive: false,
status: "requested",
}
createDispatchMutation.mutate(dispatch)
}
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='flex flex-col gap-4'>
<Dialog>
<div className="flex justify-between items-end px-4 gap-4">
<img
width={120}
src="/kios-logo.png"
alt="" />
{
(!hasAuthCookie() && !authToken) ?
<DialogTrigger
className={`px-12 ${buttonVariants({ variant: "kios" })}`}
>
Login
</DialogTrigger>
: myself && <p>Logged in as: {myself?.name}</p>
}
</div>
<DialogContent>
<DialogHeader>
<DialogTitle className="text-4xl text-center">Login</DialogTitle>
<LoginForm setAuthToken={setAuthToken} authToken={authToken} />
</DialogHeader>
</DialogContent>
</Dialog>
<div className='w-full flex justify-center align-middle'>
<div className=''>
<div>
<img src="/route-guide.png" width={300} className="absolute right-6 top-3/4 z-[998] border border-black" alt="" />
</div>
</div>
{
selectedNode.type !== 'none' && (
<div className='absolute bg-white border-gray-950 border-2 z-[998] left-10 top-1/5 mt-24 p-4'>
<div className='flex gap-8 flex-col'>
{selectedMaker !== undefined && (
<div className='flex gap-4 flex-col'>
<Contacts
name={selectedMaker.name}
email={selectedMaker.email}
phoneNumber={selectedMaker.phoneNumber}
role={'maker'}
/>
{(selectedMaker !== undefined && selectedMaker.stock !== undefined && selectedMaker.stock.length > 0) &&
<Dialog>
<DialogTrigger
className={buttonVariants({ variant: "kios" })}
>
See catalogue
</DialogTrigger>
<DialogContent className='lg:max-w-screen-lg overflow-y-scroll max-h-screen'>
<DialogHeader>
<DialogTitle className="text-4xl underline underline-offset-2">{selectedMaker.name}'s stock</DialogTitle>
<ul className='flex flex-col gap-4'>
{selectedMaker.stock.map((product, i) => {
return (
<li className="flex flex-row gap-4" key={product.id}>
{product.picture.url &&
<img
width={160}
src={product.picture.url}
alt={product.picture.alt || ''} />
}
<div className="flex flex-col pb-4">
<h3 className='text-4xl text-black py-2'>
{product.name}
</h3>
{product.weight &&
<p className='text-black text-lg'
>Weight: {product.weight}</p>
}
{myself ?
(myRetailers !== undefined) &&
<ul>
{myRetailers.map((retailer, i) => {
return (
<li key={retailer.id}>
<Button
variant="kios"
onClick={() => handleRequestDispatch([product], retailer, selectedMaker)}
>
Request product to {retailer.name}
</Button>
</li>
)
})}
</ul>
: <Button disabled>Login to request</Button>
}
</div>
</li>
)
})}
</ul>
</DialogHeader>
</DialogContent>
</Dialog>
}
</div>
)}
{selectedRetailer !== undefined && (
<>
<Contacts
name={selectedRetailer.name}
email={selectedRetailer.email}
phoneNumber={selectedRetailer.phoneNumber}
role={'retailer'}
/>
</>
)}
{selectedDispatch !== undefined && (
<div className='flex flex-col gap-8'>
<div>
<h2 className='text-xl font-bold underline-offset-2 underline py-2'>
Product{selectedDispatch.products.length > 1 && 's'}
</h2>
{selectedDispatch.products.map((product, i) => {
return (
<div className='flex flex-row items-center gap-4' key={product.id}>
<img
src={product.picture.url}
alt={product.picture.alt}
className='border-2 border-black'
width={60}
/>
<h3 className='font-bold text-xl'>{product.name}</h3>
</div>
)
})}
</div>
<Contacts
name={selectedDispatch.maker.name}
email={selectedDispatch.maker.email}
phoneNumber={selectedDispatch.maker.phoneNumber}
role={'maker'}
/>
<Contacts
name={selectedDispatch.retailer.name}
email={selectedDispatch.retailer.email}
phoneNumber={selectedDispatch.retailer.phoneNumber}
role={'retailer'}
/>
{selectedDispatch.courier !== undefined ? (
<Contacts
name={selectedDispatch.courier.name}
email={selectedDispatch.courier.email}
phoneNumber={selectedDispatch.courier.phoneNumber}
role={'courier'}
/>
) :
<div>
<h2 className='text-xl font-bold underline-offset-2 underline py-2'>No courier!</h2>
<Button
variant={"kios"}
onClick={() => handleAcceptRoute()}
>
Accept route as courier
</Button>
</div>
}
</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}
eventHandlers={{
click: () => handleSelectNode("", "none")
}}
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, "maker")
}}
key={maker.id}
position={[locationSwitcharoo(maker.location)[0], locationSwitcharoo(maker.location)[1]]}
icon={selectedNode.id === 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, "retailer")
}}
key={retailer.id}
position={[locationSwitcharoo(retailer.location)[0], locationSwitcharoo(retailer.location)[1]]}
icon={selectedNode.id === 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, "dispatch")
}}
key={dispatch.id}
positions={[[start[0], start[1]], [end[0], end[1]]]}
pathOptions={{ color: selectedNode.id === dispatch.id ? dashColorSelected : dashColor }}
opacity={dashOpacity}
dashArray={dashArray} />
);
}
})}
</LayerGroup>
}
</MapContainer >
</div>
</div>
);
};