From 0cd113861d205da73c3f60ee283b8f105cd8ff52 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 28 Oct 2023 08:55:46 +0000 Subject: [PATCH 01/11] Starting point for new firecallitem base class --- .../FirecallItems/FirecallItemBase.tsx | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 src/components/FirecallItems/FirecallItemBase.tsx diff --git a/src/components/FirecallItems/FirecallItemBase.tsx b/src/components/FirecallItems/FirecallItemBase.tsx new file mode 100644 index 0000000..77e5c0d --- /dev/null +++ b/src/components/FirecallItems/FirecallItemBase.tsx @@ -0,0 +1,70 @@ +import { FirecallItemInfo } from './infos/types'; +import { FirecallItem } from '../firebase/firestore'; +import { IconOptions, Icon } from 'leaflet'; +import { ReactNode } from 'react'; +import { defaultPosition } from '../../hooks/constants'; +import { fallbackIcon } from './icons'; + +export class FirecallItemBase { + constructor(firecallItem?: FirecallItem) { + // empty initializer + this.name = firecallItem?.name || ''; + this.beschreibung = firecallItem?.beschreibung || ''; + this.lat = defaultPosition.lat; + this.lng = defaultPosition.lng; + this.type = 'fallback' + } + + + name: string; + beschreibung?: string; + lat: number; + lng: number; + type: string; + + public title(): string { + return this.name; + } + public info(): string { + return `${this.beschreibung || ''}`; + } + + public body(): string { + return `FirecallItem ${this.name} + ${this.beschreibung} + position: ${this.lat},${this.lng}` + } + + public dialogText(): ReactNode { + return this.name || ''; + } + + public fields(): { [fieldName: string]: string; } { + return { + name: 'Bezeichnung', + beschreibung: 'Beschreibung', + } + } + + public dateFields(): string[] { + return []; + } + + public fieldTypes(): { [fieldName: string]: string; } | undefined { + return {} + } + public popupFn(): ReactNode { + return this.name; + } + public titleFn(): string { + return this.name; + } + public icon(): Icon { + return fallbackIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallItemBase(); + } + +} \ No newline at end of file From 7e7ecc2d8163145ed6c7d4bb82a33c89d240ce86 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 28 Oct 2023 09:25:04 +0000 Subject: [PATCH 02/11] Extend baseclass --- .../FirecallItems/FirecallItemBase.tsx | 27 ++++++++++++++----- 1 file changed, 21 insertions(+), 6 deletions(-) diff --git a/src/components/FirecallItems/FirecallItemBase.tsx b/src/components/FirecallItems/FirecallItemBase.tsx index 77e5c0d..7a34220 100644 --- a/src/components/FirecallItems/FirecallItemBase.tsx +++ b/src/components/FirecallItems/FirecallItemBase.tsx @@ -5,14 +5,14 @@ import { ReactNode } from 'react'; import { defaultPosition } from '../../hooks/constants'; import { fallbackIcon } from './icons'; -export class FirecallItemBase { +export class FirecallItemBase { constructor(firecallItem?: FirecallItem) { // empty initializer this.name = firecallItem?.name || ''; this.beschreibung = firecallItem?.beschreibung || ''; - this.lat = defaultPosition.lat; - this.lng = defaultPosition.lng; - this.type = 'fallback' + this.lat = firecallItem?.lat || defaultPosition.lat; + this.lng = firecallItem?.lng || defaultPosition.lng; + this.type = firecallItem?.type || 'fallback' } @@ -22,6 +22,21 @@ export class FirecallItemBase { lng: number; type: string; + /** + * prepare serialization + * @returns serializable data to save in firestore + */ + public data(): {[key: string]: any} { + return { + + lat: this.lat, + lng: this.lng, + name: this.name, + beschreibung: this.beschreibung, + type: this.type, + } + } + public title(): string { return this.name; } @@ -63,8 +78,8 @@ export class FirecallItemBase { return fallbackIcon; } - public static factory(): FirecallItemBase { - return new FirecallItemBase(); + public static factory(): FirecallItemBase { + return new FirecallItemBase(); } } \ No newline at end of file From d4b13d6355bd176a91b98690ebc5a6f7dd535a68 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 28 Oct 2023 11:43:10 +0000 Subject: [PATCH 03/11] Dockerfile and devcontainer --- .devcontainer/devcontainer.json | 22 +++++++++++++++++++ Dockerfile | 2 +- .../FirecallItems/FirecallItemBase.tsx | 17 ++++++++++++-- 3 files changed, 38 insertions(+), 3 deletions(-) create mode 100644 .devcontainer/devcontainer.json diff --git a/.devcontainer/devcontainer.json b/.devcontainer/devcontainer.json new file mode 100644 index 0000000..c0b19d3 --- /dev/null +++ b/.devcontainer/devcontainer.json @@ -0,0 +1,22 @@ +// For format details, see https://aka.ms/devcontainer.json. For config options, see the +// README at: https://github.com/devcontainers/templates/tree/main/src/typescript-node +{ + "name": "Node.js & TypeScript", + // Or use a Dockerfile or Docker Compose file. More info: https://containers.dev/guide/dockerfile + "image": "mcr.microsoft.com/devcontainers/typescript-node:1-20-bullseye", + + // Features to add to the dev container. More info: https://containers.dev/features. + // "features": {}, + + // Use 'forwardPorts' to make a list of ports inside the container available locally. + "forwardPorts": [3000] + + // Use 'postCreateCommand' to run commands after the container is created. + // "postCreateCommand": "yarn install", + + // Configure tool-specific properties. + // "customizations": {}, + + // Uncomment to connect as root instead. More info: https://aka.ms/dev-containers-non-root. + // "remoteUser": "root" +} diff --git a/Dockerfile b/Dockerfile index 972d624..f257c40 100644 --- a/Dockerfile +++ b/Dockerfile @@ -1,6 +1,6 @@ # https://nextjs.org/docs/deployment # Install dependencies only when needed -FROM node:18-alpine AS base +FROM node:20-alpine AS base FROM base AS deps # Check https://github.com/nodejs/docker-node/tree/b4117f9333da4138b03a546ec926ef50a31506c3#nodealpine to understand why libc6-compat might be needed. RUN apk add --no-cache libc6-compat diff --git a/src/components/FirecallItems/FirecallItemBase.tsx b/src/components/FirecallItems/FirecallItemBase.tsx index 7a34220..9ae2331 100644 --- a/src/components/FirecallItems/FirecallItemBase.tsx +++ b/src/components/FirecallItems/FirecallItemBase.tsx @@ -12,16 +12,25 @@ export class FirecallItemBase { this.beschreibung = firecallItem?.beschreibung || ''; this.lat = firecallItem?.lat || defaultPosition.lat; this.lng = firecallItem?.lng || defaultPosition.lng; - this.type = firecallItem?.type || 'fallback' + this.type = firecallItem?.type || 'fallback'; + this.id = firecallItem?.id; + this.original = firecallItem; } - + id?: string; name: string; beschreibung?: string; lat: number; lng: number; type: string; + deleted?: boolean; + datum?: string; + editable?: boolean; + original?: FirecallItem; + rotation?: string; + + /** * prepare serialization * @returns serializable data to save in firestore @@ -82,4 +91,8 @@ export class FirecallItemBase { return new FirecallItemBase(); } + public renderPopup(): ReactNode { + return <>; + } + } \ No newline at end of file From c5c2af166430f32ea9519ef92af592d2d6792510 Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Sun, 26 Nov 2023 14:04:32 +0100 Subject: [PATCH 04/11] Update rendering --- .../FirecallItems/FirecallItemBase.tsx | 98 ------------- .../FirecallItems/elements/CircleMarker.tsx | 102 +++++++++++++ .../FirecallItems/elements/FirecallArea.tsx | 96 ++++++++++++ .../elements/FirecallItemBase.tsx | 138 ++++++++++++++++++ .../elements/FirecallItemDefault.tsx | 84 +++++++++++ .../elements/FirecallItemMarker.tsx | 78 ++++++++++ src/components/FirecallItems/icons.tsx | 2 +- src/components/FirecallItems/infos/area.tsx | 16 +- src/components/FirecallItems/infos/circle.tsx | 4 +- .../FirecallItems/infos/connection.tsx | 4 +- src/components/FirecallItems/infos/line.tsx | 4 +- src/components/Map/Leitungen/Draw.tsx | 4 +- src/components/firebase/firestore.ts | 1 + 13 files changed, 516 insertions(+), 115 deletions(-) delete mode 100644 src/components/FirecallItems/FirecallItemBase.tsx create mode 100644 src/components/FirecallItems/elements/CircleMarker.tsx create mode 100644 src/components/FirecallItems/elements/FirecallArea.tsx create mode 100644 src/components/FirecallItems/elements/FirecallItemBase.tsx create mode 100644 src/components/FirecallItems/elements/FirecallItemDefault.tsx create mode 100644 src/components/FirecallItems/elements/FirecallItemMarker.tsx diff --git a/src/components/FirecallItems/FirecallItemBase.tsx b/src/components/FirecallItems/FirecallItemBase.tsx deleted file mode 100644 index 9ae2331..0000000 --- a/src/components/FirecallItems/FirecallItemBase.tsx +++ /dev/null @@ -1,98 +0,0 @@ -import { FirecallItemInfo } from './infos/types'; -import { FirecallItem } from '../firebase/firestore'; -import { IconOptions, Icon } from 'leaflet'; -import { ReactNode } from 'react'; -import { defaultPosition } from '../../hooks/constants'; -import { fallbackIcon } from './icons'; - -export class FirecallItemBase { - constructor(firecallItem?: FirecallItem) { - // empty initializer - this.name = firecallItem?.name || ''; - this.beschreibung = firecallItem?.beschreibung || ''; - this.lat = firecallItem?.lat || defaultPosition.lat; - this.lng = firecallItem?.lng || defaultPosition.lng; - this.type = firecallItem?.type || 'fallback'; - this.id = firecallItem?.id; - this.original = firecallItem; - } - - id?: string; - name: string; - beschreibung?: string; - lat: number; - lng: number; - type: string; - - deleted?: boolean; - datum?: string; - editable?: boolean; - original?: FirecallItem; - rotation?: string; - - - /** - * prepare serialization - * @returns serializable data to save in firestore - */ - public data(): {[key: string]: any} { - return { - - lat: this.lat, - lng: this.lng, - name: this.name, - beschreibung: this.beschreibung, - type: this.type, - } - } - - public title(): string { - return this.name; - } - public info(): string { - return `${this.beschreibung || ''}`; - } - - public body(): string { - return `FirecallItem ${this.name} - ${this.beschreibung} - position: ${this.lat},${this.lng}` - } - - public dialogText(): ReactNode { - return this.name || ''; - } - - public fields(): { [fieldName: string]: string; } { - return { - name: 'Bezeichnung', - beschreibung: 'Beschreibung', - } - } - - public dateFields(): string[] { - return []; - } - - public fieldTypes(): { [fieldName: string]: string; } | undefined { - return {} - } - public popupFn(): ReactNode { - return this.name; - } - public titleFn(): string { - return this.name; - } - public icon(): Icon { - return fallbackIcon; - } - - public static factory(): FirecallItemBase { - return new FirecallItemBase(); - } - - public renderPopup(): ReactNode { - return <>; - } - -} \ No newline at end of file diff --git a/src/components/FirecallItems/elements/CircleMarker.tsx b/src/components/FirecallItems/elements/CircleMarker.tsx new file mode 100644 index 0000000..43964dc --- /dev/null +++ b/src/components/FirecallItems/elements/CircleMarker.tsx @@ -0,0 +1,102 @@ +import L, { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { Circle as LeafletCircle } from 'react-leaflet'; +import { Circle, FirecallItem } from '../../firebase/firestore'; +import { circleIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class CircleMarker extends FirecallItemBase { + color: string; + radius: number; + opacity: number; + + public constructor(firecallItem?: Circle) { + super(firecallItem); + this.color = firecallItem?.color || 'green'; + this.radius = firecallItem?.radius || 50; + this.opacity = firecallItem?.opacity || 100; + } + + public data(): FirecallItem { + return { + ...super.data(), + color: this.color, + radius: this.radius, + opacity: this.opacity, + } as Circle; + } + + public title(): string { + return `Kreis ${this.name}`; + } + public info(): string { + return `Radius: ${this.radius || 0}m`; + } + + public body(): string { + return `${this.lat},${this.lng}\nUmkreis: ${this.radius || 0}m`; + } + + public dialogText(): ReactNode { + return ( + <> + Um die Kreis zu zeichnen, auf die gewünschten Positionen klicken. Zum + Abschluss auf einen belibigen Punkt klicken.
+ {this.name || ''} + + ); + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + radius: 'Radius (m)', + color: 'Farbe (HTML bzw. Englisch)', + opacity: 'Deckkraft (in Prozent)', + }; + } + + public dateFields(): string[] { + return []; + } + + public fieldTypes(): { [fieldName: string]: string } | undefined { + return {}; + } + public popupFn(): ReactNode { + return ( + <> + Kreis {this.name} +
+ {this.radius || 0}m + + ); + } + public titleFn(): string { + return `Kreis ${this.name}: Radius ${this.radius || 0}m`; + } + public icon(): Icon { + return circleIcon; + } + + public static factory(): FirecallItemBase { + return new CircleMarker(); + } + + public renderMarker(selectItem: (item: FirecallItem) => void) { + return ( + <> + {super.renderMarker(selectItem)} + + {this.renderPopup(selectItem)} + + + ); + } +} diff --git a/src/components/FirecallItems/elements/FirecallArea.tsx b/src/components/FirecallItems/elements/FirecallArea.tsx new file mode 100644 index 0000000..989eefe --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallArea.tsx @@ -0,0 +1,96 @@ +import { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { defaultPosition } from '../../../hooks/constants'; +import { Area, FirecallItem } from '../../firebase/firestore'; +import { circleIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallArea extends FirecallItemBase { + distance: number = 0; + destLat: number = defaultPosition.lat; + destLng: number = defaultPosition.lng; + /** stringified LatLngPosition[] */ + positions?: string; + color?: string; + opacity?: number; + + public constructor(firecallItem?: Area) { + super(firecallItem); + this.distance = firecallItem?.distance || 0; + this.destLat = firecallItem?.destLat || defaultPosition.lat; + this.destLng = firecallItem?.destLng || defaultPosition.lng + 0.0001; + this.positions = firecallItem?.positions || JSON.stringify([]); + this.color = firecallItem?.color || 'blue'; + this.opacity = firecallItem?.opacity || 50; + } + + public data(): FirecallItem { + return { + ...super.data(), + } as Area; + } + + public markerName() { + return 'Fläche'; + } + + // public title(): string { + // return `Marker ${this.name}`; + // } + // public info(): string { + // return `Länge ${this.distance}m`; + // } + + public body(): string { + return `${this.lat},${this.lng} => ${this.destLat},${this.destLng}`; + } + + public dialogText(): ReactNode { + return ( + <> + Um die Fläche zu zeichnen, auf die gewünschten Positionen klicken. Zum + Abschluss auf einen belibigen Punkt klicken.
+ {this.name || ''} + + ); + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + color: 'Farbe (HTML bzw. Englisch)', + opacity: 'Deckkraft (in Prozent)', + }; + } + + // public dateFields(): string[] { + // return []; + // } + + // public fieldTypes(): { [fieldName: string]: string } | undefined { + // return {}; + // } + public popupFn(): ReactNode { + return ( + <> + Fläche {this.name} + + ); + } + public titleFn(): string { + return `${this.markerName()} ${this.name}\n${this.beschreibung || ''}`; + } + public icon(): Icon { + return circleIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallArea(); + } + + // public renderMarker(selectItem: (this: FirecallItem) => void) { + // return ( + + // ); + // } +} diff --git a/src/components/FirecallItems/elements/FirecallItemBase.tsx b/src/components/FirecallItems/elements/FirecallItemBase.tsx new file mode 100644 index 0000000..1e64268 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallItemBase.tsx @@ -0,0 +1,138 @@ +import EditIcon from '@mui/icons-material/Edit'; +import { IconButton } from '@mui/material'; +import { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { Popup } from 'react-leaflet'; +import { defaultPosition } from '../../../hooks/constants'; +import { FirecallItem } from '../../firebase/firestore'; +import { fallbackIcon } from '../icons'; +import { FirecallItemMarkerDefault } from './FirecallItemDefault'; + +export interface FirecallItemPopupProps { + children: ReactNode; + onClick: () => void; +} + +export function FirecallItemPopup({ + children, + onClick, +}: FirecallItemPopupProps) { + return ( + + + + + {children} + + ); +} + +/** + * base class for all firecall items + */ + +export class FirecallItemBase { + constructor(firecallItem?: FirecallItem) { + // empty initializer + this.name = firecallItem?.name || ''; + this.beschreibung = firecallItem?.beschreibung || ''; + this.lat = firecallItem?.lat || defaultPosition.lat; + this.lng = firecallItem?.lng || defaultPosition.lng; + this.type = firecallItem?.type || 'fallback'; + this.id = firecallItem?.id; + this.original = firecallItem; + this.datum = firecallItem?.datum || new Date().toISOString(); + } + + id?: string; + name: string; + beschreibung?: string; + lat: number; + lng: number; + type: string; + + deleted?: boolean; + datum?: string; + editable?: boolean; + original?: FirecallItem; + rotation?: string; + + /** + * prepare serialization + * @returns serializable data to save in firestore + */ + public data(): FirecallItem { + return { + id: this.id, + lat: this.lat, + lng: this.lng, + name: this.name, + beschreibung: this.beschreibung, + type: this.type, + datum: this.datum, + rotation: this.rotation, + }; + } + + public markerName() { + return 'Firecallitem'; + } + + public title(): string { + return `${this.markerName()} ${this.name}`; + } + + public info(): string { + return `${this.beschreibung || ''}`; + } + + public body(): string { + return `${this.markerName()} ${this.name} + ${this.beschreibung} + position: ${this.lat},${this.lng}`; + } + + public dialogText(): ReactNode { + return this.name || ''; + } + + public fields(): { [fieldName: string]: string } { + return { + name: 'Bezeichnung', + beschreibung: 'Beschreibung', + }; + } + + public dateFields(): string[] { + return []; + } + + public fieldTypes(): { [fieldName: string]: string } | undefined { + return {}; + } + public popupFn(): ReactNode { + return this.name; + } + public titleFn(): string { + return this.name; + } + public icon(): Icon { + return fallbackIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallItemBase(); + } + + public renderPopup(selectItem: (item: FirecallItem) => void): ReactNode { + return ( + selectItem(this.data())}> + {this.popupFn()} + + ); + } + + public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + return ; + } +} diff --git a/src/components/FirecallItems/elements/FirecallItemDefault.tsx b/src/components/FirecallItems/elements/FirecallItemDefault.tsx new file mode 100644 index 0000000..eba68f1 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallItemDefault.tsx @@ -0,0 +1,84 @@ +import { doc, setDoc } from 'firebase/firestore'; +import L from 'leaflet'; +import { useEffect, useState } from 'react'; +import { defaultPosition } from '../../../hooks/constants'; +import { useFirecallId } from '../../../hooks/useFirecall'; +import { RotatedMarker } from '../../Map/markers/RotatedMarker'; +import { firestore } from '../../firebase/firebase'; +import { FirecallItem } from '../../firebase/firestore'; +import { FirecallItemBase } from './FirecallItemBase'; + +export interface FirecallItemMarkerProps { + record: FirecallItemBase; + selectItem: (item: FirecallItem) => void; +} + +async function updateFircallItemPos( + firecallId: string, + event: L.DragEndEvent, + fcItem: FirecallItem +) { + const newPos = (event.target as L.Marker)?.getLatLng(); + // console.info(`drag end on ${JSON.stringify(fcItem)}: ${newPos}`); + if (fcItem.id && newPos) { + const updatePos = { + lat: newPos.lat, + lng: newPos.lng, + }; + + await setDoc( + doc(firestore, 'call', firecallId, 'item', fcItem.id), + updatePos, + { + merge: true, + } + ); + } +} + +export function FirecallItemMarkerDefault({ + record, + selectItem, +}: FirecallItemMarkerProps) { + const icon = record.icon(); + const firecallId = useFirecallId(); + const [startPos, setStartPos] = useState( + L.latLng( + record.lat || defaultPosition.lat, + record.lng || defaultPosition.lng + ) + ); + + useEffect(() => { + if (record.lat && record.lng) { + setStartPos(L.latLng(record.lat, record.lng)); + } + }, [record.lat, record.lng]); + + return ( + <> + { + setStartPos((event.target as L.Marker)?.getLatLng()); + updateFircallItemPos(firecallId, event, record); + }, + }} + rotationAngle={ + record?.rotation && + !Number.isNaN(Number.parseInt(record?.rotation, 10)) + ? Number.parseInt(record?.rotation, 10) % 360 + : 0 + } + rotationOrigin="center" + > + {record.renderPopup(selectItem)} + + + ); +} diff --git a/src/components/FirecallItems/elements/FirecallItemMarker.tsx b/src/components/FirecallItems/elements/FirecallItemMarker.tsx new file mode 100644 index 0000000..31e2908 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallItemMarker.tsx @@ -0,0 +1,78 @@ +import { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { FirecallItem } from '../../firebase/firestore'; +import { markerIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallItemMarker extends FirecallItemBase { + public constructor(firecallItem?: FirecallItem) { + super(firecallItem); + this.type = 'marker'; + } + + public data(): FirecallItem { + return { + ...super.data(), + } as FirecallItem; + } + + public markerName() { + return 'Marker'; + } + + // public title(): string { + // return `Marker ${this.name}`; + // } + + // public info(): string { + // return `${this.beschreibung || ''}`; + // } + + public body(): string { + return `Marker ${this.name} + ${this.beschreibung}`; + } + + public dialogText(): ReactNode { + return <>Markierung {this.name}; + } + + // public fields(): { [fieldName: string]: string } { + // return { + // ...super.fields(), + // }; + // } + + // public dateFields(): string[] { + // return []; + // } + + public fieldTypes(): { [fieldName: string]: string } | undefined { + return {}; + } + public popupFn(): ReactNode { + return ( + <> + {this.name} +
+ {this.beschreibung || ''} + + ); + } + public titleFn(): string { + return `${this.name}\n${this.beschreibung || ''}`; + } + public icon(): Icon { + return markerIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallItemMarker(); + } + + // public renderMarker(selectItem: (item: FirecallItem) => void) { + // return ( + + // ); + // } +} diff --git a/src/components/FirecallItems/icons.tsx b/src/components/FirecallItems/icons.tsx index 1e3c94d..acd636a 100644 --- a/src/components/FirecallItems/icons.tsx +++ b/src/components/FirecallItems/icons.tsx @@ -28,7 +28,7 @@ export const markerIcon = L.icon({ popupAnchor: [0, -25], }); -export const connectionIcon = L.icon({ +export const circleIcon = L.icon({ iconUrl: `/icons/circle.svg`, iconSize: [11, 11], iconAnchor: [6, 6], diff --git a/src/components/FirecallItems/infos/area.tsx b/src/components/FirecallItems/infos/area.tsx index eedfce3..3f8c8fe 100644 --- a/src/components/FirecallItems/infos/area.tsx +++ b/src/components/FirecallItems/infos/area.tsx @@ -1,9 +1,9 @@ import { mapPosition } from '../../../hooks/useMapPosition'; -import { Connection } from '../../firebase/firestore'; -import { connectionIcon } from '../icons'; +import { Area } from '../../firebase/firestore'; +import { circleIcon } from '../icons'; import { FirecallItemInfo } from './types'; -export const areaInfo: FirecallItemInfo = { +export const areaInfo: FirecallItemInfo = { name: 'Fläche', title: (item) => `Fläche ${item.name}`, info: (item) => `Länge: ${item.distance || 0}m`, @@ -23,7 +23,7 @@ export const areaInfo: FirecallItemInfo = { }, dateFields: [], factory: () => ({ - type: 'connection', + type: 'area', name: '', beschreibung: '', destLat: mapPosition.lat, @@ -33,13 +33,13 @@ export const areaInfo: FirecallItemInfo = { opacity: 50, datum: new Date().toISOString(), }), - popupFn: (item: Connection) => ( + popupFn: (item: Area) => ( <> Fläche {item.name} ), - titleFn: (item: Connection) => `Fläche ${item.name}`, - icon: (item: Connection) => { - return connectionIcon; + titleFn: (item: Area) => `Fläche ${item.name}`, + icon: (item: Area) => { + return circleIcon; }, }; diff --git a/src/components/FirecallItems/infos/circle.tsx b/src/components/FirecallItems/infos/circle.tsx index 5329f05..f840b59 100644 --- a/src/components/FirecallItems/infos/circle.tsx +++ b/src/components/FirecallItems/infos/circle.tsx @@ -1,5 +1,5 @@ import { Circle } from '../../firebase/firestore'; -import { connectionIcon } from '../icons'; +import { circleIcon } from '../icons'; import { FirecallItemInfo } from './types'; export const circleInfo: FirecallItemInfo = { @@ -39,6 +39,6 @@ export const circleInfo: FirecallItemInfo = { ), titleFn: (item: Circle) => `Kreis ${item.name}: Radius ${item.radius || 0}m`, icon: (item: Circle) => { - return connectionIcon; + return circleIcon; }, }; diff --git a/src/components/FirecallItems/infos/connection.tsx b/src/components/FirecallItems/infos/connection.tsx index cb6c94a..db4f8dc 100644 --- a/src/components/FirecallItems/infos/connection.tsx +++ b/src/components/FirecallItems/infos/connection.tsx @@ -2,7 +2,7 @@ import { latLngPosition, LatLngPosition } from '../../../common/geo'; import { toLatLng } from '../../../hooks/constants'; import { mapPosition } from '../../../hooks/useMapPosition'; import { Connection } from '../../firebase/firestore'; -import { connectionIcon } from '../icons'; +import { circleIcon } from '../icons'; import { FirecallItemInfo } from './types'; export const getConnectionPositions = ( @@ -76,6 +76,6 @@ export const connectionInfo: FirecallItemInfo = { ), titleFn: (item: Connection) => `Leitung ${item.name}: ${item.distance || 0}m`, icon: (item: Connection) => { - return connectionIcon; + return circleIcon; }, }; diff --git a/src/components/FirecallItems/infos/line.tsx b/src/components/FirecallItems/infos/line.tsx index 5f3a071..0838289 100644 --- a/src/components/FirecallItems/infos/line.tsx +++ b/src/components/FirecallItems/infos/line.tsx @@ -1,6 +1,6 @@ import { mapPosition } from '../../../hooks/useMapPosition'; import { Line } from '../../firebase/firestore'; -import { connectionIcon } from '../icons'; +import { circleIcon } from '../icons'; import { FirecallItemInfo } from './types'; export const lineInfo: FirecallItemInfo = { @@ -42,6 +42,6 @@ export const lineInfo: FirecallItemInfo = { ), titleFn: (item: Line) => `Linie ${item.name}: ${item.distance || 0}m`, icon: (item: Line) => { - return connectionIcon; + return circleIcon; }, }; diff --git a/src/components/Map/Leitungen/Draw.tsx b/src/components/Map/Leitungen/Draw.tsx index 54c8af2..d73d4d7 100644 --- a/src/components/Map/Leitungen/Draw.tsx +++ b/src/components/Map/Leitungen/Draw.tsx @@ -1,7 +1,7 @@ import { LatLng, LeafletMouseEvent, LeafletMouseEventHandlerFn } from 'leaflet'; import { useCallback, useState } from 'react'; import { Marker, Polyline, useMap, useMapEvent } from 'react-leaflet'; -import { connectionIcon } from '../../FirecallItems/icons'; +import { circleIcon } from '../../FirecallItems/icons'; import { useLeitungen } from './context'; // const itemInfo = firecallItemInfo('marker'); @@ -34,7 +34,7 @@ const LeitungenDraw = () => { key={p.toString()} position={p} title={`p ${p}`} - icon={connectionIcon} + icon={circleIcon} draggable autoPan={false} eventHandlers={{ diff --git a/src/components/firebase/firestore.ts b/src/components/firebase/firestore.ts index a08a885..b103890 100644 --- a/src/components/firebase/firestore.ts +++ b/src/components/firebase/firestore.ts @@ -55,6 +55,7 @@ export interface Area extends FirecallItem { positions?: string; distance?: number; color?: string; + opacity?: number; } export interface Line extends Connection { From e24005353168162ebfecabeabce4e908b7b10089 Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Sat, 23 Dec 2023 09:45:35 +0100 Subject: [PATCH 05/11] Rewerite components --- .../FirecallItems/FirecallItemDialog.tsx | 98 ++++++----- .../FirecallItems/elements/CircleMarker.tsx | 22 ++- .../FirecallItems/elements/FirecallArea.tsx | 19 ++- .../FirecallItems/elements/FirecallAssp.tsx | 79 +++++++++ .../elements/FirecallConnection.tsx | 112 +++++++++++++ .../FirecallItems/elements/FirecallEl.tsx | 80 +++++++++ .../elements/FirecallItemBase.tsx | 15 +- .../elements/FirecallItemMarker.tsx | 13 +- .../FirecallItems/elements/FirecallLine.tsx | 38 +++++ .../FirecallItems/elements/FirecallRohr.tsx | 109 ++++++++++++ .../elements/FirecallVehicle.tsx | 156 ++++++++++++++++++ .../elements/area/AreaComponent.tsx | 106 ++++++++++++ .../elements/area/areaFunctions.ts | 54 ++++++ .../connection/ConnectionComponent.tsx | 144 ++++++++++++++++ .../elements/connection/distance.ts | 36 ++++ .../elements/connection/positions.ts | 88 ++++++++++ .../FirecallItems/elements/index.tsx | 63 +++++++ .../{ => marker}/FirecallItemDefault.tsx | 12 +- src/components/Map/markers/FirecallItems.tsx | 24 ++- src/components/firebase/firestore.ts | 9 +- src/hooks/useFirecallItemUpdate.ts | 21 ++- 21 files changed, 1202 insertions(+), 96 deletions(-) create mode 100644 src/components/FirecallItems/elements/FirecallAssp.tsx create mode 100644 src/components/FirecallItems/elements/FirecallConnection.tsx create mode 100644 src/components/FirecallItems/elements/FirecallEl.tsx create mode 100644 src/components/FirecallItems/elements/FirecallLine.tsx create mode 100644 src/components/FirecallItems/elements/FirecallRohr.tsx create mode 100644 src/components/FirecallItems/elements/FirecallVehicle.tsx create mode 100644 src/components/FirecallItems/elements/area/AreaComponent.tsx create mode 100644 src/components/FirecallItems/elements/area/areaFunctions.ts create mode 100644 src/components/FirecallItems/elements/connection/ConnectionComponent.tsx create mode 100644 src/components/FirecallItems/elements/connection/distance.ts create mode 100644 src/components/FirecallItems/elements/connection/positions.ts create mode 100644 src/components/FirecallItems/elements/index.tsx rename src/components/FirecallItems/elements/{ => marker}/FirecallItemDefault.tsx (83%) diff --git a/src/components/FirecallItems/FirecallItemDialog.tsx b/src/components/FirecallItems/FirecallItemDialog.tsx index 0eb0eaa..2e74a20 100644 --- a/src/components/FirecallItems/FirecallItemDialog.tsx +++ b/src/components/FirecallItems/FirecallItemDialog.tsx @@ -12,10 +12,14 @@ import TextField from '@mui/material/TextField'; import moment from 'moment'; import React, { useState } from 'react'; import ConfirmDialog from '../dialogs/ConfirmDialog'; -import MyDateTimePicker from '../inputs/DateTimePicker'; import { FirecallItem } from '../firebase/firestore'; -import { firecallItemInfo, firecallItems } from './infos/firecallitems'; -import { FirecallItemInfo } from './infos/types'; +import MyDateTimePicker from '../inputs/DateTimePicker'; +import { fcItemClasses, fcItemNames, getItemClass } from './elements'; +import { FirecallItemBase } from './elements/FirecallItemBase'; +import { CheckBox } from '@mui/icons-material'; +import FormGroup from '@mui/material/FormGroup'; +import FormControlLabel from '@mui/material/FormControlLabel'; +import Checkbox from '@mui/material/Checkbox'; export interface FirecallItemDialogOptions { onClose: (item?: FirecallItem) => void; @@ -31,18 +35,13 @@ export default function FirecallItemDialog({ type: itemType, }: FirecallItemDialogOptions) { const [open, setOpen] = useState(true); - const [item, setFirecallItem] = useState( - itemDefault || firecallItemInfo(itemType).factory() + const [item, setFirecallItem] = useState( + getItemClass(itemDefault || ({ type: itemType } as FirecallItem)) ); const [confirmDelete, setConfirmDelete] = useState(false); - const itemInfo: FirecallItemInfo = firecallItemInfo(item.type); - const setItemField = (field: string, value: any) => { - setFirecallItem((prev) => ({ - ...prev, - [field]: value, - })); + setFirecallItem((prev) => getItemClass({ ...prev.data(), [field]: value })); }; const onChange = @@ -52,10 +51,10 @@ export default function FirecallItemDialog({ const handleChange = (event: SelectChangeEvent) => { setItemField('type', event.target.value); - setFirecallItem((prev) => ({ - ...firecallItemInfo(event.target.value).factory(), - ...prev, - })); + // setFirecallItem((prev) => ({ + // ...firecallItemInfo(event.target.value).factory(), + // ...prev, + // })); }; return ( @@ -63,13 +62,13 @@ export default function FirecallItemDialog({ onClose()}> {item.id ? ( - <>{itemInfo.name} bearbeiten + <>{item.markerName()} bearbeiten ) : ( - <>Neu: {itemInfo.name} hinzufügen + <>Neu: {item.markerName()} hinzufügen )} - {itemInfo.dialogText(item)} + {item.dialogText()} {allowTypeChange && ( Element Typ @@ -80,19 +79,19 @@ export default function FirecallItemDialog({ label="Art" onChange={handleChange} > - {Object.entries(firecallItems) - .filter(([key, fcItem]) => key !== 'fallback') - .map(([key, fcItem]) => ( + {Object.entries(fcItemNames) + .filter(([key, name]) => key !== 'fallback') + .map(([key, name]) => ( - {fcItem.name} + {name} ))} )} - {Object.entries(itemInfo.fields).map(([key, label]) => ( + {Object.entries(item.fields()).map(([key, label]) => ( - {itemInfo.dateFields.includes(key) && ( + {item.dateFields().includes(key) && ( )} - {!itemInfo.dateFields.includes(key) && ( - + {item.fieldTypes()[key] === 'boolean' && ( + + { + setItemField(key, checked ? 'true' : 'false'); + }} + /> + } + label={label} + /> + )} + {!item.dateFields().includes(key) && + item.fieldTypes()[key] !== 'boolean' && ( + + )} ))} @@ -155,10 +171,8 @@ export default function FirecallItemDialog({ {confirmDelete && ( { setConfirmDelete(false); if (result) { diff --git a/src/components/FirecallItems/elements/CircleMarker.tsx b/src/components/FirecallItems/elements/CircleMarker.tsx index 43964dc..c9f2bf9 100644 --- a/src/components/FirecallItems/elements/CircleMarker.tsx +++ b/src/components/FirecallItems/elements/CircleMarker.tsx @@ -9,12 +9,14 @@ export class CircleMarker extends FirecallItemBase { color: string; radius: number; opacity: number; + fill: string; public constructor(firecallItem?: Circle) { super(firecallItem); this.color = firecallItem?.color || 'green'; this.radius = firecallItem?.radius || 50; this.opacity = firecallItem?.opacity || 100; + this.fill = firecallItem?.fill === undefined ? 'true' : firecallItem.fill; } public data(): FirecallItem { @@ -23,11 +25,16 @@ export class CircleMarker extends FirecallItemBase { color: this.color, radius: this.radius, opacity: this.opacity, + fill: this.fill, } as Circle; } + public markerName(): string { + return `Kreis`; + } + public title(): string { - return `Kreis ${this.name}`; + return `${this.markerName()} ${this.name}`; } public info(): string { return `Radius: ${this.radius || 0}m`; @@ -40,8 +47,7 @@ export class CircleMarker extends FirecallItemBase { public dialogText(): ReactNode { return ( <> - Um die Kreis zu zeichnen, auf die gewünschten Positionen klicken. Zum - Abschluss auf einen belibigen Punkt klicken.
+ Um die Kreis zu zeichnen, auf die gewünschten Positionen klicken.
{this.name || ''} ); @@ -52,6 +58,7 @@ export class CircleMarker extends FirecallItemBase { ...super.fields(), radius: 'Radius (m)', color: 'Farbe (HTML bzw. Englisch)', + fill: 'Kreis ausfüllen', opacity: 'Deckkraft (in Prozent)', }; } @@ -60,8 +67,10 @@ export class CircleMarker extends FirecallItemBase { return []; } - public fieldTypes(): { [fieldName: string]: string } | undefined { - return {}; + public fieldTypes(): { [fieldName: string]: string } { + return { + fill: 'boolean', + }; } public popupFn(): ReactNode { return ( @@ -88,11 +97,12 @@ export class CircleMarker extends FirecallItemBase { <> {super.renderMarker(selectItem)} {this.renderPopup(selectItem)} diff --git a/src/components/FirecallItems/elements/FirecallArea.tsx b/src/components/FirecallItems/elements/FirecallArea.tsx index 989eefe..9fd6cc8 100644 --- a/src/components/FirecallItems/elements/FirecallArea.tsx +++ b/src/components/FirecallItems/elements/FirecallArea.tsx @@ -4,6 +4,7 @@ import { defaultPosition } from '../../../hooks/constants'; import { Area, FirecallItem } from '../../firebase/firestore'; import { circleIcon } from '../icons'; import { FirecallItemBase } from './FirecallItemBase'; +import AreaMarker from './area/AreaComponent'; export class FirecallArea extends FirecallItemBase { distance: number = 0; @@ -24,7 +25,7 @@ export class FirecallArea extends FirecallItemBase { this.opacity = firecallItem?.opacity || 50; } - public data(): FirecallItem { + public data(): Area { return { ...super.data(), } as Area; @@ -67,9 +68,11 @@ export class FirecallArea extends FirecallItemBase { // return []; // } - // public fieldTypes(): { [fieldName: string]: string } | undefined { - // return {}; - // } + public fieldTypes(): { [fieldName: string]: string } { + return { + opacity: 'number', + }; + } public popupFn(): ReactNode { return ( <> @@ -88,9 +91,7 @@ export class FirecallArea extends FirecallItemBase { return new FirecallArea(); } - // public renderMarker(selectItem: (this: FirecallItem) => void) { - // return ( - - // ); - // } + public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + return ; + } } diff --git a/src/components/FirecallItems/elements/FirecallAssp.tsx b/src/components/FirecallItems/elements/FirecallAssp.tsx new file mode 100644 index 0000000..3cb708d --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallAssp.tsx @@ -0,0 +1,79 @@ +import { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { FirecallItem } from '../../firebase/firestore'; +import { asspIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallAssp extends FirecallItemBase { + public constructor(firecallItem?: FirecallItem) { + super(firecallItem); + this.type = 'assp'; + } + + public data(): FirecallItem { + return { + ...super.data(), + } as FirecallItem; + } + + public markerName() { + return 'Atemschutzsammelplatz'; + } + + // public title(): string { + // return `Marker ${this.name}`; + // } + + // public info(): string { + // return `${this.beschreibung || ''}`; + // } + + // public body(): string { + // return `${this.markerName()} ${this.name} + // ${this.beschreibung} + // position: ${this.lat},${this.lng}`; + // } + + public dialogText(): ReactNode { + return <>ASSP {this.name}; + } + + // public fields(): { [fieldName: string]: string } { + // return { + // ...super.fields(), + // }; + // } + + // public dateFields(): string[] { + // return []; + // } + + public fieldTypes(): { [fieldName: string]: string } { + return {}; + } + public popupFn(): ReactNode { + return ( + <> + {this.name} +
+ {this.beschreibung || ''} + + ); + } + public titleFn(): string { + return `ASSP ${this.name}\n${this.beschreibung || ''}`; + } + public icon(): Icon { + return asspIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallAssp(); + } + + // public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + // return ( + + // ); + // } +} diff --git a/src/components/FirecallItems/elements/FirecallConnection.tsx b/src/components/FirecallItems/elements/FirecallConnection.tsx new file mode 100644 index 0000000..d33d126 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallConnection.tsx @@ -0,0 +1,112 @@ +import { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { defaultPosition } from '../../../hooks/constants'; +import { Connection, FirecallItem } from '../../firebase/firestore'; +import { circleIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; +import ConnectionMarker from './connection/ConnectionComponent'; + +export class FirecallConnection extends FirecallItemBase { + destLat: number = defaultPosition.lat; + destLng: number = defaultPosition.lng; + /** stringified LatLngPosition[] */ + positions?: string; + distance?: number; + color?: string; + + public constructor(firecallItem?: Connection) { + super(firecallItem); + this.type = 'connection'; + if (firecallItem) { + ({ + destLat: this.destLat, + destLng: this.destLng, + positions: this.positions, + distance: this.distance, + color: this.color, + } = firecallItem); + } + } + + public markerName() { + return 'Leitung'; + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + color: 'Farbe (HTML bzw. Englisch)', + }; + } + + // public fieldTypes(): { [fieldName: string]: string } { + // return { + // }; + // } + + public data(): Connection { + return { + ...super.data(), + destLat: this.destLat, + destLng: this.destLng, + positions: this.positions, + distance: this.distance, + color: this.color, + } as Connection; + } + + // public title(): string { + // return `${this.name}`; + // } + + public info(): string { + return `Länge: ${this.distance || 0}m`; + } + + public body(): string { + return `${this.lat},${this.lng} => ${this.destLat},${this.destLng}`; + } + + public dialogText(): ReactNode { + return ( + <> + Um die Leitung zu zeichnen, auf die gewünschten Positionen klicken. Zum + Abschluss auf einen belibigen Punkt klicken.
+ {this.name || ''} + + ); + } + + // public dateFields(): string[] { + // return [...super.dateFields()]; + // } + + public titleFn(): string { + return `${this.markerName()} ${this.name}: ${this.distance || 0}m`; + } + public icon(): Icon { + return circleIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallConnection(); + } + + public popupFn(): ReactNode { + return ( + <> + + {this.markerName()} {this.name} + +
+ {this.distance || 0} + m, {Math.ceil((this.distance || 0) / 20)} B Schläuche + + ); + } + public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + return ( + + ); + } +} diff --git a/src/components/FirecallItems/elements/FirecallEl.tsx b/src/components/FirecallItems/elements/FirecallEl.tsx new file mode 100644 index 0000000..b04a71e --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallEl.tsx @@ -0,0 +1,80 @@ +import { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { FirecallItem } from '../../firebase/firestore'; +import { asspIcon, elIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallEinsatzleitung extends FirecallItemBase { + public constructor(firecallItem?: FirecallItem) { + super(firecallItem); + this.type = 'el'; + } + + public data(): FirecallItem { + return { + ...super.data(), + } as FirecallItem; + } + + public markerName() { + return 'Einsatzleitung'; + } + + // public title(): string { + // return `Marker ${this.name}`; + // } + + // public info(): string { + // return `${this.beschreibung || ''}`; + // } + + // public body(): string { + // return `${this.markerName()} ${this.name} + // ${this.beschreibung} + // position: ${this.lat},${this.lng}`; + // } + + public dialogText(): ReactNode { + return <>Einsatzleitung {this.name}; + } + + // public fields(): { [fieldName: string]: string } { + // return { + // ...super.fields(), + // }; + // } + + // public dateFields(): string[] { + // return []; + // } + + public titleFn(): string { + return `ELung ${this.name}\n${this.beschreibung || ''}`; + } + public icon(): Icon { + return elIcon; + } + + public fieldTypes(): { [fieldName: string]: string } { + return {}; + } + + public static factory(): FirecallItemBase { + return new FirecallEinsatzleitung(); + } + + public popupFn(): ReactNode { + return ( + <> + Einsatzleitung {this.name} +
+ {this.beschreibung || ''} + + ); + } + // public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + // return ( + + // ); + // } +} diff --git a/src/components/FirecallItems/elements/FirecallItemBase.tsx b/src/components/FirecallItems/elements/FirecallItemBase.tsx index 1e64268..a62a18d 100644 --- a/src/components/FirecallItems/elements/FirecallItemBase.tsx +++ b/src/components/FirecallItems/elements/FirecallItemBase.tsx @@ -6,7 +6,7 @@ import { Popup } from 'react-leaflet'; import { defaultPosition } from '../../../hooks/constants'; import { FirecallItem } from '../../firebase/firestore'; import { fallbackIcon } from '../icons'; -import { FirecallItemMarkerDefault } from './FirecallItemDefault'; +import { FirecallItemMarkerDefault } from './marker/FirecallItemDefault'; export interface FirecallItemPopupProps { children: ReactNode; @@ -42,6 +42,7 @@ export class FirecallItemBase { this.id = firecallItem?.id; this.original = firecallItem; this.datum = firecallItem?.datum || new Date().toISOString(); + this.rotation = firecallItem?.rotation || '0'; } id?: string; @@ -104,10 +105,10 @@ export class FirecallItemBase { } public dateFields(): string[] { - return []; + return ['datum']; } - public fieldTypes(): { [fieldName: string]: string } | undefined { + public fieldTypes(): { [fieldName: string]: string } { return {}; } public popupFn(): ReactNode { @@ -133,6 +134,12 @@ export class FirecallItemBase { } public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { - return ; + return ( + + ); } } diff --git a/src/components/FirecallItems/elements/FirecallItemMarker.tsx b/src/components/FirecallItems/elements/FirecallItemMarker.tsx index 31e2908..eec2c8e 100644 --- a/src/components/FirecallItems/elements/FirecallItemMarker.tsx +++ b/src/components/FirecallItems/elements/FirecallItemMarker.tsx @@ -28,10 +28,11 @@ export class FirecallItemMarker extends FirecallItemBase { // return `${this.beschreibung || ''}`; // } - public body(): string { - return `Marker ${this.name} - ${this.beschreibung}`; - } + // public body(): string { + // return `${this.markerName()} ${this.name} + // ${this.beschreibung} + // position: ${this.lat},${this.lng}`; + // } public dialogText(): ReactNode { return <>Markierung {this.name}; @@ -47,7 +48,7 @@ export class FirecallItemMarker extends FirecallItemBase { // return []; // } - public fieldTypes(): { [fieldName: string]: string } | undefined { + public fieldTypes(): { [fieldName: string]: string } { return {}; } public popupFn(): ReactNode { @@ -70,7 +71,7 @@ export class FirecallItemMarker extends FirecallItemBase { return new FirecallItemMarker(); } - // public renderMarker(selectItem: (item: FirecallItem) => void) { + // public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { // return ( // ); diff --git a/src/components/FirecallItems/elements/FirecallLine.tsx b/src/components/FirecallItems/elements/FirecallLine.tsx new file mode 100644 index 0000000..9e2daad --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallLine.tsx @@ -0,0 +1,38 @@ +import { Line } from '../../firebase/firestore'; +import { FirecallConnection } from './FirecallConnection'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallLine extends FirecallConnection { + opacity?: number; + + public constructor(firecallItem?: Line) { + super(firecallItem); + this.type = 'line'; + if (firecallItem) { + ({ opacity: this.opacity } = firecallItem); + } + this.color = firecallItem?.color || 'green'; + } + + public markerName() { + return 'Linie'; + } + + public static factory(): FirecallItemBase { + return new FirecallLine(); + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + opacity: 'Deckkraft (in Prozent)', + }; + } + + public data(): Line { + return { + ...super.data(), + opacity: this.opacity, + }; + } +} diff --git a/src/components/FirecallItems/elements/FirecallRohr.tsx b/src/components/FirecallItems/elements/FirecallRohr.tsx new file mode 100644 index 0000000..18b9f69 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallRohr.tsx @@ -0,0 +1,109 @@ +import L, { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { FirecallItem, Rohr } from '../../firebase/firestore'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallRohr extends FirecallItemBase { + art: 'C' | 'B' | 'Wasserwerfer' | string = 'C'; + durchfluss?: number; + + public constructor(firecallItem?: Rohr) { + super(firecallItem); + this.type = 'rohr'; + if (firecallItem) { + ({ durchfluss: this.durchfluss, art: this.art } = firecallItem); + } + } + + public markerName() { + return 'Rohr'; + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + durchfluss: 'Durchfluss (l/min)', + rotation: 'Drehung in Grad', + }; + } + + public fieldTypes(): { [fieldName: string]: string } { + return { + rotation: 'number', + durchfluss: 'number', + }; + } + + public data(): FirecallItem { + return { + ...super.data(), + durchfluss: this.durchfluss, + art: this.art, + } as FirecallItem; + } + + public title(): string { + return `${this.art} Rohr ${this.name}`; + } + + public info(): string { + return `${this.durchfluss ? this.durchfluss + ' l/min' : ''}`; + } + + // public body(): string { + // return `${this.markerName()} ${this.name} + // ${this.beschreibung} + // position: ${this.lat},${this.lng}`; + // } + + public dialogText(): ReactNode { + return <>C/B Rohr oder Wasserwerfer; + } + + // public dateFields(): string[] { + // return []; + // } + + public titleFn(): string { + return `${this.name} ${this.art || ''}${ + this.durchfluss ? ` ${this.durchfluss}l/min` : '' + }`; + } + public icon(): Icon { + return L.icon({ + iconUrl: `/icons/rohr${ + ['b', 'c', 'ww', 'wasserwerfer'].indexOf(this.art.toLowerCase()) >= 0 + ? '-' + this.art.toLowerCase() + : '' + }.svg`, + iconSize: [24, 24], + iconAnchor: [12, 12], + popupAnchor: [0, 0], + }); + } + + public static factory(): FirecallItemBase { + return new FirecallRohr(); + } + + public popupFn(): ReactNode { + return ( + <> + + {this.name} {this.art} Rohr + + {this.durchfluss && ( + <> +
+ Durchfluss: {this.durchfluss} l/min + + )} + + ); + } + // public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + // return ( + + // ); + // } +} diff --git a/src/components/FirecallItems/elements/FirecallVehicle.tsx b/src/components/FirecallItems/elements/FirecallVehicle.tsx new file mode 100644 index 0000000..e06d22d --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallVehicle.tsx @@ -0,0 +1,156 @@ +import L, { Icon, IconOptions } from 'leaflet'; +import { ReactNode } from 'react'; +import { formatTimestamp } from '../../../common/time-format'; +import { FirecallItem, Fzg } from '../../firebase/firestore'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallVehicle extends FirecallItemBase { + fw?: string; + besatzung?: string; + ats?: number; + alarmierung?: string; + eintreffen?: string; + abruecken?: string; + + public constructor(firecallItem?: Fzg) { + super(firecallItem); + this.type = 'vehicle'; + if (firecallItem) { + ({ + fw: this.fw, + besatzung: this.besatzung, + ats: this.ats, + alarmierung: this.alarmierung, + eintreffen: this.eintreffen, + abruecken: this.abruecken, + } = firecallItem); + } + } + + public markerName() { + return 'Fahrzeug'; + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + fw: 'Feuerwehr', + besatzung: 'Besatzung 1:?', + ats: 'ATS Träger', + beschreibung: 'Beschreibung', + alarmierung: 'Alarmierung', + eintreffen: 'Eintreffen', + abruecken: 'Abrücken', + rotation: 'Drehung in Grad', + }; + } + + public fieldTypes(): { [fieldName: string]: string } { + return { + rotation: 'number', + ats: 'number', + }; + } + + public data(): FirecallItem { + return { + ...super.data(), + fw: this.fw, + besatzung: this.besatzung, + ats: this.ats, + alarmierung: this.alarmierung, + eintreffen: this.eintreffen, + abruecken: this.abruecken, + } as FirecallItem; + } + + public title(): string { + return `${this.name} ${this.fw}`; + } + + public info(): string { + return `1:${this.besatzung || 0} ATS: ${this.ats || 0}`; + } + + public body(): string { + return `${ + this.alarmierung + ? 'Alarmierung: ' + formatTimestamp(this.alarmierung) + : '' + } + ${this.eintreffen ? ' Eintreffen: ' + formatTimestamp(this.eintreffen) : ''} + ${this.abruecken ? ' Abrücken: ' + formatTimestamp(this.abruecken) : ''} + Position ${this.lat} ${this.lng}`; + } + + public dialogText(): ReactNode { + return <>Einsatzfahrzeug; + } + + public dateFields(): string[] { + return [...super.dateFields(), 'alarmierung', 'eintreffen', 'abruecken']; + } + + public titleFn(): string { + return `${this.name} ${this.fw || ''}`; + } + public icon(): Icon { + return L.icon({ + iconUrl: `/api/fzg?name=${encodeURIComponent( + this.name || '' + )}&fw=${encodeURIComponent(this.fw || '')}`, + iconSize: [45, 20], + iconAnchor: [20, 0], + popupAnchor: [0, 0], + }); + } + + public static factory(): FirecallItemBase { + return new FirecallVehicle(); + } + + public popupFn(): ReactNode { + return ( + <> + + {this.name} {this.fw || ''} + + {this.besatzung && Number.parseInt(this.besatzung) > 0 && ( + <> +
+ Besatzung: 1:{this.besatzung} + + )} + {this.ats !== undefined && this.ats > 0 && ( + <> + {!(this.besatzung && Number.parseInt(this.besatzung) > 0) &&
}{' '} + ({this.ats} ATS) + + )} + {this.alarmierung && ( + <> +
+ Alarmierung: {formatTimestamp(this.alarmierung)} + + )} + {this.eintreffen && ( + <> +
+ Eintreffen: {formatTimestamp(this.eintreffen)} + + )} + {this.abruecken && ( + <> +
+ Abrücken: {formatTimestamp(this.abruecken)} + + )} + + ); + } + // public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + // return ( + + // ); + // } +} diff --git a/src/components/FirecallItems/elements/area/AreaComponent.tsx b/src/components/FirecallItems/elements/area/AreaComponent.tsx new file mode 100644 index 0000000..d66b5af --- /dev/null +++ b/src/components/FirecallItems/elements/area/AreaComponent.tsx @@ -0,0 +1,106 @@ +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import { IconButton } from '@mui/material'; +import L from 'leaflet'; +import { useMemo } from 'react'; +import { Marker, Polygon, Popup } from 'react-leaflet'; +import { LatLngPosition, latLngPosition } from '../../../../common/geo'; +import { useFirecallId } from '../../../../hooks/useFirecall'; +import { FirecallItem } from '../../../firebase/firestore'; +import { FirecallArea } from '../FirecallArea'; +import { + deleteFirecallPosition, + updateFirecallPositions, +} from '../connection/positions'; + +export interface AreaMarkerProps { + record: FirecallArea; + selectItem: (item: FirecallItem) => void; +} + +export default function AreaMarker({ record, selectItem }: AreaMarkerProps) { + const firecallId = useFirecallId(); + + const positions: LatLngPosition[] = useMemo(() => { + let p: LatLngPosition[] = [ + latLngPosition(record.lat, record.lng), + [record.destLat, record.destLng], + ]; + + try { + if (record.positions) { + p = JSON.parse(record.positions); + } + } catch (err) { + console.warn(`unable to parse positions ${err} ${record.positions}`); + } + return p; + }, [ + record.destLat, + record.destLng, + record.lat, + record.lng, + record.positions, + ]); + + return ( + <> + {positions.map((p, index) => ( + { + updateFirecallPositions( + firecallId, + (event.target as L.Marker)?.getLatLng(), + record.data(), + index + ); + }, + }} + > + + selectItem(record)} + > + + + + deleteFirecallPosition(firecallId, record.data(), index) + } + > + + + {record.popupFn()} + + + ))} + + + selectItem(record)} + > + + + {record.popupFn()} + + + + ); +} diff --git a/src/components/FirecallItems/elements/area/areaFunctions.ts b/src/components/FirecallItems/elements/area/areaFunctions.ts new file mode 100644 index 0000000..8be17be --- /dev/null +++ b/src/components/FirecallItems/elements/area/areaFunctions.ts @@ -0,0 +1,54 @@ +import { doc, setDoc } from 'firebase/firestore'; +import L from 'leaflet'; +import { LatLngPosition } from '../../../../common/geo'; +import { + calculateDistance, + getConnectionPositions, +} from '../../../FirecallItems/infos/connection'; +import { firestore } from '../../../firebase/firebase'; +import { Area } from '../../../firebase/firestore'; + +export async function updateFirecallPositions( + firecallId: string, + newPos: L.LatLng, + fcItem: Area, + index: number +) { + // console.info(`drag end on ${JSON.stringify(gisObject)}: ${newPos}`); + if (fcItem.id && newPos && fcItem.positions) { + const positions: LatLngPosition[] = getConnectionPositions(fcItem); + positions.splice(index, 1, [newPos.lat, newPos.lng]); + await updateConnectionInFirestore(firecallId, fcItem, positions); + } +} + +export async function deleteFirecallPosition( + firecallId: string, + fcItem: Area, + index: number +) { + // console.info(`drag end on ${JSON.stringify(gisObject)}: ${newPos}`); + if (fcItem.id && fcItem.positions) { + const positions: LatLngPosition[] = getConnectionPositions(fcItem); + positions.splice(index, 1); + await updateConnectionInFirestore(firecallId, fcItem, positions); + } +} + +const updateConnectionInFirestore = async ( + firecallId: string, + fcItem: Area, + positions: LatLngPosition[] +) => { + if (fcItem.id) + return await setDoc( + doc(firestore, 'call', firecallId, 'item', fcItem.id), + { + positions: JSON.stringify(positions), + distance: Math.round(calculateDistance(positions)), + }, + { + merge: true, + } + ); +}; diff --git a/src/components/FirecallItems/elements/connection/ConnectionComponent.tsx b/src/components/FirecallItems/elements/connection/ConnectionComponent.tsx new file mode 100644 index 0000000..091ac14 --- /dev/null +++ b/src/components/FirecallItems/elements/connection/ConnectionComponent.tsx @@ -0,0 +1,144 @@ +import AddIcon from '@mui/icons-material/Add'; +import DeleteIcon from '@mui/icons-material/Delete'; +import EditIcon from '@mui/icons-material/Edit'; +import { IconButton, Tooltip } from '@mui/material'; +import L from 'leaflet'; +import { useMemo, useState } from 'react'; +import { Marker, Polyline, Popup } from 'react-leaflet'; +import { LatLngPosition, latLngPosition } from '../../../../common/geo'; +import { defaultPosition } from '../../../../hooks/constants'; +import { useFirecallId } from '../../../../hooks/useFirecall'; +import { FirecallItem } from '../../../firebase/firestore'; +import { FirecallConnection } from '../FirecallConnection'; +import { + addFirecallPosition, + deleteFirecallPosition, + findSectionOnPolyline, + updateFirecallPositions, +} from './positions'; + +export interface ConnectionMarkerProps { + record: FirecallConnection; + selectItem: (item: FirecallItem) => void; +} + +export default function ConnectionMarker({ + record, + selectItem, +}: ConnectionMarkerProps) { + const firecallId = useFirecallId(); + const [point, setPoint] = useState(defaultPosition); + const [pointIndex, setPointIndex] = useState(-1); + + const positions: LatLngPosition[] = useMemo(() => { + let p: LatLngPosition[] = [ + latLngPosition(record.lat, record.lng), + [record.destLat, record.destLng], + ]; + + try { + if (record.positions) { + p = JSON.parse(record.positions); + } + } catch (err) { + console.warn(`unable to parse positions ${err} ${record.positions}`); + } + return p; + }, [ + record.destLat, + record.destLng, + record.lat, + record.lng, + record.positions, + ]); + + return ( + <> + {positions.map((p, index) => ( + { + updateFirecallPositions( + firecallId, + (event.target as L.Marker)?.getLatLng(), + record.data(), + index + ); + }, + }} + > + + + selectItem(record)} + > + + + + + + deleteFirecallPosition(firecallId, record, index) + } + > + + + + {record.popupFn()} +
+ Punkt {index} von {positions.length} +
+
+ ))} + { + const index = findSectionOnPolyline(positions, event.latlng); + // console.info( + // `clicked on polyline ${event.latlng} index in points: ${index}` + // ); + setPoint(event.latlng); + setPointIndex(index); + }, + }} + > + + {pointIndex >= 0 && ( + + + addFirecallPosition(firecallId, point, record, pointIndex) + } + > + + + + )} + selectItem(record)} + > + + + + {record.popupFn()} + + + + ); +} diff --git a/src/components/FirecallItems/elements/connection/distance.ts b/src/components/FirecallItems/elements/connection/distance.ts new file mode 100644 index 0000000..5e1c5a0 --- /dev/null +++ b/src/components/FirecallItems/elements/connection/distance.ts @@ -0,0 +1,36 @@ +import { latLngPosition, LatLngPosition } from '../../../../common/geo'; +import { toLatLng } from '../../../../hooks/constants'; +import { Connection } from '../../../firebase/firestore'; + +export const getConnectionPositions = ( + record: Connection +): LatLngPosition[] => { + let p: LatLngPosition[] = [ + latLngPosition(record.lat, record.lng), + [record.destLat, record.destLng], + ]; + + try { + if (record.positions) { + p = JSON.parse(record.positions); + } + } catch (err) { + console.warn(`unable to parse positions ${err} ${record.positions}`); + } + return p; +}; + +export const calculateDistance = (positions: LatLngPosition[]): number => { + let distance = 0; + positions.forEach((p, index) => { + if (index > 0) { + distance += toLatLng(p[0], p[1]).distanceTo( + toLatLng(positions[index - 1][0], positions[index - 1][1]) + ); + } + }); + return distance; +}; + +export const calculateDistanceForConnection = (record: Connection) => + Math.round(calculateDistance(getConnectionPositions(record))); diff --git a/src/components/FirecallItems/elements/connection/positions.ts b/src/components/FirecallItems/elements/connection/positions.ts new file mode 100644 index 0000000..a089954 --- /dev/null +++ b/src/components/FirecallItems/elements/connection/positions.ts @@ -0,0 +1,88 @@ +import { doc, setDoc } from 'firebase/firestore'; +import L from 'leaflet'; +import GeometryUtil from 'leaflet-geometryutil'; +import { LatLngPosition } from '../../../../common/geo'; +import { firestore } from '../../../firebase/firebase'; +import { Connection } from '../../../firebase/firestore'; +import { calculateDistance, getConnectionPositions } from './distance'; + +export async function updateFirecallPositions( + firecallId: string, + newPos: L.LatLng, + fcItem: Connection, + index: number +) { + // console.info(`drag end on ${JSON.stringify(gisObject)}: ${newPos}`); + if (fcItem.id && newPos && fcItem.positions) { + const positions: LatLngPosition[] = getConnectionPositions(fcItem); + positions.splice(index, 1, [newPos.lat, newPos.lng]); + await updateConnectionInFirestore(firecallId, fcItem, positions); + } +} +export async function addFirecallPosition( + firecallId: string, + newPos: L.LatLng, + fcItem: Connection, + index: number +) { + // console.info(`drag end on ${JSON.stringify(gisObject)}: ${newPos}`); + if (fcItem.id && newPos && fcItem.positions) { + const positions: LatLngPosition[] = getConnectionPositions(fcItem); + positions.splice(index, 0, [newPos.lat, newPos.lng]); + await updateConnectionInFirestore(firecallId, fcItem, positions); + } +} + +export async function deleteFirecallPosition( + firecallId: string, + fcItem: Connection, + index: number +) { + // console.info(`drag end on ${JSON.stringify(gisObject)}: ${newPos}`); + if (fcItem.id && fcItem.positions) { + const positions: LatLngPosition[] = getConnectionPositions(fcItem); + positions.splice(index, 1); + await updateConnectionInFirestore(firecallId, fcItem, positions); + } +} + +export function findSectionOnPolyline( + positions: LatLngPosition[], + point: L.LatLng +) { + for (let i = 1; i < positions.length; i++) { + const belongsToSection = GeometryUtil.belongsSegment( + point, + new L.LatLng(positions[i - 1][0], positions[i - 1][1]), + new L.LatLng(positions[i][0], positions[i][1]) + ); + if (belongsToSection) { + console.info( + `click point ${point} belongs to section ${positions[i - 1]}-${ + positions[i] + } ${i - 1}-${i}` + ); + return i; + } + } + + return -1; +} + +const updateConnectionInFirestore = async ( + firecallId: string, + fcItem: Connection, + positions: LatLngPosition[] +) => { + if (fcItem.id) + return await setDoc( + doc(firestore, 'call', firecallId, 'item', fcItem.id), + { + positions: JSON.stringify(positions), + distance: Math.round(calculateDistance(positions)), + }, + { + merge: true, + } + ); +}; diff --git a/src/components/FirecallItems/elements/index.tsx b/src/components/FirecallItems/elements/index.tsx new file mode 100644 index 0000000..1115df6 --- /dev/null +++ b/src/components/FirecallItems/elements/index.tsx @@ -0,0 +1,63 @@ +import { + Area, + Circle, + Connection, + FirecallItem, + Fzg, + Line, + Rohr, +} from '../../firebase/firestore'; +import { CircleMarker } from './CircleMarker'; +import { FirecallArea } from './FirecallArea'; +import { FirecallAssp } from './FirecallAssp'; +import { FirecallConnection } from './FirecallConnection'; +import { FirecallEinsatzleitung } from './FirecallEl'; +import { FirecallItemBase } from './FirecallItemBase'; +import { FirecallItemMarker } from './FirecallItemMarker'; +import { FirecallLine } from './FirecallLine'; +import { FirecallRohr } from './FirecallRohr'; +import { FirecallVehicle } from './FirecallVehicle'; + +export const fcItemClasses: { [key: string]: typeof FirecallItemBase } = { + fallback: FirecallItemBase, + marker: FirecallItemMarker, + connection: FirecallConnection, + circle: CircleMarker, + area: FirecallArea, + assp: FirecallAssp, + el: FirecallEinsatzleitung, + line: FirecallLine, + rohr: FirecallRohr, + vehicle: FirecallVehicle, +}; + +export const fcItemNames: { [key: string]: string } = {}; + +Object.entries(fcItemClasses).forEach(([k, FcClass]) => { + fcItemNames[k] = new FcClass().markerName(); +}); + +export function getItemClass(record?: FirecallItem) { + switch (record?.type) { + case 'marker': + return new FirecallItemMarker(record); + case 'connection': + return new FirecallConnection(record as Connection); + case 'circle': + return new CircleMarker(record as Circle); + case 'area': + return new FirecallArea(record as Area); + case 'assp': + return new FirecallAssp(record); + case 'el': + return new FirecallEinsatzleitung(record); + case 'line': + return new FirecallLine(record as Line); + case 'rohr': + return new FirecallRohr(record as Rohr); + case 'vehicle': + return new FirecallVehicle(record as Fzg); + default: + return new FirecallItemBase(record); + } +} diff --git a/src/components/FirecallItems/elements/FirecallItemDefault.tsx b/src/components/FirecallItems/elements/marker/FirecallItemDefault.tsx similarity index 83% rename from src/components/FirecallItems/elements/FirecallItemDefault.tsx rename to src/components/FirecallItems/elements/marker/FirecallItemDefault.tsx index eba68f1..3997b18 100644 --- a/src/components/FirecallItems/elements/FirecallItemDefault.tsx +++ b/src/components/FirecallItems/elements/marker/FirecallItemDefault.tsx @@ -1,12 +1,12 @@ import { doc, setDoc } from 'firebase/firestore'; import L from 'leaflet'; import { useEffect, useState } from 'react'; -import { defaultPosition } from '../../../hooks/constants'; -import { useFirecallId } from '../../../hooks/useFirecall'; -import { RotatedMarker } from '../../Map/markers/RotatedMarker'; -import { firestore } from '../../firebase/firebase'; -import { FirecallItem } from '../../firebase/firestore'; -import { FirecallItemBase } from './FirecallItemBase'; +import { defaultPosition } from '../../../../hooks/constants'; +import { useFirecallId } from '../../../../hooks/useFirecall'; +import { RotatedMarker } from '../../../Map/markers/RotatedMarker'; +import { firestore } from '../../../firebase/firebase'; +import { FirecallItem } from '../../../firebase/firestore'; +import { FirecallItemBase } from '../FirecallItemBase'; export interface FirecallItemMarkerProps { record: FirecallItemBase; diff --git a/src/components/Map/markers/FirecallItems.tsx b/src/components/Map/markers/FirecallItems.tsx index 19e4979..bbef231 100644 --- a/src/components/Map/markers/FirecallItems.tsx +++ b/src/components/Map/markers/FirecallItems.tsx @@ -1,9 +1,10 @@ +import { useState } from 'react'; import useFirebaseCollection from '../../../hooks/useFirebaseCollection'; import useFirecall from '../../../hooks/useFirecall'; import { filterDisplayableItems, FirecallItem } from '../../firebase/firestore'; +import { getItemClass } from '../../FirecallItems/elements'; import ItemOverlay from './ItemOverlay'; -import FirecallItemMarker from './FirecallItemMarker'; -import { useEffect, useState } from 'react'; +import React from 'react'; export default function FirecallItems() { const firecall = useFirecall(); @@ -17,13 +18,18 @@ export default function FirecallItems() { return ( <> - {records.map((record) => ( - - ))} + {records.map( + (record) => ( + + {getItemClass(record).renderMarker(setFirecallItem)} + + ) + // + )} {firecallItem && ( diff --git a/src/hooks/useFirecallItemUpdate.ts b/src/hooks/useFirecallItemUpdate.ts index cac9642..570273a 100644 --- a/src/hooks/useFirecallItemUpdate.ts +++ b/src/hooks/useFirecallItemUpdate.ts @@ -8,18 +8,25 @@ export default function useFirecallItemUpdate(firecallId: string = 'unknown') { const { email } = useFirebaseLogin(); return useCallback( async (item: FirecallItem) => { + const newData: any = { + datum: new Date().toISOString(), + ...Object.entries(item) + .filter(([k, v]) => v) + .reduce((p, [k, v]) => { + p[k] = v; + return p; + }, {} as any), + updatedAt: new Date(), + updatedBy: email, + }; console.info( `update of firecall item ${item.id}: ${JSON.stringify(item)}` ); + await setDoc( doc(firestore, 'call', firecallId, 'item', '' + item.id), - { - datum: new Date().toISOString(), - ...item, - updatedAt: new Date(), - updatedBy: email, - }, - { merge: true } + newData, + { merge: false } ); }, [email, firecallId] From 67fcdfef35dcaa23deb44ad1c50d57b7be0ace13 Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Sat, 23 Dec 2023 11:49:43 +0100 Subject: [PATCH 06/11] Add login page before rendering any other pages --- src/components/pages/LoginUi.tsx | 2 +- src/pages/_app.tsx | 92 ++++++++++++++++++++++---------- 2 files changed, 64 insertions(+), 30 deletions(-) diff --git a/src/components/pages/LoginUi.tsx b/src/components/pages/LoginUi.tsx index ea426f8..9795f38 100644 --- a/src/components/pages/LoginUi.tsx +++ b/src/components/pages/LoginUi.tsx @@ -5,7 +5,7 @@ import OneTapLogin from '../auth/OneTapLogin'; import StyledLoginButton from '../firebase/StyledLogin'; import { auth } from '../firebase/firebase'; -export default function Login() { +export default function LoginUi() { const { isSignedIn, isAuthorized, displayName, email } = useFirebaseLogin(); return ( diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 3394970..59bbc97 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -9,40 +9,74 @@ import HeaderBar from '../components/site/HeaderBar'; import SingedOutOneTapLogin from '../components/auth/SingedOutOneTapLogin'; import '../styles/globals.css'; import styles from '../styles/Home.module.css'; +import useFirebaseLogin from '../hooks/useFirebaseLogin'; -function MyApp({ Component, pageProps }: AppProps) { +import dynamic from 'next/dynamic'; + +const DynamicLogin = dynamic( + () => { + return import('../components/pages/LoginUi'); + }, + { ssr: false } +); + +function LogedinApp({ Component, pageProps }: AppProps) { const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); + return ( + + + + + + + + + ); +} +function AuthorizationApp({ Component, pageProps, router }: AppProps) { + const { isSignedIn, isAuthorized, displayName, email } = useFirebaseLogin(); + const [isDrawerOpen, setIsDrawerOpen] = React.useState(false); + if (isAuthorized) { + return ( + + ); + } + return ( + <> + + ; + + ); +} + +function MyApp({ Component, pageProps, router }: AppProps) { return ( - -
- - Hydrantenkarte - - - - - - - - - - - - - -
-
+ + Hydrantenkarte + + + + +
+ + + +
); } From 323f1d7636b49f6d06be05a180e9fb6fc99e1635 Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Sat, 23 Dec 2023 11:53:41 +0100 Subject: [PATCH 07/11] Add about to login page --- src/components/pages/LoginUi.tsx | 16 +++++++++------- src/pages/_app.tsx | 4 +++- 2 files changed, 12 insertions(+), 8 deletions(-) diff --git a/src/components/pages/LoginUi.tsx b/src/components/pages/LoginUi.tsx index 9795f38..6a598b4 100644 --- a/src/components/pages/LoginUi.tsx +++ b/src/components/pages/LoginUi.tsx @@ -1,4 +1,4 @@ -import { Button, Typography } from '@mui/material'; +import { Button, Paper, Typography } from '@mui/material'; import Link from 'next/link'; import useFirebaseLogin from '../../hooks/useFirebaseLogin'; import OneTapLogin from '../auth/OneTapLogin'; @@ -12,12 +12,14 @@ export default function LoginUi() { <> {!isSignedIn && ( <> - - Für die Nutzung der Hydrantenkarte ist eine Anmeldung und manuelle - Freischaltung erforderlich. Bitte registriere dich hier. - - - + + + Für die Nutzung der Hydrantenkarte ist eine Anmeldung und manuelle + Freischaltung erforderlich. Bitte registriere dich hier. + + + + )} diff --git a/src/pages/_app.tsx b/src/pages/_app.tsx index 59bbc97..445009f 100644 --- a/src/pages/_app.tsx +++ b/src/pages/_app.tsx @@ -12,6 +12,7 @@ import styles from '../styles/Home.module.css'; import useFirebaseLogin from '../hooks/useFirebaseLogin'; import dynamic from 'next/dynamic'; +import About from './about'; const DynamicLogin = dynamic( () => { @@ -30,7 +31,6 @@ function LogedinApp({ Component, pageProps }: AppProps) { isDrawerOpen={isDrawerOpen} setIsDrawerOpen={setIsDrawerOpen} /> - @@ -52,6 +52,7 @@ function AuthorizationApp({ Component, pageProps, router }: AppProps) { setIsDrawerOpen={setIsDrawerOpen} /> ; + ); } @@ -70,6 +71,7 @@ function MyApp({ Component, pageProps, router }: AppProps) {
+ Date: Sat, 23 Dec 2023 20:29:47 +0100 Subject: [PATCH 08/11] Use a page to switch firecalls --- src/components/pages/Einsaetze.tsx | 3 +++ src/pages/einsaetze.tsx | 4 ++-- src/pages/einsatz/[firecallId].tsx | 26 ++++++++++++++++++++++++++ 3 files changed, 31 insertions(+), 2 deletions(-) create mode 100644 src/pages/einsatz/[firecallId].tsx diff --git a/src/components/pages/Einsaetze.tsx b/src/components/pages/Einsaetze.tsx index ab1a8eb..a0ec339 100644 --- a/src/components/pages/Einsaetze.tsx +++ b/src/components/pages/Einsaetze.tsx @@ -18,6 +18,7 @@ import { useFirecallId, useFirecallSelect } from '../../hooks/useFirecall'; import ConfirmDialog from '../dialogs/ConfirmDialog'; import { firestore } from '../firebase/firebase'; import EinsatzDialog from '../FirecallItems/EinsatzDialog'; +import { useRouter } from 'next/router'; function useFirecallUpdate() { const { email } = useFirebaseLogin(); @@ -48,6 +49,7 @@ function EinsatzCard({ const updateFirecall = useFirecallUpdate(); const { isAdmin } = useFirebaseLogin(); const setFirecallId = useFirecallSelect(); + const router = useRouter(); const updateFn = useCallback( (fzg?: Firecall) => { @@ -89,6 +91,7 @@ function EinsatzCard({ if (setFirecallId) { setFirecallId(einsatz.id); } + router.push(`/einsatz/${einsatz.id}`); }} > Aktivieren diff --git a/src/pages/einsaetze.tsx b/src/pages/einsaetze.tsx index 4ce934f..a6fe48b 100644 --- a/src/pages/einsaetze.tsx +++ b/src/pages/einsaetze.tsx @@ -1,7 +1,7 @@ import type { NextPage } from 'next'; import dynamic from 'next/dynamic'; -const DynamicFahrzeuge = dynamic( +const DynamicEinsatz = dynamic( () => { return import('../components/pages/Einsaetze'); }, @@ -9,7 +9,7 @@ const DynamicFahrzeuge = dynamic( ); const Home: NextPage = () => { - return ; + return ; }; export default Home; diff --git a/src/pages/einsatz/[firecallId].tsx b/src/pages/einsatz/[firecallId].tsx new file mode 100644 index 0000000..d89649f --- /dev/null +++ b/src/pages/einsatz/[firecallId].tsx @@ -0,0 +1,26 @@ +import { useRouter } from 'next/router'; + +import { useEffect } from 'react'; +import { useFirecallSelect } from '../../hooks/useFirecall'; + +import dynamic from 'next/dynamic'; + +const DynamicMap = dynamic( + () => { + return import('../../components/Map/PositionedMap'); + }, + { ssr: false } +); + +export default function EinsatzPage() { + const router = useRouter(); + const setFirecallId = useFirecallSelect(); + + useEffect(() => { + if (router.query.firecallId && setFirecallId) { + setFirecallId('' + router.query.firecallId); + } + }, [router.query.firecallId, setFirecallId]); + + return ; +} From 8674829bf9d47eb3062f446f949f496e78ed8b3f Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Sat, 23 Dec 2023 22:53:42 +0100 Subject: [PATCH 09/11] Add diary --- .../FirecallItems/FirecallItemDialog.tsx | 2 +- .../FirecallItems/elements/FirecallDiary.tsx | 72 +++++++++++++++++++ .../elements/FirecallItemBase.tsx | 6 ++ .../elements/FirecallItemMarker.tsx | 51 ++++++------- .../FirecallItems/elements/index.tsx | 24 ++++--- src/components/FirecallItems/infos/marker.tsx | 4 +- src/components/firebase/firestore.ts | 3 +- src/components/pages/EinsatzTagebuch.tsx | 28 ++++---- 8 files changed, 134 insertions(+), 56 deletions(-) create mode 100644 src/components/FirecallItems/elements/FirecallDiary.tsx diff --git a/src/components/FirecallItems/FirecallItemDialog.tsx b/src/components/FirecallItems/FirecallItemDialog.tsx index 2e74a20..304e9b6 100644 --- a/src/components/FirecallItems/FirecallItemDialog.tsx +++ b/src/components/FirecallItems/FirecallItemDialog.tsx @@ -162,7 +162,7 @@ export default function FirecallItemDialog({ color="primary" onClick={() => { setOpen(false); - onClose(item); + onClose(item.filteredData()); }} > {item.id ? 'Aktualisieren' : 'Hinzufügen'} diff --git a/src/components/FirecallItems/elements/FirecallDiary.tsx b/src/components/FirecallItems/elements/FirecallDiary.tsx new file mode 100644 index 0000000..3e37b9e --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallDiary.tsx @@ -0,0 +1,72 @@ +import { ReactNode } from 'react'; +import { Diary, FirecallItem } from '../../firebase/firestore'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallDiary extends FirecallItemBase { + von: string; + an: string; + erledigt: string; + + public constructor(firecallItem?: Diary) { + super(firecallItem); + this.type = 'diary'; + this.von = firecallItem?.von ?? ''; + this.an = firecallItem?.an ?? ''; + this.erledigt = firecallItem?.erledigt ?? ''; + } + + public data(): Diary { + return { + ...super.data(), + von: this.von, + an: this.an, + erledigt: this.erledigt, + } as Diary; + } + + public markerName() { + return 'Einsatztagebuch'; + } + + public dialogText(): ReactNode { + return <>Einsatztagebuch {this.name}; + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + von: 'Meldung von', + an: 'Meldung an', + erledigt: 'Erledigt', + }; + } + + public fieldTypes(): { [fieldName: string]: string } { + return {}; + } + + public dateFields(): string[] { + return ['datum', 'erledigt']; + } + + public popupFn(): ReactNode { + return ( + <> + {this.name} +
+ {this.beschreibung || ''} + + ); + } + public titleFn(): string { + return `${this.markerName()} ${this.name}\n${this.beschreibung || ''}`; + } + + public static factory(): FirecallItemBase { + return new FirecallDiary(); + } + + public renderMarker(selectItem: (item: FirecallItem) => void): ReactNode { + return <>; + } +} diff --git a/src/components/FirecallItems/elements/FirecallItemBase.tsx b/src/components/FirecallItems/elements/FirecallItemBase.tsx index a62a18d..47f23c0 100644 --- a/src/components/FirecallItems/elements/FirecallItemBase.tsx +++ b/src/components/FirecallItems/elements/FirecallItemBase.tsx @@ -75,6 +75,12 @@ export class FirecallItemBase { }; } + public filteredData(): FirecallItem { + return Object.fromEntries( + Object.entries(this.data()).filter(([key, value]) => value) + ) as FirecallItem; + } + public markerName() { return 'Firecallitem'; } diff --git a/src/components/FirecallItems/elements/FirecallItemMarker.tsx b/src/components/FirecallItems/elements/FirecallItemMarker.tsx index eec2c8e..d5fca77 100644 --- a/src/components/FirecallItems/elements/FirecallItemMarker.tsx +++ b/src/components/FirecallItems/elements/FirecallItemMarker.tsx @@ -1,52 +1,38 @@ -import { Icon, IconOptions } from 'leaflet'; +import L, { IconOptions, Icon as LeafletIcon } from 'leaflet'; import { ReactNode } from 'react'; -import { FirecallItem } from '../../firebase/firestore'; +import { FcMarker } from '../../firebase/firestore'; import { markerIcon } from '../icons'; import { FirecallItemBase } from './FirecallItemBase'; export class FirecallItemMarker extends FirecallItemBase { - public constructor(firecallItem?: FirecallItem) { + iconUrl: string; + public constructor(firecallItem?: FcMarker) { super(firecallItem); this.type = 'marker'; + this.iconUrl = firecallItem?.iconUrl || ''; } - public data(): FirecallItem { + public data(): FcMarker { return { ...super.data(), - } as FirecallItem; + iconUrl: this.iconUrl, + } as FcMarker; } public markerName() { return 'Marker'; } - // public title(): string { - // return `Marker ${this.name}`; - // } - - // public info(): string { - // return `${this.beschreibung || ''}`; - // } - - // public body(): string { - // return `${this.markerName()} ${this.name} - // ${this.beschreibung} - // position: ${this.lat},${this.lng}`; - // } - public dialogText(): ReactNode { return <>Markierung {this.name}; } - // public fields(): { [fieldName: string]: string } { - // return { - // ...super.fields(), - // }; - // } - - // public dateFields(): string[] { - // return []; - // } + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + iconUrl: 'Icon URL', + }; + } public fieldTypes(): { [fieldName: string]: string } { return {}; @@ -63,7 +49,14 @@ export class FirecallItemMarker extends FirecallItemBase { public titleFn(): string { return `${this.name}\n${this.beschreibung || ''}`; } - public icon(): Icon { + public icon(): LeafletIcon { + if (this.iconUrl) { + return L.icon({ + iconUrl: this.iconUrl, + iconSize: [24, 24], + }); + } + return markerIcon; } diff --git a/src/components/FirecallItems/elements/index.tsx b/src/components/FirecallItems/elements/index.tsx index 1115df6..5f1486b 100644 --- a/src/components/FirecallItems/elements/index.tsx +++ b/src/components/FirecallItems/elements/index.tsx @@ -2,15 +2,18 @@ import { Area, Circle, Connection, + Diary, FirecallItem, Fzg, Line, Rohr, + FcMarker, } from '../../firebase/firestore'; import { CircleMarker } from './CircleMarker'; import { FirecallArea } from './FirecallArea'; import { FirecallAssp } from './FirecallAssp'; import { FirecallConnection } from './FirecallConnection'; +import { FirecallDiary } from './FirecallDiary'; import { FirecallEinsatzleitung } from './FirecallEl'; import { FirecallItemBase } from './FirecallItemBase'; import { FirecallItemMarker } from './FirecallItemMarker'; @@ -21,13 +24,14 @@ import { FirecallVehicle } from './FirecallVehicle'; export const fcItemClasses: { [key: string]: typeof FirecallItemBase } = { fallback: FirecallItemBase, marker: FirecallItemMarker, + rohr: FirecallRohr, connection: FirecallConnection, + diary: FirecallDiary, + line: FirecallLine, circle: CircleMarker, area: FirecallArea, assp: FirecallAssp, el: FirecallEinsatzleitung, - line: FirecallLine, - rohr: FirecallRohr, vehicle: FirecallVehicle, }; @@ -40,23 +44,25 @@ Object.entries(fcItemClasses).forEach(([k, FcClass]) => { export function getItemClass(record?: FirecallItem) { switch (record?.type) { case 'marker': - return new FirecallItemMarker(record); + return new FirecallItemMarker(record as FcMarker); + case 'rohr': + return new FirecallRohr(record as Rohr); case 'connection': return new FirecallConnection(record as Connection); + case 'diary': + return new FirecallDiary(record as Diary); + case 'line': + return new FirecallLine(record as Line); case 'circle': return new CircleMarker(record as Circle); case 'area': return new FirecallArea(record as Area); + case 'vehicle': + return new FirecallVehicle(record as Fzg); case 'assp': return new FirecallAssp(record); case 'el': return new FirecallEinsatzleitung(record); - case 'line': - return new FirecallLine(record as Line); - case 'rohr': - return new FirecallRohr(record as Rohr); - case 'vehicle': - return new FirecallVehicle(record as Fzg); default: return new FirecallItemBase(record); } diff --git a/src/components/FirecallItems/infos/marker.tsx b/src/components/FirecallItems/infos/marker.tsx index fe535e6..7e8b4b4 100644 --- a/src/components/FirecallItems/infos/marker.tsx +++ b/src/components/FirecallItems/infos/marker.tsx @@ -1,9 +1,9 @@ -import { FirecallItem, FirecallItemMarker } from '../../firebase/firestore'; +import { FirecallItem, FcMarker } from '../../firebase/firestore'; import { markerIcon } from '../icons'; import { FirecallItemInfo } from './types'; import L from 'leaflet'; -export const markerInfo: FirecallItemInfo = { +export const markerInfo: FirecallItemInfo = { name: 'Marker', title: (item) => `${item.name || ''}`, info: (item) => ``, diff --git a/src/components/firebase/firestore.ts b/src/components/firebase/firestore.ts index f4af5ec..3b4be5a 100644 --- a/src/components/firebase/firestore.ts +++ b/src/components/firebase/firestore.ts @@ -12,8 +12,9 @@ export interface FirecallItem { rotation?: string; } -export interface FirecallItemMarker extends FirecallItem { +export interface FcMarker extends FirecallItem { type: 'marker'; + iconUrl?: string; } export interface Fzg extends FirecallItem { diff --git a/src/components/pages/EinsatzTagebuch.tsx b/src/components/pages/EinsatzTagebuch.tsx index e9044ef..53439db 100644 --- a/src/components/pages/EinsatzTagebuch.tsx +++ b/src/components/pages/EinsatzTagebuch.tsx @@ -93,20 +93,20 @@ export function useDiaries() { original: item, } as Diary) ), - firecallItems - .filter( - (item: FirecallItem) => - ['vehicle', 'diary'].indexOf(item.type) < 0 && item.datum - ) - .map( - (item: FirecallItem) => - ({ - ...item, - type: 'diary', - editable: true, - original: item, - } as Diary) - ), + // firecallItems + // .filter( + // (item: FirecallItem) => + // ['vehicle', 'diary'].indexOf(item.type) < 0 && item.datum + // ) + // .map( + // (item: FirecallItem) => + // ({ + // ...item, + // type: 'diary', + // editable: true, + // original: item, + // } as Diary) + // ), firecallItems .filter((item: FirecallItem) => item.type === 'diary') .map( From 482f67d924cb56c7d86469ac976e146652a50c52 Mon Sep 17 00:00:00 2001 From: Paul Woelfel Date: Sat, 23 Dec 2023 23:04:33 +0100 Subject: [PATCH 10/11] Add progress --- src/components/pages/Schadstoff.tsx | 4 +++- src/hooks/useHazmatDb.ts | 10 ++++++++-- 2 files changed, 11 insertions(+), 3 deletions(-) diff --git a/src/components/pages/Schadstoff.tsx b/src/components/pages/Schadstoff.tsx index 4ad44f8..ea46421 100644 --- a/src/components/pages/Schadstoff.tsx +++ b/src/components/pages/Schadstoff.tsx @@ -7,11 +7,12 @@ import TextField from '@mui/material/TextField'; import Typography from '@mui/material/Typography'; import { useCallback, useState } from 'react'; import useHazmatDb from '../../hooks/useHazmatDb'; +import CircularProgress from '@mui/material/CircularProgress'; export default function SchadstoffPage() { const [unNumber, setUnNumber] = useState(''); const [materialName, setMaterialName] = useState(''); - const hazmatRecords = useHazmatDb(unNumber, materialName); + const [hazmatRecords, isInProgress] = useHazmatDb(unNumber, materialName); const openEricards = useCallback((num: string, nam: string) => { let form = document.createElement('form'); @@ -71,6 +72,7 @@ export default function SchadstoffPage() { }} value={materialName} /> + {isInProgress && }