From 157394767ff5a18ff37e4b51c06c27ac9bea56fb Mon Sep 17 00:00:00 2001 From: PxlLoewe <72106766+PxlLoewe@users.noreply.github.com> Date: Tue, 1 Jul 2025 02:19:00 -0700 Subject: [PATCH] =?UTF-8?q?Fahrzeugauswahl=20=C3=BCberarbeitet?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../dispatch/_components/StationSelect.tsx | 151 ++++++++++++++++++ .../_components/pannel/MissionForm.tsx | 50 +++--- apps/dispatch/app/_components/Select.tsx | 58 +++---- apps/dispatch/app/_components/left/Chat.tsx | 1 + .../dispatch/app/_components/map/BaseMaps.tsx | 22 +-- .../map/_components/MissionMarkerTabs.tsx | 117 +++++--------- .../(app)/admin/keyword/_components/Form.tsx | 4 +- apps/hub/app/_components/ui/List.tsx | 1 + packages/prometheus/prometheus.dev.yml | 2 +- 9 files changed, 249 insertions(+), 157 deletions(-) create mode 100644 apps/dispatch/app/(app)/dispatch/_components/StationSelect.tsx diff --git a/apps/dispatch/app/(app)/dispatch/_components/StationSelect.tsx b/apps/dispatch/app/(app)/dispatch/_components/StationSelect.tsx new file mode 100644 index 00000000..73dbab03 --- /dev/null +++ b/apps/dispatch/app/(app)/dispatch/_components/StationSelect.tsx @@ -0,0 +1,151 @@ +import { HpgState } from "@repo/db"; +import { cn } from "@repo/shared-components"; +import { useQuery } from "@tanstack/react-query"; +import { Select } from "_components/Select"; +import { getConnectedAircraftsAPI } from "_querys/aircrafts"; +import { getStationsAPI } from "_querys/stations"; +import { Ambulance, FireExtinguisher, Radio, Siren } from "lucide-react"; +import { useState } from "react"; +import { FieldValues } from "react-hook-form"; + +type MissionStationsSelectProps = { + selectedStations?: number[]; + className?: string; + menuPlacement?: "top" | "bottom" | "auto"; // Added menuPlacement prop for better control + onChange?: (value: { + selectedStationIds: number[]; + hpgAmbulanceState?: HpgState; + hpgFireEngineState?: HpgState; + hpgPoliceState?: HpgState; + }) => void; + vehicleStates: { + hpgAmbulanceState?: HpgState; + hpgFireEngineState?: HpgState; + hpgPoliceState?: HpgState; + }; + filterSelected?: boolean; // If true, filter out selected stations from the options + isMulti?: boolean; +}; + +export function StationsSelect({ + onChange, + selectedStations, + vehicleStates, + className = "", + isMulti = true, + menuPlacement = "bottom", + filterSelected = false, +}: MissionStationsSelectProps) { + const { data: connectedAircrafts } = useQuery({ + queryKey: ["aircrafts"], + queryFn: () => getConnectedAircraftsAPI(), + }); + const { data: stations } = useQuery({ + queryKey: ["stations"], + queryFn: () => getStationsAPI(), + }); + + const [value, setValue] = useState(selectedStations?.map((id) => String(id)) || []); + + // Helper to check if a station is a vehicle and its state is NOT_REQUESTED + const stationsOptions = [ + ...(stations?.map((station) => ({ + label: station.bosCallsign, + value: station.id, + type: "station" as const, + isOnline: !!connectedAircrafts?.find((a) => a.stationId === station.id), + })) || []), + + { label: "Feuerwehr", value: "FW", type: "vehicle" as const }, + { label: "Polizei", value: "POL", type: "vehicle" as const }, + { label: "RTW", value: "RTW", type: "vehicle" as const }, + ] + .sort((a, b) => { + // 1. Vehicles first + if (a.type === "vehicle" && b.type !== "vehicle") return -1; + if (a.type !== "vehicle" && b.type === "vehicle") return 1; + + // 2. Online stations before offline stations + if (a.type === "station" && b.type === "station") { + if (a.isOnline && !b.isOnline) return -1; + if (!a.isOnline && b.isOnline) return 1; + } + + // 3. Otherwise, sort alphabetically by label + return a.label.localeCompare(b.label); + }) + .filter((s) => { + if (!filterSelected) return true; // If filterSelected is false, include all stations + // Filter out selected stations if filterSelectedStations is true + if (s.type === "station") { + return !selectedStations?.includes(s.value); + } + // If the station is a vehicle, we need to check its state + if (s.type === "vehicle") { + if (s.value === "FW" && vehicleStates.hpgFireEngineState !== HpgState.NOT_REQUESTED) + return false; + if (s.value === "POL" && vehicleStates.hpgPoliceState !== HpgState.NOT_REQUESTED) + return false; + if (s.value === "RTW" && vehicleStates.hpgAmbulanceState !== HpgState.NOT_REQUESTED) + return false; + } + return true; + }); + + return ( + { - const aHasAircraft = aircrafts?.some((ac) => ac.stationId === a.id) ? 0 : 1; - const bHasAircraft = aircrafts?.some((ac) => ac.stationId === b.id) ? 0 : 1; - return aHasAircraft - bHasAircraft; - }) - .map((s) => ({ - label: ( - - {aircrafts?.some((a) => a.stationId === s.id) && ( - - )} - {s.bosCallsign} - - ), - value: s.id, - }))} + selectedStations={form.watch("missionStationIds")} + onChange={(v) => { + form.setValue("missionStationIds", v.selectedStationIds); + form.setValue("hpgAmbulanceState", v.hpgAmbulanceState); + form.setValue("hpgFireEngineState", v.hpgFireEngineState); + form.setValue("hpgPoliceState", v.hpgPoliceState); + }} + vehicleStates={{ + hpgAmbulanceState: form.watch("hpgAmbulanceState"), + hpgFireEngineState: form.watch("hpgFireEngineState"), + hpgPoliceState: form.watch("hpgPoliceState"), + }} /> @@ -371,11 +358,12 @@ export const MissionForm = () => { form.setValue("hpgMissionString", e.target.value); const [name] = e.target.value.split(":"); const allHpgMissionTypes = keywords?.map((k) => k.hpgMissionTypes).flat(); + const missionAdditionalInfo = form.watch("missionAdditionalInfo"); if ( - !form.watch("missionAdditionalInfo") || + !missionAdditionalInfo || allHpgMissionTypes?.find((t) => { const [hpgName] = t.split(":"); - return hpgName === form.watch("missionAdditionalInfo"); + return hpgName === missionAdditionalInfo; }) ) { form.setValue("missionAdditionalInfo", name || ""); @@ -444,7 +432,7 @@ export const MissionForm = () => { try { console.log("Saving mission", mission.addressOSMways); const newMission = await saveMission(mission); - toast.success(`Einsatz ${newMission.id} erfolgreich aktualisiert`); + toast.success(`Einsatz ${newMission.publicId} aktualisiert`); setSearchElements([]); // Reset search elements setEditingMission(null); // Reset editing state form.reset(); // Reset the form diff --git a/apps/dispatch/app/_components/Select.tsx b/apps/dispatch/app/_components/Select.tsx index 60ea8e27..81e38ff4 100644 --- a/apps/dispatch/app/_components/Select.tsx +++ b/apps/dispatch/app/_components/Select.tsx @@ -1,16 +1,8 @@ "use client"; -import { FieldValues, Path, RegisterOptions, UseFormReturn } from "react-hook-form"; +import { FieldValues, Path } from "react-hook-form"; import SelectTemplate, { Props as SelectTemplateProps, StylesConfig } from "react-select"; import { cn } from "@repo/shared-components"; import dynamic from "next/dynamic"; -import { CSSProperties } from "react"; - -interface SelectProps extends Omit { - label?: any; - name: Path; - form: UseFormReturn | any; - formOptions?: RegisterOptions; -} const customStyles: StylesConfig = { control: (provided) => ({ @@ -28,7 +20,7 @@ const customStyles: StylesConfig = { ...provided, backgroundColor: state.isSelected ? "hsl(var(--p))" : "hsl(var(--b1))", color: "var(--color-primary-content)", - "&:hover": { backgroundColor: "var(--color-base-200)" }, // DaisyUI secondary color + "&:hover": { backgroundColor: "var(--color-base-200)" }, }), multiValueLabel: (provided) => ({ ...provided, @@ -46,53 +38,55 @@ const customStyles: StylesConfig = { ...provided, backgroundColor: "var(--color-base-100)", borderRadius: "0.5rem", + zIndex: 9999, }), }; -const SelectCom = ({ - name, - label = name, +interface SelectProps extends Omit { + label?: any; + value: any; + onChange: (value: any) => void; + error?: string; +} + +const SelectCom = ({ + label, placeholder = label, - form, - formOptions, + value, + onChange, + error, className, ...inputProps -}: SelectProps) => { +}: SelectProps) => { return ( -
+
{label} { - if (Array.isArray(newValue)) { - form.setValue(name, newValue.map((v: any) => v.value) as any, { - shouldDirty: true, - }); + if ((inputProps as any)?.isMulti) { + onChange(Array.isArray(newValue) ? newValue.map((v: any) => v.value) : []); } else { - form.setValue(name, newValue.value, { - shouldDirty: true, - }); + onChange(newValue ? newValue.value : null); } - form.trigger(name); - form.Dirty; }} value={ (inputProps as any)?.isMulti - ? (inputProps as any).options?.filter((o: any) => form.watch(name)?.includes(o.value)) - : (inputProps as any).options?.find((o: any) => o.value === form.watch(name)) + ? (inputProps as any).options?.filter( + (o: any) => Array.isArray(value) && value.includes(o.value), + ) + : (inputProps as any).options?.find((o: any) => o.value === value) } styles={customStyles as any} className={cn("w-full placeholder:text-neutral-600", className)} placeholder={placeholder} {...inputProps} /> - {form.formState.errors[name]?.message && ( -

{form.formState.errors[name].message as string}

- )} + {error &&

{error}

}
); }; -const SelectWrapper = (props: SelectProps) => ; +const SelectWrapper = (props: SelectProps) => ; export const Select = dynamic(() => Promise.resolve(SelectWrapper), { ssr: false, diff --git a/apps/dispatch/app/_components/left/Chat.tsx b/apps/dispatch/app/_components/left/Chat.tsx index b3a9a4bf..79ae6b36 100644 --- a/apps/dispatch/app/_components/left/Chat.tsx +++ b/apps/dispatch/app/_components/left/Chat.tsx @@ -140,6 +140,7 @@ export const Chat = () => { {chat.notification && }
+ {/* So macht man kein overflow handeling, weiß ich. Aber es funktioniert... */} {chat.messages.map((chatMessage) => { const isSender = chatMessage.senderId === session.data?.user.id; return ( diff --git a/apps/dispatch/app/_components/map/BaseMaps.tsx b/apps/dispatch/app/_components/map/BaseMaps.tsx index 6c377602..3da4b31a 100644 --- a/apps/dispatch/app/_components/map/BaseMaps.tsx +++ b/apps/dispatch/app/_components/map/BaseMaps.tsx @@ -1,7 +1,7 @@ "use client"; import { usePannelStore } from "_store/pannelStore"; import { Control, Icon, LatLngExpression } from "leaflet"; -import { useEffect, useRef, useState } from "react"; +import { useEffect, useMemo, useRef, useState } from "react"; import { LayerGroup, LayersControl, @@ -73,9 +73,9 @@ const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) => queryKey: ["stations"], queryFn: () => getStationsAPI(), }); + console.log("StationsLayer: stations", stations); const [selectedStations, setSelectedStations] = useState([]); - const [stationsWithIcon, setStationsWithIcon] = useState<(Station & { icon?: string })[]>([]); // Zustand für die Stationen mit Icon const attributionText = ""; const resetSelection = () => { @@ -94,27 +94,29 @@ const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) => } }; - useEffect(() => { - // Erstelle die Icons für alle Stationen + const [stationsWithIcon, setStationsWithIcon] = useState<(Station & { icon: string })[]>([]); + useEffect(() => { + if (!stations) { + setStationsWithIcon([]); + return; + } const fetchIcons = async () => { - if (!stations) return; const urls = await Promise.all( stations.map(async (station) => { - return createCustomMarker(station.operator); + return await createCustomMarker(station.operator); }), ); - setStationsWithIcon(stations.map((station, index) => ({ ...station, icon: urls[index] }))); + setStationsWithIcon(stations.map((station, index) => ({ ...station, icon: urls[index]! }))); }; - fetchIcons(); }, [stations]); return ( - {stationsWithIcon.map((station) => { + {stationsWithIcon?.map((station) => { const coordinates: LatLngExpression = [station.latitude, station.longitude]; - const typeLabel = station.bosUse.charAt(0).toUpperCase(); + const typeLabel = station.bosUse?.charAt(0).toUpperCase(); return ( { }, }); - const stationsOptions = [ - ...(allStations - ?.filter((s) => !mission.missionStationIds.includes(s.id)) - ?.map((station) => ({ - label: station.bosCallsign, - value: station.id, - type: "station" as const, - isOnline: !!connectedAircrafts?.find((a) => a.stationId === station.id), - })) || []), - ...(!mission.hpgFireEngineState || mission.hpgFireEngineState === "NOT_REQUESTED" - ? [{ label: "Feuerwehr", value: "FW", type: "vehicle" as const }] - : []), - ...(!mission.hpgAmbulanceState || mission.hpgAmbulanceState === "NOT_REQUESTED" - ? [{ label: "Rettungsdienst", value: "RTW", type: "vehicle" as const }] - : []), - ...(!mission.hpgPoliceState || mission.hpgPoliceState === "NOT_REQUESTED" - ? [{ label: "Polizei", value: "POL", type: "vehicle" as const }] - : []), - ].sort((a, b) => { - // 1. Vehicles first - if (a.type === "vehicle" && b.type !== "vehicle") return -1; - if (a.type !== "vehicle" && b.type === "vehicle") return 1; - - // 2. Online stations before offline stations - if (a.type === "station" && b.type === "station") { - if (a.isOnline && !b.isOnline) return -1; - if (!a.isOnline && b.isOnline) return 1; - } - - // 3. Otherwise, sort alphabetically by label - return a.label.localeCompare(b.label); - }); - const dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected"; const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => ( @@ -503,70 +471,57 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => { const connectedAircraft = connectedAircrafts?.find( (aircraft) => aircraft.stationId === station.id, ); + console.log("connectedAircraft", connectedAircraft); return (
  • - {connectedAircraft && ( - - {connectedAircraft.fmsStatus} - - )} - -
    - {station.bosCallsign} - {/* {item.min > 0 && ( - <> -
    - Ankunft in ca. {item.min} min - - )} */} -
    + + {connectedAircraft?.fmsStatus || "6"} + + + {station.bosCallsign} + {!connectedAircraft && ( + Kein Benutzer verbunden + )}
  • ); })} - {mission.hpgAmbulanceState && } - {mission.hpgFireEngineState && ( + {mission.hpgAmbulanceState && mission.hpgAmbulanceState !== "NOT_REQUESTED" && ( + + )} + {mission.hpgFireEngineState && mission.hpgFireEngineState !== "NOT_REQUESTED" && ( )} - {mission.hpgPoliceState && } + {mission.hpgPoliceState && mission.hpgPoliceState !== "NOT_REQUESTED" && ( + + )} {dispatcherConnected && (
    {/* TODO: make it a small multiselect */} - + selectedStations={mission.missionStationIds} + filterSelected + vehicleStates={{ + hpgAmbulanceState: mission.hpgAmbulanceState || undefined, + hpgFireEngineState: mission.hpgFireEngineState || undefined, + hpgPoliceState: mission.hpgPoliceState || undefined, + }} + />