diff --git a/apps/dispatch/app/_components/Select.tsx b/apps/dispatch/app/_components/Select.tsx new file mode 100644 index 00000000..6752abb8 --- /dev/null +++ b/apps/dispatch/app/_components/Select.tsx @@ -0,0 +1,119 @@ +"use client"; +import { + FieldValues, + Path, + RegisterOptions, + UseFormReturn, +} from "react-hook-form"; +import SelectTemplate, { + Props as SelectTemplateProps, + StylesConfig, +} from "react-select"; +import { cn } from "helpers/cn"; +import dynamic from "next/dynamic"; +import { CSSProperties } from "react"; + +interface SelectProps + extends Omit { + label?: any; + name: Path; + form: UseFormReturn | any; + formOptions?: RegisterOptions; + // eslint-disable-next-line @typescript-eslint/no-explicit-any +} + +const customStyles: StylesConfig = { + control: (provided) => ({ + ...provided, + backgroundColor: "var(--color-base-100)", + borderColor: "color-mix(in oklab, var(--color-base-content) 20%, #0000);", + borderRadius: "0.5rem", + padding: "0.25rem", + boxShadow: "none", + "&:hover": { + borderColor: "color-mix(in oklab, var(--color-base-content) 20%, #0000);", + }, + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isSelected ? "hsl(var(--p))" : "hsl(var(--b1))", + color: "var(--color-primary-content)", + "&:hover": { backgroundColor: "var(--color-base-200)" }, // DaisyUI secondary color + }), + multiValueLabel: (provided) => ({ + ...provided, + color: "var(--color-primary-content)", + }), + singleValue: (provided) => ({ + ...provided, + color: "var(--color-primary-content)", + }), + multiValue: (provided) => ({ + ...provided, + backgroundColor: "var(--color-base-300)", + }), + menu: (provided) => ({ + ...provided, + backgroundColor: "var(--color-base-100)", + borderRadius: "0.5rem", + }), +}; + +const SelectCom = ({ + name, + label = name, + placeholder = label, + form, + formOptions, + className, + ...inputProps +}: SelectProps) => { + return ( +
+ + {label} + + { + if (Array.isArray(newValue)) { + form.setValue(name, newValue.map((v: any) => v.value) as any, { + shouldDirty: true, + }); + } else { + form.setValue(name, newValue.value, { + shouldDirty: true, + }); + } + 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), + ) + } + 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} +

