From de9751510902ab1fedf03e4dab0269a65684b834 Mon Sep 17 00:00:00 2001 From: Paul Date: Sat, 23 Dec 2023 23:30:02 +0100 Subject: [PATCH] Refactor firecallitem (#13) * Starting point for new firecallitem base class * Extend baseclass * Dockerfile and devcontainer * Update rendering * Rewerite components * Add login page before rendering any other pages * Add about to login page * Use a page to switch firecalls * Add diary * Add progress * Add polyline check --- .devcontainer/devcontainer.json | 22 +++ Dockerfile | 2 +- .../FirecallItems/FirecallItemDialog.tsx | 100 ++++++----- .../FirecallItems/elements/CircleMarker.tsx | 112 +++++++++++++ .../FirecallItems/elements/FirecallArea.tsx | 101 ++++++++++++ .../FirecallItems/elements/FirecallAssp.tsx | 79 +++++++++ .../elements/FirecallConnection.tsx | 116 +++++++++++++ .../FirecallItems/elements/FirecallDiary.tsx | 72 ++++++++ .../FirecallItems/elements/FirecallEl.tsx | 80 +++++++++ .../elements/FirecallItemBase.tsx | 155 +++++++++++++++++ .../elements/FirecallItemMarker.tsx | 72 ++++++++ .../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 | 69 ++++++++ .../elements/marker/FirecallItemDefault.tsx | 84 ++++++++++ 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/FirecallItems/infos/marker.tsx | 4 +- src/components/Map/Leitungen/Draw.tsx | 4 +- src/components/Map/MapActionButtons.tsx | 3 +- src/components/Map/markers/FirecallItems.tsx | 24 ++- src/components/firebase/firestore.ts | 13 +- src/components/pages/Einsaetze.tsx | 3 + src/components/pages/EinsatzTagebuch.tsx | 28 ++-- src/components/pages/LoginUi.tsx | 18 +- src/components/pages/Schadstoff.tsx | 4 +- src/hooks/useFirecallItemUpdate.ts | 21 ++- src/hooks/useHazmatDb.ts | 10 +- src/pages/_app.tsx | 94 +++++++---- src/pages/einsaetze.tsx | 4 +- src/pages/einsatz/[firecallId].tsx | 26 +++ 40 files changed, 1937 insertions(+), 144 deletions(-) create mode 100644 .devcontainer/devcontainer.json 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/FirecallAssp.tsx create mode 100644 src/components/FirecallItems/elements/FirecallConnection.tsx create mode 100644 src/components/FirecallItems/elements/FirecallDiary.tsx create mode 100644 src/components/FirecallItems/elements/FirecallEl.tsx create mode 100644 src/components/FirecallItems/elements/FirecallItemBase.tsx create mode 100644 src/components/FirecallItems/elements/FirecallItemMarker.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 create mode 100644 src/components/FirecallItems/elements/marker/FirecallItemDefault.tsx create mode 100644 src/pages/einsatz/[firecallId].tsx 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/FirecallItemDialog.tsx b/src/components/FirecallItems/FirecallItemDialog.tsx index 0eb0eaa..304e9b6 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' && ( + + )} ))} @@ -146,7 +162,7 @@ export default function FirecallItemDialog({ color="primary" onClick={() => { setOpen(false); - onClose(item); + onClose(item.filteredData()); }} > {item.id ? 'Aktualisieren' : 'Hinzufügen'} @@ -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 new file mode 100644 index 0000000..c9f2bf9 --- /dev/null +++ b/src/components/FirecallItems/elements/CircleMarker.tsx @@ -0,0 +1,112 @@ +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; + 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 { + return { + ...super.data(), + color: this.color, + radius: this.radius, + opacity: this.opacity, + fill: this.fill, + } as Circle; + } + + public markerName(): string { + return `Kreis`; + } + + public title(): string { + return `${this.markerName()} ${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.
+ {this.name || ''} + + ); + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + radius: 'Radius (m)', + color: 'Farbe (HTML bzw. Englisch)', + fill: 'Kreis ausfüllen', + opacity: 'Deckkraft (in Prozent)', + }; + } + + public dateFields(): string[] { + return []; + } + + public fieldTypes(): { [fieldName: string]: string } { + return { + fill: 'boolean', + }; + } + 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..f97aac5 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallArea.tsx @@ -0,0 +1,101 @@ +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'; +import AreaMarker from './area/AreaComponent'; + +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(): Area { + 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 } { + return { + opacity: 'number', + }; + } + 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: (item: FirecallItem) => void): ReactNode { + return ; + } + + public static isPolyline(): boolean { + return true; + } +} 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..fdcd252 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallConnection.tsx @@ -0,0 +1,116 @@ +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 ( + + ); + } + + public static isPolyline(): boolean { + return true; + } +} 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/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 new file mode 100644 index 0000000..c1a61d4 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallItemBase.tsx @@ -0,0 +1,155 @@ +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 './marker/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(); + this.rotation = firecallItem?.rotation || '0'; + } + + 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 filteredData(): FirecallItem { + return Object.fromEntries( + Object.entries(this.data()).filter(([key, value]) => value) + ) as FirecallItem; + } + + 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 ['datum']; + } + + public fieldTypes(): { [fieldName: string]: string } { + 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 ( + + ); + } + + public static isPolyline(): boolean { + return false; + } +} diff --git a/src/components/FirecallItems/elements/FirecallItemMarker.tsx b/src/components/FirecallItems/elements/FirecallItemMarker.tsx new file mode 100644 index 0000000..d5fca77 --- /dev/null +++ b/src/components/FirecallItems/elements/FirecallItemMarker.tsx @@ -0,0 +1,72 @@ +import L, { IconOptions, Icon as LeafletIcon } from 'leaflet'; +import { ReactNode } from 'react'; +import { FcMarker } from '../../firebase/firestore'; +import { markerIcon } from '../icons'; +import { FirecallItemBase } from './FirecallItemBase'; + +export class FirecallItemMarker extends FirecallItemBase { + iconUrl: string; + public constructor(firecallItem?: FcMarker) { + super(firecallItem); + this.type = 'marker'; + this.iconUrl = firecallItem?.iconUrl || ''; + } + + public data(): FcMarker { + return { + ...super.data(), + iconUrl: this.iconUrl, + } as FcMarker; + } + + public markerName() { + return 'Marker'; + } + + public dialogText(): ReactNode { + return <>Markierung {this.name}; + } + + public fields(): { [fieldName: string]: string } { + return { + ...super.fields(), + iconUrl: 'Icon URL', + }; + } + + public fieldTypes(): { [fieldName: string]: string } { + return {}; + } + public popupFn(): ReactNode { + return ( + <> + {this.name} +
+ {this.beschreibung || ''} + + ); + } + public titleFn(): string { + return `${this.name}\n${this.beschreibung || ''}`; + } + public icon(): LeafletIcon { + if (this.iconUrl) { + return L.icon({ + iconUrl: this.iconUrl, + iconSize: [24, 24], + }); + } + + return markerIcon; + } + + public static factory(): FirecallItemBase { + return new FirecallItemMarker(); + } + + // 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..5f1486b --- /dev/null +++ b/src/components/FirecallItems/elements/index.tsx @@ -0,0 +1,69 @@ +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'; +import { FirecallLine } from './FirecallLine'; +import { FirecallRohr } from './FirecallRohr'; +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, + 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 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); + default: + return new FirecallItemBase(record); + } +} diff --git a/src/components/FirecallItems/elements/marker/FirecallItemDefault.tsx b/src/components/FirecallItems/elements/marker/FirecallItemDefault.tsx new file mode 100644 index 0000000..3997b18 --- /dev/null +++ b/src/components/FirecallItems/elements/marker/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/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/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/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/Map/MapActionButtons.tsx b/src/components/Map/MapActionButtons.tsx index 809a76b..83c7997 100644 --- a/src/components/Map/MapActionButtons.tsx +++ b/src/components/Map/MapActionButtons.tsx @@ -11,6 +11,7 @@ import { Connection, FirecallItem } from '../firebase/firestore'; import FirecallItemDialog from '../FirecallItems/FirecallItemDialog'; import { firecallItemInfo } from '../FirecallItems/infos/firecallitems'; import { useLeitungen } from './Leitungen/context'; +import { fcItemClasses } from '../FirecallItems/elements'; export interface MapActionButtonsOptions { map: L.Map; @@ -42,7 +43,7 @@ export default function MapActionButtons({ map }: MapActionButtonsOptions) { const fzgDialogClose = useCallback( (fzg?: FirecallItem) => { setFzgDialogIsOpen(false); - if (['connection', 'line', 'area'].includes(fzg?.type || '')) { + if (fcItemClasses[fzg?.type || ''].isPolyline()) { leitungen.setIsDrawing(true); leitungen.setFirecallItem(fzg as Connection); } else { 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/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/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( diff --git a/src/components/pages/LoginUi.tsx b/src/components/pages/LoginUi.tsx index ea426f8..6a598b4 100644 --- a/src/components/pages/LoginUi.tsx +++ b/src/components/pages/LoginUi.tsx @@ -1,23 +1,25 @@ -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'; 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 ( <> {!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/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 && }