Made Mission marker DB compatible

This commit is contained in:
PxlLoewe
2025-04-24 22:32:18 -07:00
parent 46cbdf6bb9
commit 5bca37182d
22 changed files with 445 additions and 187 deletions

View File

@@ -25,6 +25,7 @@ io.on("connection", (socket) => {
}); });
app.use(cors()); app.use(cors());
app.use(express.json());
app.use(router); app.use(router);
server.listen(process.env.PORT, () => { server.listen(process.env.PORT, () => {

View File

@@ -0,0 +1,79 @@
import { prisma } from "@repo/db";
import { Router } from "express";
const router = Router();
// Get all missions
router.post("/", async (req, res) => {
try {
const filter = req.body?.filter || {};
const missions = await prisma.mission.findMany({
where: filter,
});
res.json(missions);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to fetch missions" });
}
});
// Get a single mission by ID
router.get("/:id", async (req, res) => {
const { id } = req.params;
try {
const mission = await prisma.mission.findUnique({
where: { id: Number(id) },
});
if (mission) {
res.json(mission);
} else {
res.status(404).json({ error: "Mission not found" });
}
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to fetch mission" });
}
});
// Create a new mission
router.put("/", async (req, res) => {
try {
const newMission = await prisma.mission.create({
data: req.body,
});
res.status(201).json(newMission);
} catch (error) {
res.status(500).json({ error: "Failed to create mission" });
}
});
// Update a mission by ID
router.patch("/:id", async (req, res) => {
const { id } = req.params;
try {
const updatedMission = await prisma.mission.update({
where: { id: Number(id) },
data: req.body,
});
res.json(updatedMission);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to update mission" });
}
});
// Delete a mission by ID
router.delete("/:id", async (req, res) => {
const { id } = req.params;
try {
await prisma.mission.delete({
where: { id: Number(id) },
});
res.status(204).send();
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to delete mission" });
}
});
export default router;

View File

@@ -1,10 +1,12 @@
import { Router } from "express"; import { Router } from "express";
import livekitRouter from "./livekit"; import livekitRouter from "./livekit";
import dispatcherRotuer from "./dispatcher"; import dispatcherRotuer from "./dispatcher";
import missionRouter from "./mission";
const router = Router(); const router = Router();
router.use("/livekit", livekitRouter); router.use("/livekit", livekitRouter);
router.use("/dispatcher", dispatcherRotuer); router.use("/dispatcher", dispatcherRotuer);
router.use("/mission", missionRouter);
export default router; export default router;

View File

@@ -16,7 +16,7 @@ export const useSmartPopup = () => {
return context; return context;
}; };
export const useConflict = (id: string, mode: "popup" | "marker") => { export const calculateAnchor = (id: string, mode: "popup" | "marker") => {
const otherMarkers = document.querySelectorAll(".map-collision"); const otherMarkers = document.querySelectorAll(".map-collision");
// get markers and check if they are overlapping // get markers and check if they are overlapping
const ownMarker = const ownMarker =
@@ -105,7 +105,7 @@ export const SmartPopup = (
>("topleft"); >("topleft");
const handleConflict = () => { const handleConflict = () => {
const newAnchor = useConflict(id, "popup"); const newAnchor = calculateAnchor(id, "popup");
setAnchor(newAnchor); setAnchor(newAnchor);
}; };

View File

@@ -11,12 +11,12 @@ interface MapStore {
zoom: number; zoom: number;
}; };
openMissionMarker: { openMissionMarker: {
id: string; id: number;
tab: "home" | ""; tab: "home" | "details" | "chat" | "log";
}[]; }[];
setOpenMissionMarker: (mission: { setOpenMissionMarker: (mission: {
open: MapStore["openMissionMarker"]; open: MapStore["openMissionMarker"];
close: string[]; close: number[];
}) => void; }) => void;
openAircraftMarker: { openAircraftMarker: {
id: string; id: string;
@@ -58,22 +58,16 @@ interface MapStore {
aircraftId: string, aircraftId: string,
tab: MapStore["aircraftTabs"][string], tab: MapStore["aircraftTabs"][string],
) => void; ) => void;
missionTabs: {
[missionId: string]: "home" | "details" | "chat";
};
setMissionTab: (
missionId: string,
tab: MapStore["missionTabs"][string],
) => void;
} }
export const useMapStore = create<MapStore>((set, get) => ({ export const useMapStore = create<MapStore>((set, get) => ({
openMissionMarker: [], openMissionMarker: [],
setOpenMissionMarker: ({ open, close }) => { setOpenMissionMarker: ({ open, close }) => {
set((state) => ({ const oldMarkers = get().openMissionMarker.filter(
openMissionMarker: [...state.openMissionMarker, ...open].filter( (m) => !close.includes(m.id) && !open.find((o) => o.id === m.id),
(marker) => !close.includes(marker.id), );
), set(() => ({
openMissionMarker: [...oldMarkers, ...open],
})); }));
}, },
openAircraftMarker: [], openAircraftMarker: [],
@@ -81,7 +75,7 @@ export const useMapStore = create<MapStore>((set, get) => ({
const oldMarkers = get().openAircraftMarker.filter( const oldMarkers = get().openAircraftMarker.filter(
(m) => !close.includes(m.id) && !open.find((o) => o.id === m.id), (m) => !close.includes(m.id) && !open.find((o) => o.id === m.id),
); );
console.log("oldMarkers", oldMarkers, open);
set(() => ({ set(() => ({
openAircraftMarker: [...oldMarkers, ...open], openAircraftMarker: [...oldMarkers, ...open],
})); }));
@@ -114,11 +108,4 @@ export const useMapStore = create<MapStore>((set, get) => ({
}, },
})), })),
missionTabs: {}, missionTabs: {},
setMissionTab: (missionId, tab) =>
set((state) => ({
missionTabs: {
...state.missionTabs,
[missionId]: tab,
},
})),
})); }));

View File

@@ -1,46 +1,20 @@
import { Mission, Prisma } from "@repo/db"; import { Mission, Prisma } from "@repo/db";
import { MissionOptionalDefaults } from "@repo/db/zod"; import { MissionOptionalDefaults } from "@repo/db/zod";
import { serverApi } from "helpers/axios";
import { create } from "zustand"; import { create } from "zustand";
import { toast } from "react-hot-toast";
interface MissionStore { interface MissionStore {
missions: Mission[]; missions: Mission[];
setMissions: (missions: Mission[]) => void; setMissions: (missions: Mission[]) => void;
getMissions: () => Promise<undefined>; getMissions: () => Promise<undefined>;
createMission: (mission: MissionOptionalDefaults) => Promise<Mission>; createMission: (mission: MissionOptionalDefaults) => Promise<Mission>;
setMission: (mission: Mission) => void; deleteMission: (id: number) => Promise<void>;
editMission: (id: number, mission: Partial<Mission>) => Promise<void>;
} }
export const useMissionsStore = create<MissionStore>((set) => ({ export const useMissionsStore = create<MissionStore>((set) => ({
missions: [ missions: [],
{
id: 1,
type: "primär",
state: "draft",
addressCity: "Berlin",
addressStreet: "Alexanderplatz",
addressZip: "10178",
addressOSMways: [],
missionAdditionalInfo: "",
addressLat: 52.520008,
addressLng: 13.404954,
missionKeywordName: "TestKName",
missionKeywordCategory: "TestKCategory",
missionKeywordAbbreviation: "TestKAbbreviation",
missionPatientInfo: "TestPatientInfo",
missionStationIds: [],
createdUserId: "1",
hpgMissionString: null,
missionLog: [],
missionStationUserIds: [],
hpgLocationLat: 52.520008,
hpgLocationLng: 13.404954,
hpgAmbulanceState: null,
hpgFireEngineState: null,
hpgPoliceState: null,
createdAt: new Date(),
updatedAt: new Date(),
},
],
setMissions: (missions) => set({ missions }), setMissions: (missions) => set({ missions }),
createMission: async (mission) => { createMission: async (mission) => {
const res = await fetch("/api/mission", { const res = await fetch("/api/mission", {
@@ -55,41 +29,46 @@ export const useMissionsStore = create<MissionStore>((set) => ({
set((state) => ({ missions: [...state.missions, data] })); set((state) => ({ missions: [...state.missions, data] }));
return data; return data;
}, },
editMission: async (id, mission) => {
const { data, status } = await serverApi.patch<Mission>(
`/mission/${id}`,
mission,
);
if (status.toString().startsWith("2") && data) {
set((state) => ({
missions: state.missions.map((m) => (m.id === id ? data : m)),
}));
toast.success("Mission updated successfully");
} else {
toast.error("Failed to update mission");
}
},
deleteMission: async (id) => {
serverApi
.delete(`/mission/${id}`)
.then((res) => {
if (res.status.toString().startsWith("2")) {
set((state) => ({
missions: state.missions.filter((mission) => mission.id !== id),
}));
toast.success("Mission deleted successfully");
} else {
toast.error("Failed to delete mission");
}
})
.catch((err) => {
toast.error("Failed to delete mission");
});
},
getMissions: async () => { getMissions: async () => {
const res = await fetch("/api/mission", { const { data } = await serverApi.post<Mission[]>("/mission", {
method: "POST", filter: {
headers: { OR: [{ state: "draft" }, { state: "running" }],
"Content-Type": "application/json", } as Prisma.MissionWhereInput,
},
body: JSON.stringify({
OR: [
{
state: "draft",
},
{
state: "running",
},
],
} as Prisma.MissionWhereInput),
}); });
if (!res.ok) return undefined; set({ missions: data });
const data = await res.json();
//set({ missions: data });
return undefined; 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] };
}
}),
})); }));
useMissionsStore useMissionsStore

View File

@@ -17,7 +17,11 @@ import {
MessageSquareText, MessageSquareText,
Minimize2, Minimize2,
} from "lucide-react"; } from "lucide-react";
import { SmartPopup, useConflict, useSmartPopup } from "_components/SmartPopup"; import {
SmartPopup,
calculateAnchor,
useSmartPopup,
} from "_components/SmartPopup";
import FMSStatusHistory, { import FMSStatusHistory, {
FMSStatusSelector, FMSStatusSelector,
RettungsmittelTab, RettungsmittelTab,
@@ -249,24 +253,25 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
}); });
} }
}; };
markerRef.current?.on("click", handleClick); const marker = markerRef.current;
marker?.on("click", handleClick);
return () => { return () => {
markerRef.current?.off("click", handleClick); marker?.off("click", handleClick);
}; };
}, [markerRef.current, aircraft.id, openAircraftMarker]); }, [aircraft.id, openAircraftMarker, setOpenAircraftMarker]);
const [anchor, setAnchor] = useState< const [anchor, setAnchor] = useState<
"topleft" | "topright" | "bottomleft" | "bottomright" "topleft" | "topright" | "bottomleft" | "bottomright"
>("topleft"); >("topleft");
const handleConflict = () => { const handleConflict = useCallback(() => {
const newAnchor = useConflict(aircraft.id, "marker"); const newAnchor = calculateAnchor(aircraft.id, "marker");
setAnchor(newAnchor); setAnchor(newAnchor);
}; }, [aircraft.id]);
useEffect(() => { useEffect(() => {
handleConflict(); handleConflict();
}, [aircrafts, openAircraftMarker]); }, [aircrafts, openAircraftMarker, handleConflict]);
useEffect(() => {}); useEffect(() => {});
@@ -279,7 +284,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
return () => { return () => {
map.off("zoom", handleConflict); map.off("zoom", handleConflict);
}; };
}, [map, openAircraftMarker]); }, [map, openAircraftMarker, handleConflict]);
const getMarkerHTML = ( const getMarkerHTML = (
aircraft: Aircraft, aircraft: Aircraft,

View File

@@ -12,7 +12,11 @@ import {
} from "react"; } from "react";
import { cn } from "helpers/cn"; import { cn } from "helpers/cn";
import { ClipboardList, Cross, House, Minimize2, Route } from "lucide-react"; import { ClipboardList, Cross, House, Minimize2, Route } from "lucide-react";
import { SmartPopup, useConflict, useSmartPopup } from "_components/SmartPopup"; import {
calculateAnchor,
SmartPopup,
useSmartPopup,
} from "_components/SmartPopup";
import { Mission, MissionState } from "@repo/db"; import { Mission, MissionState } from "@repo/db";
import Einsatzdetails, { import Einsatzdetails, {
FMSStatusHistory, FMSStatusHistory,
@@ -31,36 +35,42 @@ export const MISSION_STATUS_TEXT_COLORS: Record<MissionState, string> = {
}; };
const MissionPopupContent = ({ mission }: { mission: Mission }) => { const MissionPopupContent = ({ mission }: { mission: Mission }) => {
const setMissionTab = useMapStore((state) => state.setMissionTab); const setMissionMarker = useMapStore((state) => state.setOpenMissionMarker);
const currentTab = useMapStore( const currentTab = useMapStore(
(state) => (state) =>
state.missionTabs[mission.id] || state.openMissionMarker.find((m) => m.id === mission.id)?.tab ?? "home",
("home" as "home" | "details" | "chat" | "log"),
); );
const handleTabChange = useCallback( const handleTabChange = useCallback(
(tab: "home" | "details" | "chat" | "log") => { (tab: "home" | "details" | "chat" | "log") => {
if (currentTab !== tab) { console.log("handleTabChange", tab);
setMissionTab(mission.id, tab); setMissionMarker({
} open: [
{
id: mission.id,
tab,
},
],
close: [],
});
}, },
[currentTab, mission.id, setMissionTab], [setMissionMarker, mission.id],
); );
const renderTabContent = useMemo(() => { const renderTabContent = useMemo(() => {
switch (currentTab) { switch (currentTab) {
case "home": case "home":
return <Einsatzdetails />; return <Einsatzdetails mission={mission} />;
case "details": case "details":
return <div>Details Content</div>; return <div>Details Content</div>;
case "chat": case "chat":
return <div>Chat Content</div>; return <div>Chat Content</div>;
case "log": case "log":
return <FMSStatusHistory />; return <FMSStatusHistory mission={mission} />;
default: default:
return <span className="text-gray-100">Error</span>; return <span className="text-gray-100">Error</span>;
} }
}, [currentTab]); }, [currentTab, mission]);
const setOpenMissionMarker = useMapStore( const setOpenMissionMarker = useMapStore(
(state) => state.setOpenMissionMarker, (state) => state.setOpenMissionMarker,
@@ -169,7 +179,6 @@ const MissionPopupContent = ({ mission }: { mission: Mission }) => {
}; };
const MissionMarker = ({ mission }: { mission: Mission }) => { const MissionMarker = ({ mission }: { mission: Mission }) => {
const missions = useMissionsStore((state) => state.missions);
const map = useMap(); const map = useMap();
const markerRef = useRef<LMarker>(null); const markerRef = useRef<LMarker>(null);
const popupRef = useRef<LPopup>(null); const popupRef = useRef<LPopup>(null);
@@ -198,24 +207,25 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
}); });
} }
}; };
markerRef.current?.on("click", handleClick); const markerCopy = markerRef.current;
markerCopy?.on("click", handleClick);
return () => { return () => {
markerRef.current?.off("click", handleClick); markerCopy?.off("click", handleClick);
}; };
}, [markerRef.current, mission.id, openMissionMarker]); }, [mission.id, openMissionMarker, setOpenMissionMarker]);
const [anchor, setAnchor] = useState< const [anchor, setAnchor] = useState<
"topleft" | "topright" | "bottomleft" | "bottomright" "topleft" | "topright" | "bottomleft" | "bottomright"
>("topleft"); >("topleft");
const handleConflict = () => { const handleConflict = useCallback(() => {
const newAnchor = useConflict(mission.id, "marker"); const newAnchor = calculateAnchor(mission.id.toString(), "marker");
setAnchor(newAnchor); setAnchor(newAnchor);
}; }, [mission.id]);
useEffect(() => { useEffect(() => {
handleConflict(); handleConflict();
}, [missions, openMissionMarker]); }, [handleConflict]);
useEffect(() => {}); useEffect(() => {});
@@ -228,7 +238,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
return () => { return () => {
map.off("zoom", handleConflict); map.off("zoom", handleConflict);
}; };
}, [map, openMissionMarker]); }, [map, openMissionMarker, handleConflict]);
const getMarkerHTML = ( const getMarkerHTML = (
mission: Mission, mission: Mission,
@@ -258,7 +268,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
" "
></div> ></div>
<span class="text-white text-[15px] text-nowrap"> <span class="text-white text-[15px] text-nowrap">
${mission.missionKeywordCategory} ${mission.missionKeywordAbbreviation} ${mission.missionKeywordName}
</span> </span>
<div <div
data-id="${mission.id}" data-id="${mission.id}"
@@ -286,7 +296,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
/> />
{openMissionMarker.some((m) => m.id === mission.id) && ( {openMissionMarker.some((m) => m.id === mission.id) && (
<SmartPopup <SmartPopup
id={mission.id} id={mission.id.toString()}
ref={popupRef} ref={popupRef}
position={[mission.addressLat, mission.addressLng]} position={[mission.addressLat, mission.addressLng]}
autoClose={false} autoClose={false}

View File

@@ -12,9 +12,21 @@ import {
Navigation, Navigation,
Plus, Plus,
Repeat2, Repeat2,
Trash,
} from "lucide-react"; } from "lucide-react";
import {
getPublicUser,
Mission,
MissionLog,
MissionMessageLog,
} from "@repo/db";
import { useMissionsStore } from "_store/missionsStore";
import { usePannelStore } from "_store/pannelStore";
import { useSession } from "next-auth/react";
const Einsatzdetails = () => { const Einsatzdetails = ({ mission }: { mission: Mission }) => {
const { deleteMission } = useMissionsStore((state) => state);
const { setMissionFormValues } = usePannelStore((state) => state);
return ( return (
<div className="p-4 text-base-content"> <div className="p-4 text-base-content">
<h2 className="flex items-center gap-2 text-lg font-bold mb-3"> <h2 className="flex items-center gap-2 text-lg font-bold mb-3">
@@ -22,66 +34,76 @@ const Einsatzdetails = () => {
</h2> </h2>
<ul className="text-base-content font-semibold"> <ul className="text-base-content font-semibold">
<li className="flex items-center gap-2 mb-1"> <li className="flex items-center gap-2 mb-1">
<BellRing size={16} /> AB_ATMUNG <BellRing size={16} /> {mission.missionKeywordCategory}
</li> </li>
<li className="flex items-center gap-2 mb-1"> <li className="flex items-center gap-2 mb-1">
<ListCollapse size={16} /> <ListCollapse size={16} />
Gleichbleibende Atembeschwerden {mission.missionKeywordName}
</li> </li>
<li className="flex items-center gap-2 mt-3"> <li className="flex items-center gap-2 mt-3">
<Hash size={16} /> <Hash size={16} />
__202504161 __{mission.id}
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />
<div className="text-sm font-semibold"> <div className="text-sm font-semibold">
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<MapPin size={16} /> 52.520008, 13.404954 <MapPin size={16} /> {mission.addressLat} {mission.addressLng}
</p> </p>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<Navigation size={16} /> Alexanderplatz <Navigation size={16} /> {mission.addressStreet}
</p> </p>
<p className="flex items-center gap-2"> <p className="flex items-center gap-2">
<LocateFixed size={16} /> Berlin, 10178 <LocateFixed size={16} /> {mission.addressZip} {mission.addressCity}
</p> </p>
</div> </div>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />
<div className="flex items-center gap-2 w-full pr-2"> <div className="flex items-center gap-2 w-full">
<button className="btn btn-sm btn-info btn-outline btn-block basis-5/8"> <button className="btn btn-sm btn-info btn-outline flex-3">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<BellRing size={16} /> Alarmieren <BellRing size={16} /> Alarmieren
</span> </span>
</button> </button>
<button className="btn btn-sm btn-primary btn-dash btn-block basis-3/8"> <button
<span className="flex items-center gap-2"> className="btn btn-sm btn-primary btn-dash flex items-center gap-2"
<Repeat2 size={18} /> Daten übernehmen onClick={() => {
</span> setMissionFormValues({
...mission,
id: undefined,
hpgAmbulanceState: null,
hpgFireEngineState: null,
hpgPoliceState: null,
hpgLocationLat: undefined,
hpgLocationLng: undefined,
state: "draft",
});
}}
>
<Repeat2 size={18} /> Daten übernehmen
</button> </button>
{mission.state === "draft" && (
<button
className="btn btn-sm btn-error btn-outline"
onClick={() => {
deleteMission(mission.id);
}}
>
<Trash size={18} />
</button>
)}
</div> </div>
</div> </div>
); );
}; };
const FMSStatusHistory = () => { const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
const session = useSession();
const [isAddingNote, setIsAddingNote] = useState(false); const [isAddingNote, setIsAddingNote] = useState(false);
const { editMission } = useMissionsStore((state) => state);
const [note, setNote] = useState(""); const [note, setNote] = useState("");
const dummyData = [ if (!session.data?.user) return null;
{
status: "N",
time: "12:39",
unit: "RTH defekt, verbleibt an Einsatzort",
unitshort: "",
},
{ status: "1", time: "12:38", unit: "Christoph 31", unitshort: "CHX31" },
{ status: "7", time: "12:34", unit: "RTW", unitshort: "RTW" },
{ status: "4", time: "12:11", unit: "Christoph 31", unitshort: "CHX31" },
{ status: "4", time: "12:09", unit: "RTW", unitshort: "RTW" },
{ status: "3", time: "12:01", unit: "Christoph 31", unitshort: "CHX31" },
{ status: "3", time: "12:00", unit: "RTW", unitshort: "RTW" },
];
return ( return (
<div className="p-4"> <div className="p-4">
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
@@ -99,16 +121,32 @@ const FMSStatusHistory = () => {
<input <input
type="text" type="text"
placeholder="" placeholder=""
className="input input-sm text-base-content" className="input input-sm text-base-content flex-1"
value={note} value={note}
onChange={(e) => setNote(e.target.value)} onChange={(e) => setNote(e.target.value)}
/> />
<button <button
className="btn btn-sm btn-primary btn-outline" className="btn btn-sm btn-primary btn-outline"
onClick={() => { onClick={() => {
console.log("Note submitted:", note); const newMissionLog = [
setIsAddingNote(false); ...mission.missionLog,
setNote(""); {
type: "message-log",
auto: false,
timeStamp: new Date().toISOString(),
data: {
message: note,
user: getPublicUser(session.data?.user),
},
} as MissionMessageLog,
];
editMission(mission.id, {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
missionLog: newMissionLog as any,
}).then(() => {
setIsAddingNote(false);
setNote("");
});
}} }}
> >
<Plus size={20} /> <Plus size={20} />
@@ -127,20 +165,39 @@ const FMSStatusHistory = () => {
</div> </div>
<div className="divider m-0" /> <div className="divider m-0" />
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto"> <ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
{dummyData.map((entry, index) => ( {(mission.missionLog as unknown as MissionLog[]).map((entry, index) => {
<li key={index} className="flex items-center gap-2"> if (entry.type === "station-log")
<span className="text-base-content">{entry.time}</span> return (
<span <li key={index} className="flex items-center gap-2">
className="font-bold text-base" <span className="text-base-content">{entry.timeStamp}</span>
style={{ <span
color: FMS_STATUS_TEXT_COLORS[entry.status], className="font-bold text-base"
}} style={{
> color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
{entry.status} }}
</span> >
<span className="text-base-content">{entry.unit}</span> {entry.data.newFMSstatus}
</li> </span>
))} <span className="text-base-content">
{entry.data.station.bosCallsign}
</span>
</li>
);
if (entry.type === "message-log")
return (
<li key={index} className="flex items-center gap-2">
<span className="text-base-content">
{new Date(entry.timeStamp).toLocaleTimeString()}
</span>
<span className="font-bold text-base">
{entry.data.user.firstname} {entry.data.user.lastname}
</span>
<span className="text-base-content">{entry.data.message}</span>
</li>
);
return null;
})}
</ul> </ul>
</div> </div>
); );

View File

@@ -24,10 +24,14 @@ export const MissionForm = () => {
createdUserId: session.data?.user.id, createdUserId: session.data?.user.id,
type: "primär", type: "primär",
addressOSMways: [], addressOSMways: [],
missionKeywordAbbreviation: "",
missionKeywordCategory: "",
missionKeywordName: "",
hpgFireEngineState: null, hpgFireEngineState: null,
hpgAmbulanceState: null, hpgAmbulanceState: null,
hpgPoliceState: null, hpgPoliceState: null,
hpgMissionString: null, hpgMissionString: null,
missionLog: [], missionLog: [],
}) as Partial<MissionOptionalDefaults>, }) as Partial<MissionOptionalDefaults>,
[session.data?.user.id], [session.data?.user.id],
@@ -54,10 +58,27 @@ export const MissionForm = () => {
useEffect(() => { useEffect(() => {
if (missionFormValues) { if (missionFormValues) {
form.reset({ if (Object.keys(missionFormValues).length === 0) {
...missionFormValues, console.log("resetting form");
...defaultFormValues, form.reset(/* {
}); addressStreet: "",
addressCity: "",
addressZip: "",
missionStationIds: [],
missionKeywordName: "",
missionKeywordAbbreviation: "",
missionKeywordCategory: "",
...defaultFormValues,
} */);
return;
}
for (const key in missionFormValues) {
form.setValue(
key as keyof MissionOptionalDefaults,
missionFormValues[key as keyof MissionOptionalDefaults],
);
}
} }
}, [missionFormValues, form, defaultFormValues]); }, [missionFormValues, form, defaultFormValues]);
@@ -73,12 +94,6 @@ export const MissionForm = () => {
}); });
}, []); }, []);
console.log(
form.watch("missionKeywordName"),
form.watch("missionKeywordAbbreviation"),
);
console.log(form.formState.errors);
return ( return (
<form className="space-y-4"> <form className="space-y-4">
{/* Koorinaten Section */} {/* Koorinaten Section */}
@@ -98,6 +113,12 @@ export const MissionForm = () => {
disabled disabled
/> />
</div> </div>
{(form.formState.errors.addressLat ||
form.formState.errors.addressLng) && (
<p className="text-error">
Bitte wähle eine Postion übder das Context-Menu über der Karte aus.
</p>
)}
</div> </div>
{/* Adresse Section */} {/* Adresse Section */}
@@ -177,9 +198,9 @@ export const MissionForm = () => {
form.setValue("missionKeywordAbbreviation", ""); form.setValue("missionKeywordAbbreviation", "");
form.setValue("hpgMissionString", ""); form.setValue("hpgMissionString", "");
}} }}
defaultValue="default" defaultValue=""
> >
<option disabled value="default"> <option disabled value="">
Einsatz Kategorie auswählen... Einsatz Kategorie auswählen...
</option> </option>
{Object.keys(KEYWORD_CATEGORY).map((use) => ( {Object.keys(KEYWORD_CATEGORY).map((use) => (
@@ -288,7 +309,23 @@ export const MissionForm = () => {
> >
<BellRing className="h-4 w-4" /> Alarmieren <BellRing className="h-4 w-4" /> Alarmieren
</button> </button>
<button type="submit" className="btn btn-primary btn-block"> <button
type="submit"
className="btn btn-primary btn-block"
onClick={form.handleSubmit(
async (mission: MissionOptionalDefaults) => {
try {
const newMission = await createMission(mission);
toast.success(`Einsatz ${newMission.id} erstellt`);
form.reset();
} catch (error) {
toast.error(
`Fehler beim Erstellen des Einsatzes: ${(error as Error).message}`,
);
}
},
)}
>
<BookmarkPlus className="h-5 w-5" /> Einsatz vorbereiten <BookmarkPlus className="h-5 w-5" /> Einsatz vorbereiten
</button> </button>
</div> </div>

View File

@@ -1,10 +1,11 @@
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { cn } from "helpers/cn"; import { cn } from "helpers/cn";
import { MissionForm } from "./MissionForm"; import { MissionForm } from "./MissionForm";
import { Rss } from "lucide-react"; import { Rss, Trash2Icon } from "lucide-react";
export const Pannel = () => { export const Pannel = () => {
const { isOpen, setOpen } = usePannelStore(); const { setOpen, setMissionFormValues } = usePannelStore();
return ( return (
<div className={cn("flex-1 max-w-[600px] z-9999999")}> <div className={cn("flex-1 max-w-[600px] z-9999999")}>
<div className="bg-base-100 min-h-screen h-full max-h-screen w-full overflow-auto"> <div className="bg-base-100 min-h-screen h-full max-h-screen w-full overflow-auto">
@@ -12,9 +13,17 @@ export const Pannel = () => {
<h1 className="text-xl font-bold flex items-center gap-2"> <h1 className="text-xl font-bold flex items-center gap-2">
<Rss /> Neuer Einsatz <Rss /> Neuer Einsatz
</h1> </h1>
<button className="btn" onClick={() => setOpen(false)}> <div>
Abbrechen <button
</button> className="btn btn-ghost btn-sm mr-2 btn-warning"
onClick={() => setMissionFormValues({})}
>
<Trash2Icon size={18} />
</button>
<button className="btn" onClick={() => setOpen(false)}>
Abbrechen
</button>
</div>
</div> </div>
<div className="divider m-0" /> <div className="divider m-0" />
<div className="p-4"> <div className="p-4">

View File

@@ -0,0 +1,9 @@
import axios from "axios";
export const serverApi = axios.create({
baseURL: process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL,
timeout: 10000,
headers: {
"Content-Type": "application/json",
},
});

View File

@@ -18,6 +18,7 @@
"@repo/ui": "*", "@repo/ui": "*",
"@tailwindcss/postcss": "^4.0.14", "@tailwindcss/postcss": "^4.0.14",
"@tanstack/react-query": "^5.69.0", "@tanstack/react-query": "^5.69.0",
"axios": "^1.9.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"livekit-client": "^2.9.7", "livekit-client": "^2.9.7",
"livekit-server-sdk": "^2.10.2", "livekit-server-sdk": "^2.10.2",

Binary file not shown.

7
package-lock.json generated
View File

@@ -28,6 +28,7 @@
"@repo/ui": "*", "@repo/ui": "*",
"@tailwindcss/postcss": "^4.0.14", "@tailwindcss/postcss": "^4.0.14",
"@tanstack/react-query": "^5.69.0", "@tanstack/react-query": "^5.69.0",
"axios": "^1.9.0",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"livekit-client": "^2.9.7", "livekit-client": "^2.9.7",
"livekit-server-sdk": "^2.10.2", "livekit-server-sdk": "^2.10.2",
@@ -4337,9 +4338,9 @@
} }
}, },
"node_modules/axios": { "node_modules/axios": {
"version": "1.8.3", "version": "1.9.0",
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz", "resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==", "integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
"license": "MIT", "license": "MIT",
"dependencies": { "dependencies": {
"follow-redirects": "^1.15.6", "follow-redirects": "^1.15.6",

View File

@@ -1,8 +1,10 @@
import { Station, User } from "../../generated/client"; import { Station, User } from "../../generated/client";
import { PublicUser } from "./User";
export interface MissionStationLog { export interface MissionStationLog {
type: "station-log"; type: "station-log";
auto: true; auto: true;
timeStamp: string;
data: { data: {
stationId: string; stationId: string;
oldFMSstatus: string; oldFMSstatus: string;
@@ -15,9 +17,11 @@ export interface MissionStationLog {
export interface MissionMessageLog { export interface MissionMessageLog {
type: "message-log"; type: "message-log";
auto: false; auto: false;
timeStamp: string;
data: { data: {
message: string; message: string;
user: User; user: PublicUser;
}; };
} }

View File

@@ -0,0 +1,20 @@
import { User } from "../../generated/client";
export interface PublicUser {
firstname: string;
lastname: string;
publicId: string;
badges: string[];
}
export const getPublicUser = (user: User): PublicUser => {
return {
firstname: user.firstname,
lastname: user.lastname
.split(" ")
.map((part) => `${part[0]}.`)
.join(" "), // Only take the first part of the name
publicId: user.publicId,
badges: user.badges,
};
};

View File

@@ -1 +1,3 @@
export type { ParticipantLog } from "./ParticipantLog"; export * from "./ParticipantLog";
export * from "./MissionVehicleLog";
export * from "./User";

View File

@@ -0,0 +1,47 @@
import React from "react";
import {
FieldValues,
Path,
RegisterOptions,
UseFormReturn,
} from "react-hook-form";
import { cn } from "../helper/cn";
interface InputProps<T extends FieldValues>
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "form"> {
name: Path<T>;
form: UseFormReturn<T>;
formOptions?: RegisterOptions<T>;
label?: string;
placeholder?: string;
}
export const Input = <T extends FieldValues>({
name,
label = name,
placeholder = label,
form,
formOptions,
className,
...inputProps
}: InputProps<T>) => {
return (
<label className="floating-label w-full mt-5">
<span className="text-lg flex items-center gap-2">{label}</span>
<input
{...form.register(name, formOptions)}
className={cn(
"input input-bordered w-full placeholder:text-neutral-600",
className,
)}
placeholder={placeholder}
{...inputProps}
/>
{form.formState.errors[name] && (
<p className="text-error">
{form.formState.errors[name].message as string}
</p>
)}
</label>
);
};

View File

@@ -0,0 +1 @@
export * from "./Input";

View File

@@ -0,0 +1,6 @@
import clsx, { ClassValue } from "clsx";
import { twMerge } from "tailwind-merge";
export const cn = (...inputs: ClassValue[]) => {
return twMerge(clsx(inputs));
};

View File

@@ -1 +1,2 @@
export * from "./helper/event"; export * from "./helper/event";
export * from "./components";