Merge branch 'main' of github.com:VAR-Virtual-Air-Rescue/var-monorepo
This commit is contained in:
151
apps/dispatch/app/(app)/dispatch/_components/StationSelect.tsx
Normal file
151
apps/dispatch/app/(app)/dispatch/_components/StationSelect.tsx
Normal file
@@ -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<T extends FieldValues> = {
|
||||||
|
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<T extends FieldValues>({
|
||||||
|
onChange,
|
||||||
|
selectedStations,
|
||||||
|
vehicleStates,
|
||||||
|
className = "",
|
||||||
|
isMulti = true,
|
||||||
|
menuPlacement = "bottom",
|
||||||
|
filterSelected = false,
|
||||||
|
}: MissionStationsSelectProps<T>) {
|
||||||
|
const { data: connectedAircrafts } = useQuery({
|
||||||
|
queryKey: ["aircrafts"],
|
||||||
|
queryFn: () => getConnectedAircraftsAPI(),
|
||||||
|
});
|
||||||
|
const { data: stations } = useQuery({
|
||||||
|
queryKey: ["stations"],
|
||||||
|
queryFn: () => getStationsAPI(),
|
||||||
|
});
|
||||||
|
|
||||||
|
const [value, setValue] = useState<string[]>(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 (
|
||||||
|
<Select
|
||||||
|
className={className}
|
||||||
|
menuPlacement={menuPlacement}
|
||||||
|
isMulti={isMulti}
|
||||||
|
onChange={(v) => {
|
||||||
|
setValue(v);
|
||||||
|
if (!isMulti) return onChange?.(v);
|
||||||
|
const hpgAmbulanceState =
|
||||||
|
vehicleStates.hpgAmbulanceState === "NOT_REQUESTED" && v.includes("RTW")
|
||||||
|
? HpgState.DISPATCHED
|
||||||
|
: vehicleStates.hpgAmbulanceState;
|
||||||
|
const hpgFireEngineState =
|
||||||
|
vehicleStates.hpgFireEngineState === "NOT_REQUESTED" && v.includes("FW")
|
||||||
|
? HpgState.DISPATCHED
|
||||||
|
: vehicleStates.hpgFireEngineState;
|
||||||
|
const hpgPoliceState =
|
||||||
|
vehicleStates.hpgPoliceState === "NOT_REQUESTED" && v.includes("POL")
|
||||||
|
? HpgState.DISPATCHED
|
||||||
|
: vehicleStates.hpgPoliceState;
|
||||||
|
|
||||||
|
onChange?.({
|
||||||
|
selectedStationIds: v
|
||||||
|
.filter((id: string) => !["FW", "POL", "RTW"].includes(id))
|
||||||
|
.map(Number),
|
||||||
|
hpgAmbulanceState,
|
||||||
|
hpgFireEngineState,
|
||||||
|
hpgPoliceState,
|
||||||
|
});
|
||||||
|
}}
|
||||||
|
value={value}
|
||||||
|
label=""
|
||||||
|
placeholder={
|
||||||
|
isMulti ? "Wähle ein oder mehrere Rettungsmittel aus" : "Wähle ein Rettungsmittel aus"
|
||||||
|
}
|
||||||
|
options={stationsOptions.map((s) => ({
|
||||||
|
label: (
|
||||||
|
<div
|
||||||
|
className={cn(s.type === "vehicle" && isMulti && "tooltip tooltip-right")}
|
||||||
|
data-tip={
|
||||||
|
"Wenn kein Pilot mit HPG-Script im Einsatz ist, bleibt dieses Fahrzeug im Status 4."
|
||||||
|
}
|
||||||
|
>
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
{s.type === "station" && s.isOnline && <Radio className="text-success" size={15} />}
|
||||||
|
{s.type === "vehicle" && s.value === "FW" && <FireExtinguisher size={15} />}
|
||||||
|
{s.type === "vehicle" && s.value === "POL" && <Siren size={15} />}
|
||||||
|
{s.type === "vehicle" && s.value === "RTW" && <Ambulance size={15} />}
|
||||||
|
{s.label}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
|
value: s.value,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -28,6 +28,7 @@ import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
|
|||||||
import { selectRandomHPGMissionSzenery } from "_helpers/selectRandomHPGMission";
|
import { selectRandomHPGMissionSzenery } from "_helpers/selectRandomHPGMission";
|
||||||
import { AxiosError } from "axios";
|
import { AxiosError } from "axios";
|
||||||
import { cn } from "@repo/shared-components";
|
import { cn } from "@repo/shared-components";
|
||||||
|
import { StationsSelect } from "(app)/dispatch/_components/StationSelect";
|
||||||
|
|
||||||
export const MissionForm = () => {
|
export const MissionForm = () => {
|
||||||
const { editingMissionId, setEditingMission } = usePannelStore();
|
const { editingMissionId, setEditingMission } = usePannelStore();
|
||||||
@@ -168,10 +169,6 @@ export const MissionForm = () => {
|
|||||||
if (createNewMission) {
|
if (createNewMission) {
|
||||||
newMission = await createMissionMutation.mutateAsync({
|
newMission = await createMissionMutation.mutateAsync({
|
||||||
...(mission as unknown as Prisma.MissionCreateInput),
|
...(mission as unknown as Prisma.MissionCreateInput),
|
||||||
missionAdditionalInfo:
|
|
||||||
!mission.missionAdditionalInfo.length && hpgSzenario
|
|
||||||
? `HPG-Szenario: ${hpgSzenario}`
|
|
||||||
: mission.missionAdditionalInfo,
|
|
||||||
hpgSelectedMissionString: szenarioCode,
|
hpgSelectedMissionString: szenarioCode,
|
||||||
});
|
});
|
||||||
if (validationRequired) {
|
if (validationRequired) {
|
||||||
@@ -205,7 +202,7 @@ export const MissionForm = () => {
|
|||||||
}
|
}
|
||||||
return newMission;
|
return newMission;
|
||||||
};
|
};
|
||||||
|
console.log(form.formState.errors);
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4">
|
<form className="space-y-4">
|
||||||
{/* Koorinaten Section */}
|
{/* Koorinaten Section */}
|
||||||
@@ -262,33 +259,23 @@ export const MissionForm = () => {
|
|||||||
className="input input-primary input-bordered w-full mt-4"
|
className="input input-primary input-bordered w-full mt-4"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
{/* Rettungsmittel Section */}
|
{/* Rettungsmittel Section */}
|
||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<h2 className="text-lg font-bold mb-2">Rettungsmittel</h2>
|
<h2 className="text-lg font-bold mb-2">Rettungsmittel</h2>
|
||||||
<Select
|
<StationsSelect
|
||||||
name="missionStationIds"
|
|
||||||
label={""}
|
|
||||||
placeholder="Wähle ein oder mehrere Rettungsmittel aus"
|
|
||||||
isMulti
|
isMulti
|
||||||
form={form}
|
selectedStations={form.watch("missionStationIds")}
|
||||||
options={stations
|
onChange={(v) => {
|
||||||
?.sort((a, b) => {
|
form.setValue("missionStationIds", v.selectedStationIds);
|
||||||
const aHasAircraft = aircrafts?.some((ac) => ac.stationId === a.id) ? 0 : 1;
|
form.setValue("hpgAmbulanceState", v.hpgAmbulanceState);
|
||||||
const bHasAircraft = aircrafts?.some((ac) => ac.stationId === b.id) ? 0 : 1;
|
form.setValue("hpgFireEngineState", v.hpgFireEngineState);
|
||||||
return aHasAircraft - bHasAircraft;
|
form.setValue("hpgPoliceState", v.hpgPoliceState);
|
||||||
})
|
}}
|
||||||
.map((s) => ({
|
vehicleStates={{
|
||||||
label: (
|
hpgAmbulanceState: form.watch("hpgAmbulanceState"),
|
||||||
<span className="flex items-center gap-2">
|
hpgFireEngineState: form.watch("hpgFireEngineState"),
|
||||||
{aircrafts?.some((a) => a.stationId === s.id) && (
|
hpgPoliceState: form.watch("hpgPoliceState"),
|
||||||
<Radio className="text-success" size={15} />
|
}}
|
||||||
)}
|
|
||||||
{s.bosCallsign}
|
|
||||||
</span>
|
|
||||||
),
|
|
||||||
value: s.id,
|
|
||||||
}))}
|
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
@@ -371,11 +358,12 @@ export const MissionForm = () => {
|
|||||||
form.setValue("hpgMissionString", e.target.value);
|
form.setValue("hpgMissionString", e.target.value);
|
||||||
const [name] = e.target.value.split(":");
|
const [name] = e.target.value.split(":");
|
||||||
const allHpgMissionTypes = keywords?.map((k) => k.hpgMissionTypes).flat();
|
const allHpgMissionTypes = keywords?.map((k) => k.hpgMissionTypes).flat();
|
||||||
|
const missionAdditionalInfo = form.watch("missionAdditionalInfo");
|
||||||
if (
|
if (
|
||||||
!form.watch("missionAdditionalInfo") ||
|
!missionAdditionalInfo ||
|
||||||
allHpgMissionTypes?.find((t) => {
|
allHpgMissionTypes?.find((t) => {
|
||||||
const [hpgName] = t.split(":");
|
const [hpgName] = t.split(":");
|
||||||
return hpgName === form.watch("missionAdditionalInfo");
|
return hpgName === missionAdditionalInfo;
|
||||||
})
|
})
|
||||||
) {
|
) {
|
||||||
form.setValue("missionAdditionalInfo", name || "");
|
form.setValue("missionAdditionalInfo", name || "");
|
||||||
@@ -444,7 +432,7 @@ export const MissionForm = () => {
|
|||||||
try {
|
try {
|
||||||
console.log("Saving mission", mission.addressOSMways);
|
console.log("Saving mission", mission.addressOSMways);
|
||||||
const newMission = await saveMission(mission);
|
const newMission = await saveMission(mission);
|
||||||
toast.success(`Einsatz ${newMission.id} erfolgreich aktualisiert`);
|
toast.success(`Einsatz ${newMission.publicId} aktualisiert`);
|
||||||
setSearchElements([]); // Reset search elements
|
setSearchElements([]); // Reset search elements
|
||||||
setEditingMission(null); // Reset editing state
|
setEditingMission(null); // Reset editing state
|
||||||
form.reset(); // Reset the form
|
form.reset(); // Reset the form
|
||||||
|
|||||||
@@ -1,16 +1,8 @@
|
|||||||
"use client";
|
"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 SelectTemplate, { Props as SelectTemplateProps, StylesConfig } from "react-select";
|
||||||
import { cn } from "@repo/shared-components";
|
import { cn } from "@repo/shared-components";
|
||||||
import dynamic from "next/dynamic";
|
import dynamic from "next/dynamic";
|
||||||
import { CSSProperties } from "react";
|
|
||||||
|
|
||||||
interface SelectProps<T extends FieldValues> extends Omit<SelectTemplateProps, "form"> {
|
|
||||||
label?: any;
|
|
||||||
name: Path<T>;
|
|
||||||
form: UseFormReturn<T> | any;
|
|
||||||
formOptions?: RegisterOptions<T>;
|
|
||||||
}
|
|
||||||
|
|
||||||
const customStyles: StylesConfig<any, false> = {
|
const customStyles: StylesConfig<any, false> = {
|
||||||
control: (provided) => ({
|
control: (provided) => ({
|
||||||
@@ -28,7 +20,7 @@ const customStyles: StylesConfig<any, false> = {
|
|||||||
...provided,
|
...provided,
|
||||||
backgroundColor: state.isSelected ? "hsl(var(--p))" : "hsl(var(--b1))",
|
backgroundColor: state.isSelected ? "hsl(var(--p))" : "hsl(var(--b1))",
|
||||||
color: "var(--color-primary-content)",
|
color: "var(--color-primary-content)",
|
||||||
"&:hover": { backgroundColor: "var(--color-base-200)" }, // DaisyUI secondary color
|
"&:hover": { backgroundColor: "var(--color-base-200)" },
|
||||||
}),
|
}),
|
||||||
multiValueLabel: (provided) => ({
|
multiValueLabel: (provided) => ({
|
||||||
...provided,
|
...provided,
|
||||||
@@ -46,53 +38,55 @@ const customStyles: StylesConfig<any, false> = {
|
|||||||
...provided,
|
...provided,
|
||||||
backgroundColor: "var(--color-base-100)",
|
backgroundColor: "var(--color-base-100)",
|
||||||
borderRadius: "0.5rem",
|
borderRadius: "0.5rem",
|
||||||
|
zIndex: 9999,
|
||||||
}),
|
}),
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectCom = <T extends FieldValues>({
|
interface SelectProps extends Omit<SelectTemplateProps, "form" | "value" | "onChange"> {
|
||||||
name,
|
label?: any;
|
||||||
label = name,
|
value: any;
|
||||||
|
onChange: (value: any) => void;
|
||||||
|
error?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectCom = ({
|
||||||
|
label,
|
||||||
placeholder = label,
|
placeholder = label,
|
||||||
form,
|
value,
|
||||||
formOptions,
|
onChange,
|
||||||
|
error,
|
||||||
className,
|
className,
|
||||||
...inputProps
|
...inputProps
|
||||||
}: SelectProps<T>) => {
|
}: SelectProps) => {
|
||||||
return (
|
return (
|
||||||
<div>
|
<div className="relative">
|
||||||
<span className="label-text text-lg flex items-center gap-2">{label}</span>
|
<span className="label-text text-lg flex items-center gap-2">{label}</span>
|
||||||
<SelectTemplate
|
<SelectTemplate
|
||||||
onChange={(newValue: any) => {
|
onChange={(newValue: any) => {
|
||||||
if (Array.isArray(newValue)) {
|
if ((inputProps as any)?.isMulti) {
|
||||||
form.setValue(name, newValue.map((v: any) => v.value) as any, {
|
onChange(Array.isArray(newValue) ? newValue.map((v: any) => v.value) : []);
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
} else {
|
} else {
|
||||||
form.setValue(name, newValue.value, {
|
onChange(newValue ? newValue.value : null);
|
||||||
shouldDirty: true,
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
form.trigger(name);
|
|
||||||
form.Dirty;
|
|
||||||
}}
|
}}
|
||||||
value={
|
value={
|
||||||
(inputProps as any)?.isMulti
|
(inputProps as any)?.isMulti
|
||||||
? (inputProps as any).options?.filter((o: any) => form.watch(name)?.includes(o.value))
|
? (inputProps as any).options?.filter(
|
||||||
: (inputProps as any).options?.find((o: any) => o.value === form.watch(name))
|
(o: any) => Array.isArray(value) && value.includes(o.value),
|
||||||
|
)
|
||||||
|
: (inputProps as any).options?.find((o: any) => o.value === value)
|
||||||
}
|
}
|
||||||
styles={customStyles as any}
|
styles={customStyles as any}
|
||||||
className={cn("w-full placeholder:text-neutral-600", className)}
|
className={cn("w-full placeholder:text-neutral-600", className)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
{...inputProps}
|
{...inputProps}
|
||||||
/>
|
/>
|
||||||
{form.formState.errors[name]?.message && (
|
{error && <p className="text-error">{error}</p>}
|
||||||
<p className="text-error">{form.formState.errors[name].message as string}</p>
|
|
||||||
)}
|
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
const SelectWrapper = <T extends FieldValues>(props: SelectProps<T>) => <SelectCom {...props} />;
|
const SelectWrapper = <T extends FieldValues>(props: SelectProps) => <SelectCom {...props} />;
|
||||||
|
|
||||||
export const Select = dynamic(() => Promise.resolve(SelectWrapper), {
|
export const Select = dynamic(() => Promise.resolve(SelectWrapper), {
|
||||||
ssr: false,
|
ssr: false,
|
||||||
|
|||||||
@@ -68,7 +68,7 @@ export const Chat = () => {
|
|||||||
{chatOpen && (
|
{chatOpen && (
|
||||||
<div
|
<div
|
||||||
tabIndex={0}
|
tabIndex={0}
|
||||||
className="dropdown-content card bg-base-200 w-150 shadow-md z-[1100] max-h-[400px] ml-2 border-1 border-primary"
|
className="dropdown-content card bg-base-200 w-150 shadow-md z-[1100] max-h-[480px] ml-2 border-1 border-primary"
|
||||||
>
|
>
|
||||||
<div className="card-body relative">
|
<div className="card-body relative">
|
||||||
<button
|
<button
|
||||||
@@ -140,6 +140,7 @@ export const Chat = () => {
|
|||||||
{chat.notification && <span className="indicator-item status status-info" />}
|
{chat.notification && <span className="indicator-item status status-info" />}
|
||||||
</a>
|
</a>
|
||||||
<div className="tab-content bg-base-100 border-base-300 p-6 overflow-y-auto max-h-[250px]">
|
<div className="tab-content bg-base-100 border-base-300 p-6 overflow-y-auto max-h-[250px]">
|
||||||
|
{/* So macht man kein overflow handeling, weiß ich. Aber es funktioniert... */}
|
||||||
{chat.messages.map((chatMessage) => {
|
{chat.messages.map((chatMessage) => {
|
||||||
const isSender = chatMessage.senderId === session.data?.user.id;
|
const isSender = chatMessage.senderId === session.data?.user.id;
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { usePannelStore } from "_store/pannelStore";
|
import { usePannelStore } from "_store/pannelStore";
|
||||||
import { Control, Icon, LatLngExpression } from "leaflet";
|
import { Control, Icon, LatLngExpression } from "leaflet";
|
||||||
import { useEffect, useRef, useState } from "react";
|
import { useEffect, useMemo, useRef, useState } from "react";
|
||||||
import {
|
import {
|
||||||
LayerGroup,
|
LayerGroup,
|
||||||
LayersControl,
|
LayersControl,
|
||||||
@@ -73,9 +73,9 @@ const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) =>
|
|||||||
queryKey: ["stations"],
|
queryKey: ["stations"],
|
||||||
queryFn: () => getStationsAPI(),
|
queryFn: () => getStationsAPI(),
|
||||||
});
|
});
|
||||||
|
console.log("StationsLayer: stations", stations);
|
||||||
|
|
||||||
const [selectedStations, setSelectedStations] = useState<Station["id"][]>([]);
|
const [selectedStations, setSelectedStations] = useState<Station["id"][]>([]);
|
||||||
const [stationsWithIcon, setStationsWithIcon] = useState<(Station & { icon?: string })[]>([]); // Zustand für die Stationen mit Icon
|
|
||||||
const attributionText = "";
|
const attributionText = "";
|
||||||
|
|
||||||
const resetSelection = () => {
|
const resetSelection = () => {
|
||||||
@@ -94,27 +94,29 @@ const StationsLayer = ({ attribution }: { attribution: Control.Attribution }) =>
|
|||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
const [stationsWithIcon, setStationsWithIcon] = useState<(Station & { icon: string })[]>([]);
|
||||||
// Erstelle die Icons für alle Stationen
|
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (!stations) {
|
||||||
|
setStationsWithIcon([]);
|
||||||
|
return;
|
||||||
|
}
|
||||||
const fetchIcons = async () => {
|
const fetchIcons = async () => {
|
||||||
if (!stations) return;
|
|
||||||
const urls = await Promise.all(
|
const urls = await Promise.all(
|
||||||
stations.map(async (station) => {
|
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();
|
fetchIcons();
|
||||||
}, [stations]);
|
}, [stations]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<FeatureGroup>
|
<FeatureGroup>
|
||||||
{stationsWithIcon.map((station) => {
|
{stationsWithIcon?.map((station) => {
|
||||||
const coordinates: LatLngExpression = [station.latitude, station.longitude];
|
const coordinates: LatLngExpression = [station.latitude, station.longitude];
|
||||||
const typeLabel = station.bosUse.charAt(0).toUpperCase();
|
const typeLabel = station.bosUse?.charAt(0).toUpperCase();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<Marker
|
<Marker
|
||||||
|
|||||||
@@ -48,6 +48,7 @@ import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
|
|||||||
import { getOsmAddress } from "_querys/osm";
|
import { getOsmAddress } from "_querys/osm";
|
||||||
import { hpgStateToFMSStatus } from "_helpers/hpgStateToFmsStatus";
|
import { hpgStateToFMSStatus } from "_helpers/hpgStateToFmsStatus";
|
||||||
import { cn } from "@repo/shared-components";
|
import { cn } from "@repo/shared-components";
|
||||||
|
import { StationsSelect } from "(app)/dispatch/_components/StationSelect";
|
||||||
|
|
||||||
const Einsatzdetails = ({
|
const Einsatzdetails = ({
|
||||||
mission,
|
mission,
|
||||||
@@ -420,39 +421,6 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
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 dispatcherConnected = useDispatchConnectionStore((s) => s.status) === "connected";
|
||||||
|
|
||||||
const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => (
|
const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => (
|
||||||
@@ -503,70 +471,57 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
|||||||
const connectedAircraft = connectedAircrafts?.find(
|
const connectedAircraft = connectedAircrafts?.find(
|
||||||
(aircraft) => aircraft.stationId === station.id,
|
(aircraft) => aircraft.stationId === station.id,
|
||||||
);
|
);
|
||||||
|
console.log("connectedAircraft", connectedAircraft);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<li key={index} className="flex items-center gap-2">
|
<li key={index} className="flex items-center gap-2">
|
||||||
{connectedAircraft && (
|
|
||||||
<span
|
<span
|
||||||
className="font-bold text-base"
|
className="font-bold text-base"
|
||||||
style={{
|
style={{
|
||||||
color: FMS_STATUS_TEXT_COLORS[connectedAircraft.fmsStatus],
|
color: FMS_STATUS_TEXT_COLORS[connectedAircraft?.fmsStatus || "6"],
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{connectedAircraft.fmsStatus}
|
{connectedAircraft?.fmsStatus || "6"}
|
||||||
</span>
|
</span>
|
||||||
)}
|
<span className="text-base-content flex flex-col ">
|
||||||
<span className="text-base-content">
|
|
||||||
<div>
|
|
||||||
<span className="font-bold">{station.bosCallsign}</span>
|
<span className="font-bold">{station.bosCallsign}</span>
|
||||||
{/* {item.min > 0 && (
|
{!connectedAircraft && (
|
||||||
<>
|
<span className="text-gray-400 text-xs">Kein Benutzer verbunden</span>
|
||||||
<br />
|
)}
|
||||||
Ankunft in ca. {item.min} min
|
|
||||||
</>
|
|
||||||
)} */}
|
|
||||||
</div>
|
|
||||||
</span>
|
</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
})}
|
})}
|
||||||
{mission.hpgAmbulanceState && <HPGVehicle state={mission.hpgAmbulanceState} name="RTW" />}
|
{mission.hpgAmbulanceState && mission.hpgAmbulanceState !== "NOT_REQUESTED" && (
|
||||||
{mission.hpgFireEngineState && (
|
<HPGVehicle state={mission.hpgAmbulanceState} name="RTW" />
|
||||||
|
)}
|
||||||
|
{mission.hpgFireEngineState && mission.hpgFireEngineState !== "NOT_REQUESTED" && (
|
||||||
<HPGVehicle state={mission.hpgFireEngineState} name="Feuerwehr" />
|
<HPGVehicle state={mission.hpgFireEngineState} name="Feuerwehr" />
|
||||||
)}
|
)}
|
||||||
{mission.hpgPoliceState && <HPGVehicle state={mission.hpgPoliceState} name="Polizei" />}
|
{mission.hpgPoliceState && mission.hpgPoliceState !== "NOT_REQUESTED" && (
|
||||||
|
<HPGVehicle state={mission.hpgPoliceState} name="Polizei" />
|
||||||
|
)}
|
||||||
</ul>
|
</ul>
|
||||||
{dispatcherConnected && (
|
{dispatcherConnected && (
|
||||||
<div>
|
<div>
|
||||||
<div className="divider mt-0 mb-0" />
|
<div className="divider mt-0 mb-0" />
|
||||||
<div className="flex items-center gap-2">
|
<div className="flex items-center gap-2">
|
||||||
{/* TODO: make it a small multiselect */}
|
{/* TODO: make it a small multiselect */}
|
||||||
<select
|
<StationsSelect
|
||||||
className="select select-sm select-primary select-bordered flex-1"
|
menuPlacement="top"
|
||||||
onChange={(e) => {
|
className="min-w-[320px] flex-1"
|
||||||
const value = e.target.value;
|
isMulti={false}
|
||||||
const parsedValue = !isNaN(Number(value)) ? parseInt(value, 10) : value;
|
onChange={(v: any) => {
|
||||||
setSelectedStation(parsedValue as number | "RTW" | "POL" | "FW" | null);
|
setSelectedStation(v);
|
||||||
}}
|
}}
|
||||||
value={selectedStation || "default"}
|
selectedStations={mission.missionStationIds}
|
||||||
>
|
filterSelected
|
||||||
<option disabled value={"default"}>
|
vehicleStates={{
|
||||||
Rettungsmittel auswählen
|
hpgAmbulanceState: mission.hpgAmbulanceState || undefined,
|
||||||
</option>
|
hpgFireEngineState: mission.hpgFireEngineState || undefined,
|
||||||
{stationsOptions.map((option) => (
|
hpgPoliceState: mission.hpgPoliceState || undefined,
|
||||||
<option
|
}}
|
||||||
key={option.value}
|
/>
|
||||||
value={option.value}
|
|
||||||
className={cn(
|
|
||||||
"flex gap-2",
|
|
||||||
"isOnline" in option && option?.isOnline && "text-green-500",
|
|
||||||
)}
|
|
||||||
>
|
|
||||||
{option.label}
|
|
||||||
{"isOnline" in option && option?.isOnline && " (Online)"}
|
|
||||||
</option>
|
|
||||||
))}
|
|
||||||
</select>
|
|
||||||
<button
|
<button
|
||||||
className="btn btn-sm btn-primary btn-outline"
|
className="btn btn-sm btn-primary btn-outline"
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
|
|||||||
@@ -58,8 +58,8 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
|||||||
/>
|
/>
|
||||||
<Input
|
<Input
|
||||||
form={form}
|
form={form}
|
||||||
label="zunehmende Atembeschwerden"
|
label="Beschreibung"
|
||||||
placeholder="Beschreibung"
|
placeholder="zunehmende Atembeschwerden"
|
||||||
name="description"
|
name="description"
|
||||||
className="input-sm"
|
className="input-sm"
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -21,6 +21,7 @@ export const ListInput = <T extends FieldValues>({
|
|||||||
const [value, setValue] = useState<string>("");
|
const [value, setValue] = useState<string>("");
|
||||||
return (
|
return (
|
||||||
<Controller
|
<Controller
|
||||||
|
defaultValue={[] as any}
|
||||||
control={control}
|
control={control}
|
||||||
name={name}
|
name={name}
|
||||||
render={({ field }) => {
|
render={({ field }) => {
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ remote_write:
|
|||||||
scrape_configs:
|
scrape_configs:
|
||||||
- job_name: core-server
|
- job_name: core-server
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["host.docker.internal:3000"]
|
- targets: ["host.docker.internal:3005"]
|
||||||
- job_name: Kuma-Status
|
- job_name: Kuma-Status
|
||||||
static_configs:
|
static_configs:
|
||||||
- targets: ["status.virtualairrescue.com"]
|
- targets: ["status.virtualairrescue.com"]
|
||||||
|
|||||||
Reference in New Issue
Block a user