Compare commits

...

3 Commits

Author SHA1 Message Date
toqvist ed420d5ff1 Make map nodes selectable and stylish
continuous-integration/drone/push Build is passing Details
2024-04-05 15:03:18 +02:00
toqvist 12c43e9a00 Style dispatches 2024-04-05 13:38:19 +02:00
toqvist 1a2d143c4f Fix dispatches 2024-04-05 13:07:17 +02:00
4 changed files with 166 additions and 109 deletions

View File

@ -1,4 +1,4 @@
import React, { useEffect } from 'react'; import React, { useEffect, useState } from 'react';
import { MapContainer, TileLayer, Marker, CircleMarker, Popup, Polyline, LayerGroup, GeoJSON, } from 'react-leaflet'; import { MapContainer, TileLayer, Marker, CircleMarker, Popup, Polyline, LayerGroup, GeoJSON, } from 'react-leaflet';
import 'leaflet/dist/leaflet.css'; import 'leaflet/dist/leaflet.css';
import L, { LatLngBounds } from 'leaflet'; import L, { LatLngBounds } from 'leaflet';
@ -7,30 +7,33 @@ import { useQuery, useMutation, useQueryClient, queryOptions } from "@tanstack/r
import axios from "axios"; import axios from "axios";
//Todo: Move types to own file //Todo: Move types to own file
type User = { interface User {
name: string; name: string;
id: string; id: string;
email: string; email: string;
phoneNumber: string; phoneNumber: string;
} }
type Product = { interface Node {
name: string;
id: string; id: string;
title: string; }
interface Product extends Node {
id: string;
name: string;
weight?: number; weight?: number;
picture: string; picture: string;
createdAt: string; createdAt: string;
updatedAt: string; updatedAt: string;
}; };
// type Location = { // interface Location = {
// latitude: number; // latitude: number;
// longitude: number; // longitude: number;
// } // }
type Maker = { interface Maker extends Node {
id: string;
name: string;
email: string; email: string;
phoneNumber?: string; phoneNumber?: string;
location: [number, number]; location: [number, number];
@ -39,9 +42,7 @@ type Maker = {
updatedAt: string; updatedAt: string;
}; };
type Retailer = { interface Retailer extends Node {
id: string;
name: string;
email: string; email: string;
phoneNumber?: string; phoneNumber?: string;
location: [number, number]; location: [number, number];
@ -53,7 +54,7 @@ type Retailer = {
const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const; const DISPATCH_STATUS = ['requested', 'accepted', 'archived'] as const;
type DispatchStatus = typeof DISPATCH_STATUS[number]; type DispatchStatus = typeof DISPATCH_STATUS[number];
type Dispatch = { interface Dispatch {
id: string; id: string;
dispatchesCode?: string; //Human readable id dispatchesCode?: string; //Human readable id
createdAt: string; createdAt: string;
@ -64,9 +65,9 @@ type Dispatch = {
products: Product[]; products: Product[];
courier?: User; courier?: User;
timeSensitive: boolean; timeSensitive: boolean;
status: DispatchStatus[]; status: DispatchStatus;
departureDate: string; departureDate: string;
arrivalDate: string; arrivalDate: string;
@ -83,8 +84,6 @@ const headers = {
"Content-Type": "application/json", "Content-Type": "application/json",
} }
const getMakers = async () => { const getMakers = async () => {
const url = `${API_URL}/api/makers` const url = `${API_URL}/api/makers`
console.log("Fetching url:", url) console.log("Fetching url:", url)
@ -109,7 +108,7 @@ const getRetailers = async () => {
console.log("Fetching url:", url) console.log("Fetching url:", url)
const response = await axios.get(url); const response = await axios.get(url);
const retailers:Retailer[] = response.data.docs; const retailers: Retailer[] = response.data.docs;
console.log(`Fetch result from ${url}`, retailers) console.log(`Fetch result from ${url}`, retailers)
return retailers; return retailers;
@ -128,7 +127,8 @@ const getDispatches = async () => {
console.log("Fetching url:", url) console.log("Fetching url:", url)
const response = await axios.get(url); const response = await axios.get(url);
const dispatches:Dispatch[] = response.data.docs; const dispatches: Dispatch[] = response.data.docs;
console.log(`Fetch result from ${url}`, dispatches) console.log(`Fetch result from ${url}`, dispatches)
return dispatches; return dispatches;
@ -137,23 +137,68 @@ const getDispatches = async () => {
const useGetDispatches = () => { const useGetDispatches = () => {
return useQuery<Dispatch[]>({ return useQuery<Dispatch[]>({
queryFn: () => getDispatches(), queryFn: () => getDispatches(),
queryKey: ['retailers'], queryKey: ['dispatches'],
enabled: true 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 = () => { export const KiosMap = () => {
const { data: makers, isLoading: isLoadingMakers } = useGetMakers(); const { data: makers, isLoading: isLoadingMakers } = useGetMakers();
const { data: retailers, isLoading: isLoadingRetailers } = useGetRetailers(); const { data: retailers, isLoading: isLoadingRetailers } = useGetRetailers();
const { data: dispatches, isLoading: isLoadingDispatches } = useGetDispatches(); 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({ const blackDotIcon = L.divIcon({
className: 'bg-gray-950 rounded-full', className: 'bg-gray-950 rounded-full',
iconSize: [20, 20], iconSize: [20, 20],
iconAnchor: [10, 10] 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( const bounds = new LatLngBounds(
[-90, -180], // Southwest corner [-90, -180], // Southwest corner
@ -162,86 +207,94 @@ export const KiosMap = () => {
return ( return (
<div className='w-full flex justify-center align-middle'> <div className='w-full flex justify-center align-middle'>
<div className=''>
<MapContainer <div></div>
id="map" </div>
center={[-6.1815, 106.8228]} <MapContainer
zoom={3} id="map"
maxBounds={bounds} // Restrict panning beyond these bounds center={[-6.1815, 106.8228]}
maxBoundsViscosity={0.9} // How strongly to snap the map's bounds to the restricted area zoom={3}
style={{ height: '800px', width: '100%' }} maxBounds={bounds} // Restrict panning beyond these bounds
> maxBoundsViscosity={0.9} // How strongly to snap the map's bounds to the restricted area
<TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png" style={{ height: '800px', width: '100%' }}
maxZoom={19} >
attribution='&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>' <TileLayer url="https://tile.openstreetmap.org/{z}/{x}/{y}.png"
/> maxZoom={19}
attribution='&copy; <a href="http://www.openstreetmap.org/copyright">OpenStreetMap</a>'
/>
{makers && {(makers && !isLoadingMakers) &&
<LayerGroup> <LayerGroup>
{makers.map((maker: any, index: number) => ( {makers.map((maker: any, index: number) => (
<Marker <Marker
key={index} eventHandlers={{
position={[maker.location[0], maker.location[1]]} click: () => handleSelectNode(maker.id)
icon={blackDotIcon} }}
> key={maker.id}
<Popup>{maker.name}</Popup> position={[locationSwitcharoo(maker.location)[0], locationSwitcharoo(maker.location)[1]]}
</Marker> icon={selectedNodeId === maker.id ? selectedDotIcon : blackDotIcon}
))} >
</LayerGroup> {/* <Popup>{maker.name}</Popup> */}
} </Marker>
))}
</LayerGroup>
}
{retailers && {(retailers && !isLoadingRetailers) &&
<LayerGroup> <LayerGroup>
{retailers.map((retailer: any, index: number) => ( {retailers.map((retailer: any, index: number) => (
<Marker <Marker
key={index} eventHandlers={{
position={[retailer.location[0], retailer.location[1]]} click: () => handleSelectNode(retailer.id)
icon={blackDotIcon} }}
> key={retailer.id}
<Popup>{retailer.name}</Popup> position={[locationSwitcharoo(retailer.location)[0], locationSwitcharoo(retailer.location)[1]]}
</Marker> icon={selectedNodeId === retailer.id ? selectedDotIcon : blackDotIcon}
))} >
</LayerGroup> {/* <Popup>{retailer.name}</Popup> */}
} </Marker>
))}
</LayerGroup>
}
{dispatches && {(dispatches && !isLoadingDispatches) &&
<LayerGroup> <LayerGroup>
{dispatches.map((dispatch: any, index: number) => { {dispatches.map((dispatch: any, index: number) => {
const start = dispatch.maker.location;
const end = dispatch.retailer.location;
let productsString = ''; if (dispatch.maker && dispatch.retailer) {
dispatch.products.forEach((product: any, i: number) => {
productsString += product.productTitle + (i + 1 < dispatch.products.length ? ', ' : '');
});
const myDashArray = const start = locationSwitcharoo(dispatch.maker.location);
dispatch.status === 'requested' ? '20, 10' : const end = locationSwitcharoo(dispatch.retailer.location);
dispatch.status === 'archived' ? '1, 5' :
'0, 0';
return (
<div key={index}> let productsString = '';
<Marker position={[start[0], start[1]]} dispatch.products.forEach((product: any, i: number) => {
icon={blackDotIcon} productsString += product.productTitle + (i + 1 < dispatch.products.length ? ', ' : '');
> });
<Popup>{dispatch.startingPoint.name}</Popup>
</Marker> //status type should already be inferred when list of dispatches is created, weird that is is required
<Marker position={[end[0], end[1]]} const status: DispatchStatus = dispatch.status;
icon={blackDotIcon}
> const dashArray: string = dashArrays[status]
<Popup>{dispatch.endPoint.name}</Popup> const dashColor: string = dashColors[status]
</Marker> const dashOpacity: number = dashOpacities[status]
<Polyline
positions={[[start[0], start[1]], [end[0], end[1]]]} return (
color="#000" <Polyline
dashArray={myDashArray} /> eventHandlers={{
</div> click: () => handleSelectNode(dispatch.id)
); }}
})} key={dispatch.id}
</LayerGroup> positions={[[start[0], start[1]], [end[0], end[1]]]}
} pathOptions={{color: selectedNodeId === dispatch.id ? dashColorSelected : dashColor}}
</MapContainer > opacity={dashOpacity}
dashArray={dashArray} />
);
}
})}
</LayerGroup>
}
</MapContainer >
</div> </div>
); );
}; };

View File

@ -52,17 +52,18 @@ export interface Courier {
} }
export interface Dispatch { export interface Dispatch {
id: string; id: string;
code: string;
products: string[] | Product[]; products: string[] | Product[];
courier?: string | Courier; courier?: string | Courier;
maker?: string | Maker; maker: string | Maker;
retailer?: string | Retailer; retailer: string | Retailer;
status: ('requested' | 'accepted' | 'archived')[]; status: 'requested' | 'accepted' | 'archived';
updatedAt: string; updatedAt: string;
createdAt: string; createdAt: string;
} }
export interface Product { export interface Product {
id: string; id: string;
title: string; name: string;
picture: string | Media; picture: string | Media;
weight?: number; weight?: number;
updatedAt: string; updatedAt: string;

View File

@ -3,17 +3,20 @@ import { CollectionConfig } from 'payload/types';
const Dispatches: CollectionConfig = { const Dispatches: CollectionConfig = {
slug: 'dispatches', slug: 'dispatches',
admin: { admin: {
useAsTitle: 'dispatchesCode', useAsTitle: 'code',
}, },
access: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
// { {
// name: 'dispatchesCode', name: 'code',
// type: 'text', type: 'text',
// required: false, required: true,
// }, maxLength: 20,
unique: true,
label: "Code, a unique name for the dispatch"
},
{ {
name: 'products', name: 'products',
type: 'relationship', type: 'relationship',
@ -33,19 +36,19 @@ const Dispatches: CollectionConfig = {
type: 'relationship', type: 'relationship',
relationTo: 'makers', relationTo: 'makers',
hasMany: false, hasMany: false,
required: false required: true
}, },
{ {
name: 'retailer', name: 'retailer',
type: 'relationship', type: 'relationship',
relationTo: 'retailers', relationTo: 'retailers',
hasMany: false, hasMany: false,
required: false required: true
}, },
{ {
name: 'status', name: 'status',
type: 'select', type: 'select',
hasMany: true, hasMany: false,
required: true, required: true,
options: [ options: [
{ {

View File

@ -3,14 +3,14 @@ import { CollectionConfig } from 'payload/types';
const Products: CollectionConfig = { const Products: CollectionConfig = {
slug: 'products', slug: 'products',
admin: { admin: {
useAsTitle: 'productTitle', useAsTitle: 'name',
}, },
access: { access: {
read: () => true, read: () => true,
}, },
fields: [ fields: [
{ {
name: 'title', name: 'name',
type: 'text', type: 'text',
required: true, required: true,
}, },