Made Mission marker DB compatible
This commit is contained in:
@@ -25,6 +25,7 @@ io.on("connection", (socket) => {
|
||||
});
|
||||
|
||||
app.use(cors());
|
||||
app.use(express.json());
|
||||
app.use(router);
|
||||
|
||||
server.listen(process.env.PORT, () => {
|
||||
|
||||
79
apps/dispatch-server/routes/mission.ts
Normal file
79
apps/dispatch-server/routes/mission.ts
Normal 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;
|
||||
@@ -1,10 +1,12 @@
|
||||
import { Router } from "express";
|
||||
import livekitRouter from "./livekit";
|
||||
import dispatcherRotuer from "./dispatcher";
|
||||
import missionRouter from "./mission";
|
||||
|
||||
const router = Router();
|
||||
|
||||
router.use("/livekit", livekitRouter);
|
||||
router.use("/dispatcher", dispatcherRotuer);
|
||||
router.use("/mission", missionRouter);
|
||||
|
||||
export default router;
|
||||
|
||||
@@ -16,7 +16,7 @@ export const useSmartPopup = () => {
|
||||
return context;
|
||||
};
|
||||
|
||||
export const useConflict = (id: string, mode: "popup" | "marker") => {
|
||||
export const calculateAnchor = (id: string, mode: "popup" | "marker") => {
|
||||
const otherMarkers = document.querySelectorAll(".map-collision");
|
||||
// get markers and check if they are overlapping
|
||||
const ownMarker =
|
||||
@@ -105,7 +105,7 @@ export const SmartPopup = (
|
||||
>("topleft");
|
||||
|
||||
const handleConflict = () => {
|
||||
const newAnchor = useConflict(id, "popup");
|
||||
const newAnchor = calculateAnchor(id, "popup");
|
||||
setAnchor(newAnchor);
|
||||
};
|
||||
|
||||
|
||||
@@ -11,12 +11,12 @@ interface MapStore {
|
||||
zoom: number;
|
||||
};
|
||||
openMissionMarker: {
|
||||
id: string;
|
||||
tab: "home" | "";
|
||||
id: number;
|
||||
tab: "home" | "details" | "chat" | "log";
|
||||
}[];
|
||||
setOpenMissionMarker: (mission: {
|
||||
open: MapStore["openMissionMarker"];
|
||||
close: string[];
|
||||
close: number[];
|
||||
}) => void;
|
||||
openAircraftMarker: {
|
||||
id: string;
|
||||
@@ -58,22 +58,16 @@ 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<MapStore>((set, get) => ({
|
||||
openMissionMarker: [],
|
||||
setOpenMissionMarker: ({ open, close }) => {
|
||||
set((state) => ({
|
||||
openMissionMarker: [...state.openMissionMarker, ...open].filter(
|
||||
(marker) => !close.includes(marker.id),
|
||||
),
|
||||
const oldMarkers = get().openMissionMarker.filter(
|
||||
(m) => !close.includes(m.id) && !open.find((o) => o.id === m.id),
|
||||
);
|
||||
set(() => ({
|
||||
openMissionMarker: [...oldMarkers, ...open],
|
||||
}));
|
||||
},
|
||||
openAircraftMarker: [],
|
||||
@@ -81,7 +75,7 @@ export const useMapStore = create<MapStore>((set, get) => ({
|
||||
const oldMarkers = get().openAircraftMarker.filter(
|
||||
(m) => !close.includes(m.id) && !open.find((o) => o.id === m.id),
|
||||
);
|
||||
|
||||
console.log("oldMarkers", oldMarkers, open);
|
||||
set(() => ({
|
||||
openAircraftMarker: [...oldMarkers, ...open],
|
||||
}));
|
||||
@@ -114,11 +108,4 @@ export const useMapStore = create<MapStore>((set, get) => ({
|
||||
},
|
||||
})),
|
||||
missionTabs: {},
|
||||
setMissionTab: (missionId, tab) =>
|
||||
set((state) => ({
|
||||
missionTabs: {
|
||||
...state.missionTabs,
|
||||
[missionId]: tab,
|
||||
},
|
||||
})),
|
||||
}));
|
||||
|
||||
@@ -1,46 +1,20 @@
|
||||
import { Mission, Prisma } from "@repo/db";
|
||||
import { MissionOptionalDefaults } from "@repo/db/zod";
|
||||
import { serverApi } from "helpers/axios";
|
||||
import { create } from "zustand";
|
||||
import { toast } from "react-hot-toast";
|
||||
|
||||
interface MissionStore {
|
||||
missions: Mission[];
|
||||
setMissions: (missions: Mission[]) => void;
|
||||
getMissions: () => Promise<undefined>;
|
||||
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) => ({
|
||||
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(),
|
||||
},
|
||||
],
|
||||
missions: [],
|
||||
setMissions: (missions) => set({ missions }),
|
||||
createMission: async (mission) => {
|
||||
const res = await fetch("/api/mission", {
|
||||
@@ -55,41 +29,46 @@ export const useMissionsStore = create<MissionStore>((set) => ({
|
||||
set((state) => ({ missions: [...state.missions, 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 () => {
|
||||
const res = await fetch("/api/mission", {
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Content-Type": "application/json",
|
||||
},
|
||||
body: JSON.stringify({
|
||||
OR: [
|
||||
{
|
||||
state: "draft",
|
||||
},
|
||||
{
|
||||
state: "running",
|
||||
},
|
||||
],
|
||||
} as Prisma.MissionWhereInput),
|
||||
const { data } = await serverApi.post<Mission[]>("/mission", {
|
||||
filter: {
|
||||
OR: [{ state: "draft" }, { state: "running" }],
|
||||
} as Prisma.MissionWhereInput,
|
||||
});
|
||||
if (!res.ok) return undefined;
|
||||
const data = await res.json();
|
||||
//set({ missions: data });
|
||||
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] };
|
||||
}
|
||||
}),
|
||||
}));
|
||||
|
||||
useMissionsStore
|
||||
|
||||
@@ -17,7 +17,11 @@ import {
|
||||
MessageSquareText,
|
||||
Minimize2,
|
||||
} from "lucide-react";
|
||||
import { SmartPopup, useConflict, useSmartPopup } from "_components/SmartPopup";
|
||||
import {
|
||||
SmartPopup,
|
||||
calculateAnchor,
|
||||
useSmartPopup,
|
||||
} from "_components/SmartPopup";
|
||||
import FMSStatusHistory, {
|
||||
FMSStatusSelector,
|
||||
RettungsmittelTab,
|
||||
@@ -249,24 +253,25 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||
});
|
||||
}
|
||||
};
|
||||
markerRef.current?.on("click", handleClick);
|
||||
const marker = markerRef.current;
|
||||
marker?.on("click", handleClick);
|
||||
return () => {
|
||||
markerRef.current?.off("click", handleClick);
|
||||
marker?.off("click", handleClick);
|
||||
};
|
||||
}, [markerRef.current, aircraft.id, openAircraftMarker]);
|
||||
}, [aircraft.id, openAircraftMarker, setOpenAircraftMarker]);
|
||||
|
||||
const [anchor, setAnchor] = useState<
|
||||
"topleft" | "topright" | "bottomleft" | "bottomright"
|
||||
>("topleft");
|
||||
|
||||
const handleConflict = () => {
|
||||
const newAnchor = useConflict(aircraft.id, "marker");
|
||||
const handleConflict = useCallback(() => {
|
||||
const newAnchor = calculateAnchor(aircraft.id, "marker");
|
||||
setAnchor(newAnchor);
|
||||
};
|
||||
}, [aircraft.id]);
|
||||
|
||||
useEffect(() => {
|
||||
handleConflict();
|
||||
}, [aircrafts, openAircraftMarker]);
|
||||
}, [aircrafts, openAircraftMarker, handleConflict]);
|
||||
|
||||
useEffect(() => {});
|
||||
|
||||
@@ -279,7 +284,7 @@ const AircraftMarker = ({ aircraft }: { aircraft: Aircraft }) => {
|
||||
return () => {
|
||||
map.off("zoom", handleConflict);
|
||||
};
|
||||
}, [map, openAircraftMarker]);
|
||||
}, [map, openAircraftMarker, handleConflict]);
|
||||
|
||||
const getMarkerHTML = (
|
||||
aircraft: Aircraft,
|
||||
|
||||
@@ -12,7 +12,11 @@ import {
|
||||
} from "react";
|
||||
import { cn } from "helpers/cn";
|
||||
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 Einsatzdetails, {
|
||||
FMSStatusHistory,
|
||||
@@ -31,36 +35,42 @@ export const MISSION_STATUS_TEXT_COLORS: Record<MissionState, string> = {
|
||||
};
|
||||
|
||||
const MissionPopupContent = ({ mission }: { mission: Mission }) => {
|
||||
const setMissionTab = useMapStore((state) => state.setMissionTab);
|
||||
const setMissionMarker = useMapStore((state) => state.setOpenMissionMarker);
|
||||
const currentTab = useMapStore(
|
||||
(state) =>
|
||||
state.missionTabs[mission.id] ||
|
||||
("home" as "home" | "details" | "chat" | "log"),
|
||||
state.openMissionMarker.find((m) => m.id === mission.id)?.tab ?? "home",
|
||||
);
|
||||
|
||||
const handleTabChange = useCallback(
|
||||
(tab: "home" | "details" | "chat" | "log") => {
|
||||
if (currentTab !== tab) {
|
||||
setMissionTab(mission.id, tab);
|
||||
}
|
||||
console.log("handleTabChange", tab);
|
||||
setMissionMarker({
|
||||
open: [
|
||||
{
|
||||
id: mission.id,
|
||||
tab,
|
||||
},
|
||||
],
|
||||
close: [],
|
||||
});
|
||||
},
|
||||
[currentTab, mission.id, setMissionTab],
|
||||
[setMissionMarker, mission.id],
|
||||
);
|
||||
|
||||
const renderTabContent = useMemo(() => {
|
||||
switch (currentTab) {
|
||||
case "home":
|
||||
return <Einsatzdetails />;
|
||||
return <Einsatzdetails mission={mission} />;
|
||||
case "details":
|
||||
return <div>Details Content</div>;
|
||||
case "chat":
|
||||
return <div>Chat Content</div>;
|
||||
case "log":
|
||||
return <FMSStatusHistory />;
|
||||
return <FMSStatusHistory mission={mission} />;
|
||||
default:
|
||||
return <span className="text-gray-100">Error</span>;
|
||||
}
|
||||
}, [currentTab]);
|
||||
}, [currentTab, mission]);
|
||||
|
||||
const setOpenMissionMarker = useMapStore(
|
||||
(state) => state.setOpenMissionMarker,
|
||||
@@ -169,7 +179,6 @@ const MissionPopupContent = ({ mission }: { mission: Mission }) => {
|
||||
};
|
||||
|
||||
const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
const missions = useMissionsStore((state) => state.missions);
|
||||
const map = useMap();
|
||||
const markerRef = useRef<LMarker>(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 () => {
|
||||
markerRef.current?.off("click", handleClick);
|
||||
markerCopy?.off("click", handleClick);
|
||||
};
|
||||
}, [markerRef.current, mission.id, openMissionMarker]);
|
||||
}, [mission.id, openMissionMarker, setOpenMissionMarker]);
|
||||
|
||||
const [anchor, setAnchor] = useState<
|
||||
"topleft" | "topright" | "bottomleft" | "bottomright"
|
||||
>("topleft");
|
||||
|
||||
const handleConflict = () => {
|
||||
const newAnchor = useConflict(mission.id, "marker");
|
||||
const handleConflict = useCallback(() => {
|
||||
const newAnchor = calculateAnchor(mission.id.toString(), "marker");
|
||||
setAnchor(newAnchor);
|
||||
};
|
||||
}, [mission.id]);
|
||||
|
||||
useEffect(() => {
|
||||
handleConflict();
|
||||
}, [missions, openMissionMarker]);
|
||||
}, [handleConflict]);
|
||||
|
||||
useEffect(() => {});
|
||||
|
||||
@@ -228,7 +238,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
return () => {
|
||||
map.off("zoom", handleConflict);
|
||||
};
|
||||
}, [map, openMissionMarker]);
|
||||
}, [map, openMissionMarker, handleConflict]);
|
||||
|
||||
const getMarkerHTML = (
|
||||
mission: Mission,
|
||||
@@ -258,7 +268,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
"
|
||||
></div>
|
||||
<span class="text-white text-[15px] text-nowrap">
|
||||
${mission.missionKeywordCategory}
|
||||
${mission.missionKeywordAbbreviation} ${mission.missionKeywordName}
|
||||
</span>
|
||||
<div
|
||||
data-id="${mission.id}"
|
||||
@@ -286,7 +296,7 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
/>
|
||||
{openMissionMarker.some((m) => m.id === mission.id) && (
|
||||
<SmartPopup
|
||||
id={mission.id}
|
||||
id={mission.id.toString()}
|
||||
ref={popupRef}
|
||||
position={[mission.addressLat, mission.addressLng]}
|
||||
autoClose={false}
|
||||
|
||||
@@ -12,9 +12,21 @@ import {
|
||||
Navigation,
|
||||
Plus,
|
||||
Repeat2,
|
||||
Trash,
|
||||
} 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 (
|
||||
<div className="p-4 text-base-content">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
@@ -22,66 +34,76 @@ const Einsatzdetails = () => {
|
||||
</h2>
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<BellRing size={16} /> AB_ATMUNG
|
||||
<BellRing size={16} /> {mission.missionKeywordCategory}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<ListCollapse size={16} />
|
||||
Gleichbleibende Atembeschwerden
|
||||
{mission.missionKeywordName}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mt-3">
|
||||
<Hash size={16} />
|
||||
__202504161
|
||||
__{mission.id}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="text-sm font-semibold">
|
||||
<p className="flex items-center gap-2">
|
||||
<MapPin size={16} /> 52.520008, 13.404954
|
||||
<MapPin size={16} /> {mission.addressLat} {mission.addressLng}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<Navigation size={16} /> Alexanderplatz
|
||||
<Navigation size={16} /> {mission.addressStreet}
|
||||
</p>
|
||||
<p className="flex items-center gap-2">
|
||||
<LocateFixed size={16} /> Berlin, 10178
|
||||
<LocateFixed size={16} /> {mission.addressZip} {mission.addressCity}
|
||||
</p>
|
||||
</div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center gap-2 w-full pr-2">
|
||||
<button className="btn btn-sm btn-info btn-outline btn-block basis-5/8">
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<button className="btn btn-sm btn-info btn-outline flex-3">
|
||||
<span className="flex items-center gap-2">
|
||||
<BellRing size={16} /> Alarmieren
|
||||
</span>
|
||||
</button>
|
||||
|
||||
<button className="btn btn-sm btn-primary btn-dash btn-block basis-3/8">
|
||||
<span className="flex items-center gap-2">
|
||||
<Repeat2 size={18} /> Daten übernehmen
|
||||
</span>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-dash flex items-center gap-2"
|
||||
onClick={() => {
|
||||
setMissionFormValues({
|
||||
...mission,
|
||||
id: undefined,
|
||||
hpgAmbulanceState: null,
|
||||
hpgFireEngineState: null,
|
||||
hpgPoliceState: null,
|
||||
hpgLocationLat: undefined,
|
||||
hpgLocationLng: undefined,
|
||||
state: "draft",
|
||||
});
|
||||
}}
|
||||
>
|
||||
<Repeat2 size={18} /> Daten übernehmen
|
||||
</button>
|
||||
{mission.state === "draft" && (
|
||||
<button
|
||||
className="btn btn-sm btn-error btn-outline"
|
||||
onClick={() => {
|
||||
deleteMission(mission.id);
|
||||
}}
|
||||
>
|
||||
<Trash size={18} />
|
||||
</button>
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
const FMSStatusHistory = () => {
|
||||
const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
const session = useSession();
|
||||
const [isAddingNote, setIsAddingNote] = useState(false);
|
||||
const { editMission } = useMissionsStore((state) => state);
|
||||
const [note, setNote] = useState("");
|
||||
|
||||
const dummyData = [
|
||||
{
|
||||
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" },
|
||||
];
|
||||
|
||||
if (!session.data?.user) return null;
|
||||
return (
|
||||
<div className="p-4">
|
||||
<div className="flex items-center gap-2">
|
||||
@@ -99,16 +121,32 @@ const FMSStatusHistory = () => {
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
className="input input-sm text-base-content"
|
||||
className="input input-sm text-base-content flex-1"
|
||||
value={note}
|
||||
onChange={(e) => setNote(e.target.value)}
|
||||
/>
|
||||
<button
|
||||
className="btn btn-sm btn-primary btn-outline"
|
||||
onClick={() => {
|
||||
console.log("Note submitted:", note);
|
||||
setIsAddingNote(false);
|
||||
setNote("");
|
||||
const newMissionLog = [
|
||||
...mission.missionLog,
|
||||
{
|
||||
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} />
|
||||
@@ -127,20 +165,39 @@ const FMSStatusHistory = () => {
|
||||
</div>
|
||||
<div className="divider m-0" />
|
||||
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto">
|
||||
{dummyData.map((entry, index) => (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">{entry.time}</span>
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.status],
|
||||
}}
|
||||
>
|
||||
{entry.status}
|
||||
</span>
|
||||
<span className="text-base-content">{entry.unit}</span>
|
||||
</li>
|
||||
))}
|
||||
{(mission.missionLog as unknown as MissionLog[]).map((entry, index) => {
|
||||
if (entry.type === "station-log")
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span className="text-base-content">{entry.timeStamp}</span>
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
|
||||
}}
|
||||
>
|
||||
{entry.data.newFMSstatus}
|
||||
</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>
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -24,10 +24,14 @@ export const MissionForm = () => {
|
||||
createdUserId: session.data?.user.id,
|
||||
type: "primär",
|
||||
addressOSMways: [],
|
||||
missionKeywordAbbreviation: "",
|
||||
missionKeywordCategory: "",
|
||||
missionKeywordName: "",
|
||||
hpgFireEngineState: null,
|
||||
hpgAmbulanceState: null,
|
||||
hpgPoliceState: null,
|
||||
hpgMissionString: null,
|
||||
|
||||
missionLog: [],
|
||||
}) as Partial<MissionOptionalDefaults>,
|
||||
[session.data?.user.id],
|
||||
@@ -54,10 +58,27 @@ export const MissionForm = () => {
|
||||
|
||||
useEffect(() => {
|
||||
if (missionFormValues) {
|
||||
form.reset({
|
||||
...missionFormValues,
|
||||
...defaultFormValues,
|
||||
});
|
||||
if (Object.keys(missionFormValues).length === 0) {
|
||||
console.log("resetting form");
|
||||
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]);
|
||||
|
||||
@@ -73,12 +94,6 @@ export const MissionForm = () => {
|
||||
});
|
||||
}, []);
|
||||
|
||||
console.log(
|
||||
form.watch("missionKeywordName"),
|
||||
form.watch("missionKeywordAbbreviation"),
|
||||
);
|
||||
console.log(form.formState.errors);
|
||||
|
||||
return (
|
||||
<form className="space-y-4">
|
||||
{/* Koorinaten Section */}
|
||||
@@ -98,6 +113,12 @@ export const MissionForm = () => {
|
||||
disabled
|
||||
/>
|
||||
</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>
|
||||
|
||||
{/* Adresse Section */}
|
||||
@@ -177,9 +198,9 @@ export const MissionForm = () => {
|
||||
form.setValue("missionKeywordAbbreviation", "");
|
||||
form.setValue("hpgMissionString", "");
|
||||
}}
|
||||
defaultValue="default"
|
||||
defaultValue=""
|
||||
>
|
||||
<option disabled value="default">
|
||||
<option disabled value="">
|
||||
Einsatz Kategorie auswählen...
|
||||
</option>
|
||||
{Object.keys(KEYWORD_CATEGORY).map((use) => (
|
||||
@@ -288,7 +309,23 @@ export const MissionForm = () => {
|
||||
>
|
||||
<BellRing className="h-4 w-4" /> Alarmieren
|
||||
</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
|
||||
</button>
|
||||
</div>
|
||||
|
||||
@@ -1,10 +1,11 @@
|
||||
import { usePannelStore } from "_store/pannelStore";
|
||||
import { cn } from "helpers/cn";
|
||||
import { MissionForm } from "./MissionForm";
|
||||
import { Rss } from "lucide-react";
|
||||
import { Rss, Trash2Icon } from "lucide-react";
|
||||
|
||||
export const Pannel = () => {
|
||||
const { isOpen, setOpen } = usePannelStore();
|
||||
const { setOpen, setMissionFormValues } = usePannelStore();
|
||||
|
||||
return (
|
||||
<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">
|
||||
@@ -12,9 +13,17 @@ export const Pannel = () => {
|
||||
<h1 className="text-xl font-bold flex items-center gap-2">
|
||||
<Rss /> Neuer Einsatz
|
||||
</h1>
|
||||
<button className="btn" onClick={() => setOpen(false)}>
|
||||
Abbrechen
|
||||
</button>
|
||||
<div>
|
||||
<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 className="divider m-0" />
|
||||
<div className="p-4">
|
||||
|
||||
9
apps/dispatch/app/helpers/axios.ts
Normal file
9
apps/dispatch/app/helpers/axios.ts
Normal 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",
|
||||
},
|
||||
});
|
||||
@@ -18,6 +18,7 @@
|
||||
"@repo/ui": "*",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"axios": "^1.9.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"livekit-client": "^2.9.7",
|
||||
"livekit-server-sdk": "^2.10.2",
|
||||
|
||||
Binary file not shown.
7
package-lock.json
generated
7
package-lock.json
generated
@@ -28,6 +28,7 @@
|
||||
"@repo/ui": "*",
|
||||
"@tailwindcss/postcss": "^4.0.14",
|
||||
"@tanstack/react-query": "^5.69.0",
|
||||
"axios": "^1.9.0",
|
||||
"leaflet": "^1.9.4",
|
||||
"livekit-client": "^2.9.7",
|
||||
"livekit-server-sdk": "^2.10.2",
|
||||
@@ -4337,9 +4338,9 @@
|
||||
}
|
||||
},
|
||||
"node_modules/axios": {
|
||||
"version": "1.8.3",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.8.3.tgz",
|
||||
"integrity": "sha512-iP4DebzoNlP/YN2dpwCgb8zoCmhtkajzS48JvwmkSkXvPI3DHc7m+XYL5tGnSlJtR6nImXZmdCuN5aP8dh1d8A==",
|
||||
"version": "1.9.0",
|
||||
"resolved": "https://registry.npmjs.org/axios/-/axios-1.9.0.tgz",
|
||||
"integrity": "sha512-re4CqKTJaURpzbLHtIi6XpDv20/CnpXOtjRY5/CU32L8gU8ek9UIivcfvSWvmKEngmVbrUtPpdDwWDWL7DNHvg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"follow-redirects": "^1.15.6",
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
import { Station, User } from "../../generated/client";
|
||||
import { PublicUser } from "./User";
|
||||
|
||||
export interface MissionStationLog {
|
||||
type: "station-log";
|
||||
auto: true;
|
||||
timeStamp: string;
|
||||
data: {
|
||||
stationId: string;
|
||||
oldFMSstatus: string;
|
||||
@@ -15,9 +17,11 @@ export interface MissionStationLog {
|
||||
export interface MissionMessageLog {
|
||||
type: "message-log";
|
||||
auto: false;
|
||||
timeStamp: string;
|
||||
|
||||
data: {
|
||||
message: string;
|
||||
user: User;
|
||||
user: PublicUser;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
20
packages/database/prisma/json/User.ts
Normal file
20
packages/database/prisma/json/User.ts
Normal 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,
|
||||
};
|
||||
};
|
||||
@@ -1 +1,3 @@
|
||||
export type { ParticipantLog } from "./ParticipantLog";
|
||||
export * from "./ParticipantLog";
|
||||
export * from "./MissionVehicleLog";
|
||||
export * from "./User";
|
||||
|
||||
47
packages/ui/src/components/Input.tsx
Normal file
47
packages/ui/src/components/Input.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
1
packages/ui/src/components/index.ts
Normal file
1
packages/ui/src/components/index.ts
Normal file
@@ -0,0 +1 @@
|
||||
export * from "./Input";
|
||||
6
packages/ui/src/helper/cn.ts
Normal file
6
packages/ui/src/helper/cn.ts
Normal file
@@ -0,0 +1,6 @@
|
||||
import clsx, { ClassValue } from "clsx";
|
||||
import { twMerge } from "tailwind-merge";
|
||||
|
||||
export const cn = (...inputs: ClassValue[]) => {
|
||||
return twMerge(clsx(inputs));
|
||||
};
|
||||
@@ -1 +1,2 @@
|
||||
export * from "./helper/event";
|
||||
export * from "./components";
|
||||
|
||||
Reference in New Issue
Block a user