"use client"; import * as React from "react"; import Link from "next/link"; import { Droplets, MapPin, Plus, Search } from "lucide-react"; import { HydrantStatus, HydrantTyp } from "@prisma/client"; import { trpc } from "@/lib/trpc/client"; import { Button } from "@/components/ui/button"; import { Input } from "@/components/ui/input"; import { Label } from "@/components/ui/label"; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from "@/components/ui/select"; import { Card, CardContent, CardDescription, CardHeader, CardTitle } from "@/components/ui/card"; import { Fehlermeldung } from "@/components/stammdaten/anzeige"; import { Karte, type KartenPunkt } from "@/components/karte/karte"; import { alsSvg } from "@/lib/karte/taktische-zeichen"; import { alsDatum } from "@/lib/utils"; /** * Die Karte des Ausrückegebiets. * * Zwei Ebenen: Hydranten und Objekte. Beide sind ein- und ausschaltbar, denn * wer nach Löschwasser sucht, will keine Feuerwehrpläne sehen und umgekehrt. * * Die Umkreissuche ist der eigentliche Zweck: **was liegt in der Nähe?** Sie * geht von einem angetippten Punkt aus — im Einsatz von der Einsatzstelle, am * Schreibtisch von dort, wo man gerade hinschaut. */ const TYP_TEXT: Record = { UNTERFLUR: "Unterflur", UEBERFLUR: "Überflur", SAUGSTELLE: "Saugstelle", ZISTERNE: "Zisterne", OFFENES_GEWAESSER: "offenes Gewässer", SONSTIGES: "sonstige", }; const STATUS_TEXT: Record = { EINSATZBEREIT: "einsatzbereit", EINGESCHRAENKT: "eingeschränkt", DEFEKT: "defekt", UNBEKANNT: "ungeprüft", }; const STATUS_FARBE: Record = { EINSATZBEREIT: "#15803d", EINGESCHRAENKT: "#ca8a04", DEFEKT: "#b91c1c", UNBEKANNT: "#6b7280", }; export function KartenAnsicht({ stilUrl, mittelpunkt, zoom, darfBearbeiten, }: { stilUrl: string | null; mittelpunkt: { breitengrad: number; laengengrad: number }; zoom: number; darfBearbeiten: boolean; }) { const [suche, setSuche] = React.useState(""); const [typ, setTyp] = React.useState("alle"); const [status, setStatus] = React.useState("alle"); const [zeigeObjekte, setZeigeObjekte] = React.useState(true); const [zeigeHydranten, setZeigeHydranten] = React.useState(true); const [gewaehlt, setGewaehlt] = React.useState<{ art: "hydrant" | "objekt"; id: string } | null>( null, ); const [umkreisPunkt, setUmkreisPunkt] = React.useState<{ breitengrad: number; laengengrad: number; } | null>(null); const [meldung, setMeldung] = React.useState(null); const hydranten = trpc.hydrant.liste.useQuery({ suche: suche || undefined, typ: typ === "alle" ? undefined : typ, status: status === "alle" ? undefined : status, }); const objekte = trpc.objekt.liste.useQuery({ suche: suche || undefined }); const kennzahlen = trpc.hydrant.kennzahlen.useQuery(); const umkreis = trpc.hydrant.umkreis.useQuery( umkreisPunkt ? { ...umkreisPunkt, umkreisMeter: 500 } : { breitengrad: 0, laengengrad: 0 }, { enabled: umkreisPunkt !== null }, ); const hydrantSvg = React.useMemo(() => alsSvg("hydrant", 26), []); const punkte = React.useMemo(() => { const liste: KartenPunkt[] = []; if (zeigeHydranten) { for (const hydrant of hydranten.data ?? []) { liste.push({ id: `h-${hydrant.id}`, breitengrad: Number(hydrant.breitengrad), laengengrad: Number(hydrant.laengengrad), svg: hydrantSvg.replace("color:#1d4ed8", `color:${STATUS_FARBE[hydrant.status]}`), titel: `${hydrant.bezeichnung ?? TYP_TEXT[hydrant.typ]} — ${STATUS_TEXT[hydrant.status]}`, beiKlick: () => setGewaehlt({ art: "hydrant", id: hydrant.id }), }); } } if (zeigeObjekte) { for (const objekt of objekte.data ?? []) { if (objekt.breitengrad === null || objekt.laengengrad === null) continue; liste.push({ id: `o-${objekt.id}`, breitengrad: Number(objekt.breitengrad), laengengrad: Number(objekt.laengengrad), farbe: "#1d4ed8", titel: objekt.name, beiKlick: () => setGewaehlt({ art: "objekt", id: objekt.id }), }); } } return liste; }, [hydranten.data, objekte.data, zeigeHydranten, zeigeObjekte, hydrantSvg]); const gewaehlterHydrant = gewaehlt?.art === "hydrant" ? hydranten.data?.find((eintrag) => eintrag.id === gewaehlt.id) : undefined; const gewaehltesObjekt = gewaehlt?.art === "objekt" ? objekte.data?.find((eintrag) => eintrag.id === gewaehlt.id) : undefined; return (
{meldung && } {/* Filter */}
setSuche(ereignis.target.value)} placeholder="Straße, Nummer, Objektname, Gefahr …" />
{darfBearbeiten && ( )}
{ setUmkreisPunkt(ort); setGewaehlt(null); }} hoehe="68vh" />
{/* Was gerade angetippt wurde. */} {gewaehlterHydrant && ( {gewaehlterHydrant.bezeichnung ?? TYP_TEXT[gewaehlterHydrant.typ]} {TYP_TEXT[gewaehlterHydrant.typ]} · {STATUS_TEXT[gewaehlterHydrant.status]} {gewaehlterHydrant.strasse && (

{gewaehlterHydrant.strasse} {gewaehlterHydrant.hausnummer ?? ""}

)}

{gewaehlterHydrant.nennweite ? `DN ${gewaehlterHydrant.nennweite}` : "Nennweite unbekannt"} {gewaehlterHydrant.durchfluss ? ` · ${gewaehlterHydrant.durchfluss} l/min` : ""}

{gewaehlterHydrant.letztePruefung ? `zuletzt geprüft ${alsDatum(gewaehlterHydrant.letztePruefung)}` : "noch nie geprüft"}

{gewaehlterHydrant.hinweis &&

{gewaehlterHydrant.hinweis}

}
)} {gewaehltesObjekt && ( {gewaehltesObjekt.name} {gewaehltesObjekt.strasse} {gewaehltesObjekt.hausnummer} {gewaehltesObjekt.gefahren && (

Gefahren: {gewaehltesObjekt.gefahren}

)}
)} {/* Umkreissuche */} In der Nähe {umkreisPunkt ? "Löschwasser im Umkreis von 500 Metern, nach Entfernung." : "In die Karte tippen — dann steht hier, was in 500 Metern erreichbar ist."} {umkreisPunkt && (umkreis.data?.length ?? 0) === 0 && !umkreis.isPending && (

Kein Löschwasser im Umkreis von 500 Metern. Das ist eine Angabe, die man vor dem Einsatz wissen will, nicht währenddessen.

)}
    {umkreis.data?.map((treffer) => (
  • {treffer.bezeichnung ?? TYP_TEXT[treffer.typ]} {treffer.strasse ?? "ohne Straße"} {treffer.nennweite ? ` · DN ${treffer.nennweite}` : ""} {treffer.durchfluss ? ` · ${treffer.durchfluss} l/min` : ""} {treffer.entfernung} m
  • ))}
Bestand

gesamt

{kennzahlen.data?.gesamt ?? 0}

defekt

{kennzahlen.data?.defekt ?? 0}

ungeprüft

{kennzahlen.data?.ungeprueft ?? 0}

); }