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 8ef1cbc4..79ae6b36 100644 --- a/apps/dispatch/app/_components/left/Chat.tsx +++ b/apps/dispatch/app/_components/left/Chat.tsx @@ -68,7 +68,7 @@ export const Chat = () => { {chatOpen && (