+ )} +
+ ); +}; + +const SelectWrapper = (props: SelectProps) => ( + +); + +export const Select = dynamic(() => Promise.resolve(SelectWrapper), { + ssr: false, +}); diff --git a/apps/dispatch/app/_store/mapStore.ts b/apps/dispatch/app/_store/mapStore.ts index c2b1bbf4..46a75180 100644 --- a/apps/dispatch/app/_store/mapStore.ts +++ b/apps/dispatch/app/_store/mapStore.ts @@ -58,6 +58,13 @@ interface MapStore { aircraftId: string, tab: MapStore["aircraftTabs"][string], ) => void; + missionTabs: { + [missionId: string]: "home" | "details" | "chat"; + }; + setMissionTab: ( + missionId: string, + tab: MapStore["missionTabs"][string], + ) => void; } export const useMapStore = create((set, get) => ({ @@ -106,4 +113,12 @@ export const useMapStore = create((set, get) => ({ [aircraftId]: tab, }, })), + missionTabs: {}, + setMissionTab: (missionId, tab) => + set((state) => ({ + missionTabs: { + ...state.missionTabs, + [missionId]: tab, + }, + })), })); diff --git a/apps/dispatch/app/_store/missionsStore.ts b/apps/dispatch/app/_store/missionsStore.ts index d8e46101..3b965ada 100644 --- a/apps/dispatch/app/_store/missionsStore.ts +++ b/apps/dispatch/app/_store/missionsStore.ts @@ -6,6 +6,7 @@ interface MissionStore { missions: MissionOptionalDefaults[]; setMissions: (missions: MissionOptionalDefaults[]) => void; getMissions: () => Promise; + setMission: (mission: MissionOptionalDefaults) => void; } export const useMissionsStore = create((set) => ({ @@ -47,4 +48,17 @@ export const useMissionsStore = create((set) => ({ set({ missions: data }); return undefined; }, + setMission: (mission) => + set((state) => { + const existingMissionIndex = state.missions.findIndex( + (m) => m.id === mission.id, + ); + if (existingMissionIndex !== -1) { + const updatedMissions = [...state.missions]; + updatedMissions[existingMissionIndex] = mission; + return { missions: updatedMissions }; + } else { + return { missions: [...state.missions, mission] }; + } + }), })); diff --git a/apps/dispatch/app/_store/pannelStore.ts b/apps/dispatch/app/_store/pannelStore.ts index c60f76f6..615ffbbf 100644 --- a/apps/dispatch/app/_store/pannelStore.ts +++ b/apps/dispatch/app/_store/pannelStore.ts @@ -7,6 +7,6 @@ interface PannelStore { } export const usePannelStore = create((set) => ({ - isOpen: false, + isOpen: true, // DEBUG, REMOVE LATER FOR PROD setOpen: (isOpen) => set({ isOpen }), })); diff --git a/apps/dispatch/app/dispatch/_components/map/MissionMarkers.tsx b/apps/dispatch/app/dispatch/_components/map/MissionMarkers.tsx index 869e990c..55e7ed85 100644 --- a/apps/dispatch/app/dispatch/_components/map/MissionMarkers.tsx +++ b/apps/dispatch/app/dispatch/_components/map/MissionMarkers.tsx @@ -2,11 +2,19 @@ import { useMissionsStore } from "_store/missionsStore"; import { Marker, Popup, useMap } from "react-leaflet"; import { DivIcon, Marker as LMarker, Popup as LPopup } from "leaflet"; import { useMapStore } from "_store/mapStore"; -import { Fragment, useCallback, useEffect, useRef, useState } from "react"; +import { + Fragment, + useCallback, + useEffect, + useMemo, + useRef, + useState, +} from "react"; import { cn } from "helpers/cn"; import { Cross, House, Minimize2, Route } from "lucide-react"; import { SmartPopup, useConflict, useSmartPopup } from "_components/SmartPopup"; import { Mission, MissionState } from "@repo/db"; +import FMSStatusHistory from "./_components/MissionMarkerTabs"; export const MISSION_STATUS_COLORS: Record = { draft: "#0092b8", @@ -21,10 +29,38 @@ export const MISSION_STATUS_TEXT_COLORS: Record = { }; const MissionPopupContent = ({ mission }: { mission: Mission }) => { + const setMissionTab = useMapStore((state) => state.setMissionTab); + const currentTab = useMapStore( + (state) => state.missionTabs[mission.id] || "home", + ); + + const handleTabChange = useCallback( + (tab: "home" | "details" | "chat") => { + if (currentTab !== tab) { + setMissionTab(mission.id, tab); + } + }, + [currentTab, mission.id, setMissionTab], + ); + + const renderTabContent = useMemo(() => { + switch (currentTab) { + case "home": + return ; + case "details": + return
Details Content
; + case "chat": + return
Chat Content
; + default: + return Error; + } + }, [currentTab]); + const setOpenMissionMarker = useMapStore( (state) => state.setOpenMissionMarker, ); const { anchor } = useSmartPopup(); + return ( <>
{ }); }} > - +
{ }} >
handleTabChange("home")} > - +
handleTabChange("details")} > - +
handleTabChange("chat")} > - {mission.state} -
-
- Einsatz 250411 +
+
{renderTabContent}
); }; @@ -231,9 +274,11 @@ const MissionMarker = ({ mission }: { mission: Mission }) => { closeOnClick={false} autoPan={false} wrapperClassName="relative" - className="w-[300px] h-[150px]" + className="w-[502px]" > - +
+ +
)} diff --git a/apps/dispatch/app/dispatch/_components/map/_components/MissionMarkerTabs.tsx b/apps/dispatch/app/dispatch/_components/map/_components/MissionMarkerTabs.tsx new file mode 100644 index 00000000..4c55daf4 --- /dev/null +++ b/apps/dispatch/app/dispatch/_components/map/_components/MissionMarkerTabs.tsx @@ -0,0 +1,58 @@ +"use client"; +import React from "react"; +import { FMS_STATUS_TEXT_COLORS } from "../AircraftMarker"; + +const FMSStatusHistory = () => { + const dummyData = [ + { status: "3", time: "12:00", unit: "RTW", unitshort: "RTW" }, + { status: "3", time: "12:01", unit: "Christoph 31", unitshort: "CHX31" }, + { status: "4", time: "12:09", unit: "RTW", unitshort: "RTW" }, + { status: "4", time: "12:11", unit: "Christoph 31", unitshort: "CHX31" }, + { status: "7", time: "12:34", unit: "RTW", unitshort: "RTW" }, + { status: "1", time: "12:38", unit: "Christoph 31", unitshort: "CHX31" }, + ]; + + return ( +
+
+ {[ + ...new Map(dummyData.map((entry) => [entry.unit, entry])).values(), + ].map((entry, index, array) => ( + +
+ + {entry.status} + + {entry.unitshort} +
+ {index < array.length - 1 && |} +
+ ))} +
+
+
    + {dummyData.map((entry, index) => ( +
  • + {entry.time} + + {entry.status} + + {entry.unit} +
  • + ))} +
+
+ ); +}; + +export default FMSStatusHistory; diff --git a/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx b/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx new file mode 100644 index 00000000..95f39d37 --- /dev/null +++ b/apps/dispatch/app/dispatch/_components/pannel/MissionForm.tsx @@ -0,0 +1,215 @@ +"use client"; +import React, { useState } from "react"; +import { useForm, Controller } from "react-hook-form"; +import { zodResolver } from "@hookform/resolvers/zod"; +import { z } from "zod"; +import { MissionSchema } from "@repo/db/zod"; +import { BellRing, BookmarkPlus, Trash2 } from "lucide-react"; +import { Select } from "_components/Select"; + +const clearBtn = () => { + return ( + + ); +}; + +const missionFormSchema = MissionSchema.pick({ + addressLat: true, + addressLng: true, + addressStreet: true, + addressCity: true, + addressZip: true, + missionCategory: true, + missionKeyword: true, + missionAdditionalInfo: true, + missionPatientInfo: true, +}); + +type MissionFormValues = z.infer; + +const dummyRettungsmittel = [ + "RTW", + "Feuerwehr", + "Polizei", + "Christoph 31", + "Christoph 100", + "Christoph Berlin", + "Christophorus 1", +]; + +export const MissionForm: React.FC = () => { + const [missionCategory, setMissionCategory] = useState<"PRIMÄR" | "SEKUNDÄR">( + "PRIMÄR", + ); + const [missionKeyword, setMissionKeyword] = useState< + "AB_ATMUNG" | "C_BLUTUNG" + >("AB_ATMUNG"); + const [missionType, setMissionType] = useState<"typ1" | "typ2" | "typ3">( + "typ1", + ); + const [selectedRettungsmittel, setSelectedRettungsmittel] = useState< + { label: string; value: string }[] + >([]); + const form = useForm({ + resolver: zodResolver(missionFormSchema), + defaultValues: { + addressLat: 0, + addressLng: 0, + missionCategory: "PRIMÄR", + }, + }); + + const onSubmit = (data: MissionFormValues) => { + console.log({ + ...data, + rettungsmittel: selectedRettungsmittel.map((item) => item.value), + }); + }; + + return ( +
+ {/* Koorinaten Section */} +
+

Koordinaten

+
+ + +
+
+ + {/* Adresse Section */} +
+

Adresse

+ +
+ + +
+ +
+ + {/* Rettungsmittel Section */} +
+

Rettungsmittel

+ + setMissionCategory(e.target.value as "PRIMÄR" | "SEKUNDÄR") + } + > + + + + + +