release V2.0 #74

Merged
PxlLoewe merged 15 commits from staging into release 2025-07-22 16:05:14 +00:00
58 changed files with 1433 additions and 3254 deletions

View File

@@ -2,5 +2,6 @@
"tabWidth": 2, "tabWidth": 2,
"useTabs": true, "useTabs": true,
"printWidth": 100, "printWidth": 100,
"singleQuote": false "singleQuote": false,
"plugins": ["prettier-plugin-tailwindcss"]
} }

View File

@@ -2,6 +2,39 @@ import { MissionLog, NotificationPayload, prisma } from "@repo/db";
import { io } from "index"; import { io } from "index";
import cron from "node-cron"; import cron from "node-cron";
const removeMission = async (id: number, reason: string) => {
const log: MissionLog = {
type: "completed-log",
auto: true,
timeStamp: new Date().toISOString(),
data: {},
};
const updatedMission = await prisma.mission.update({
where: {
id: id,
},
data: {
state: "finished",
missionLog: {
push: log as any,
},
},
});
io.to("dispatchers").emit("new-mission", { updatedMission });
io.to("dispatchers").emit("notification", {
type: "mission-auto-close",
status: "chron",
message: `Einsatz ${updatedMission.publicId} wurde aufgrund ${reason} geschlossen.`,
data: {
missionId: updatedMission.id,
publicMissionId: updatedMission.publicId,
},
} as NotificationPayload);
console.log(`Mission ${updatedMission.id} closed due to inactivity.`);
};
const removeClosedMissions = async () => { const removeClosedMissions = async () => {
const oldMissions = await prisma.mission.findMany({ const oldMissions = await prisma.mission.findMany({
where: { where: {
@@ -15,18 +48,6 @@ const removeClosedMissions = async () => {
const lastAlertTime = lastAlert ? new Date(lastAlert.timeStamp) : null; const lastAlertTime = lastAlert ? new Date(lastAlert.timeStamp) : null;
const aircraftsInMission = await prisma.connectedAircraft.findMany({
where: {
stationId: {
in: mission.missionStationIds,
},
},
});
const allConnectedAircraftsInIdleStatus = aircraftsInMission.every((a) =>
["1", "2", "6"].includes(a.fmsStatus),
);
const allStationsInMissionChangedFromStatus4to1Or8to1 = mission.missionStationIds.every( const allStationsInMissionChangedFromStatus4to1Or8to1 = mission.missionStationIds.every(
(stationId) => { (stationId) => {
const status4Log = (mission.missionLog as unknown as MissionLog[]).findIndex((l) => { const status4Log = (mission.missionLog as unknown as MissionLog[]).findIndex((l) => {
@@ -69,67 +90,24 @@ const removeClosedMissions = async () => {
}, },
); );
const missionHastManualReactivation = (mission.missionLog as unknown as MissionLog[]).some( const missionHasManualReactivation = (mission.missionLog as unknown as MissionLog[]).some(
(l) => l.type === "reopened-log", (l) => l.type === "reopened-log",
); );
console.log({
missionId: mission.publicId,
allConnectedAircraftsInIdleStatus,
lastAlertTime,
allStationsInMissionChangedFromStatus4to1Or8to1,
missionHastManualReactivation,
});
if (
!allConnectedAircraftsInIdleStatus // If some aircrafts are still active, do not close the mission
)
return;
const now = new Date(); if (missionHasManualReactivation) return;
if (!lastAlertTime) return; if (!lastAlertTime) return;
// Case 1: Forgotten Mission, last alert more than 3 Hours ago // Case 1: Forgotten Mission, last alert more than 3 Hours ago
// Case 2: All stations in mission changed from status 4 to 1 or from status 8 to 1 const now = new Date();
if ( if (now.getTime() - lastAlertTime.getTime() > 1000 * 60 * 180)
!( return removeMission(mission.id, "inaktivität");
now.getTime() - lastAlertTime.getTime() > 1000 * 60 * 180 ||
allStationsInMissionChangedFromStatus4to1Or8to1
) ||
missionHastManualReactivation
)
return;
const log: MissionLog = { // Case 2: All stations in mission changed from status 4 to 1/6 or from status 8 to 1/6
type: "completed-log", if (allStationsInMissionChangedFromStatus4to1Or8to1)
auto: true, return removeMission(mission.id, "dem freimelden aller Stationen");
timeStamp: new Date().toISOString(),
data: {},
};
const updatedMission = await prisma.mission.update({
where: {
id: mission.id,
},
data: {
state: "finished",
missionLog: {
push: log as any,
},
},
});
io.to("dispatchers").emit("new-mission", { updatedMission });
io.to("dispatchers").emit("notification", {
type: "mission-auto-close",
status: "chron",
message: `Einsatz ${updatedMission.publicId} wurde aufgrund ${allStationsInMissionChangedFromStatus4to1Or8to1 ? "des Freimeldens aller Stationen" : "von Inaktivität"} geschlossen.`,
data: {
missionId: updatedMission.id,
publicMissionId: updatedMission.publicId,
},
} as NotificationPayload);
console.log(`Mission ${mission.id} closed due to inactivity.`);
}); });
}; };
const removeConnectedAircrafts = async () => { const removeConnectedAircrafts = async () => {
const connectedAircrafts = await prisma.connectedAircraft.findMany({ const connectedAircrafts = await prisma.connectedAircraft.findMany({
where: { where: {

View File

@@ -8,31 +8,30 @@
"start": "tsx index.ts --transpile-only", "start": "tsx index.ts --transpile-only",
"build": "tsc" "build": "tsc"
}, },
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.13.1",
"devDependencies": { "devDependencies": {
"@repo/db": "workspace:*", "@repo/db": "workspace:*",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@types/cors": "^2.8.18", "@types/cors": "^2.8.19",
"@types/express": "^5.0.2", "@types/express": "^5.0.3",
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"concurrently": "^9.1.2", "concurrently": "^9.2.0",
"typescript": "latest" "typescript": "latest"
}, },
"dependencies": { "dependencies": {
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"axios": "^1.9.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^4.3.1", "cron": "^4.3.2",
"discord.js": "^14.19.3", "discord.js": "^14.21.0",
"dotenv": "^16.5.0", "dotenv": "^17.2.0",
"express": "^5.1.0", "express": "^5.1.0",
"node-cron": "^4.1.0", "node-cron": "^4.2.1",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"react": "^19.1.0", "react": "^19.1.0",
"redis": "^5.1.1", "redis": "^5.6.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"tsx": "^4.19.4" "tsx": "^4.20.3"
} }
} }

View File

@@ -19,7 +19,6 @@ router.post("/set-standard-name", async (req, res) => {
id: userId, id: userId,
}, },
}); });
console.log(`Setting standard name for user ${userId} (${user?.publicId}) to member ${memberId}`);
if (!user) { if (!user) {
res.status(404).json({ error: "User not found" }); res.status(404).json({ error: "User not found" });
return; return;

View File

@@ -8,39 +8,39 @@
"start": "tsx index.ts --transpile-only", "start": "tsx index.ts --transpile-only",
"build": "tsc" "build": "tsc"
}, },
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.13.1",
"devDependencies": { "devDependencies": {
"@repo/db": "workspace:*", "@repo/db": "workspace:*",
"@repo/shared-components": "workspace:*", "@repo/shared-components": "workspace:*",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@types/cookie-parser": "^1.4.8", "@types/cookie-parser": "^1.4.9",
"@types/cors": "^2.8.18", "@types/cors": "^2.8.19",
"@types/express": "^5.0.2", "@types/express": "^5.0.3",
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"concurrently": "^9.1.2", "concurrently": "^9.2.0",
"typescript": "latest" "typescript": "latest"
}, },
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.41", "@react-email/components": "^0.3.2",
"@redis/json": "^5.1.1", "@redis/json": "^5.6.0",
"@socket.io/redis-adapter": "^8.3.0", "@socket.io/redis-adapter": "^8.3.0",
"@types/jsonwebtoken": "^9.0.9", "@types/jsonwebtoken": "^9.0.10",
"axios": "^1.9.0", "axios": "^1.10.0",
"cookie-parser": "^1.4.7", "cookie-parser": "^1.4.7",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^4.3.1", "cron": "^4.3.2",
"dotenv": "^16.5.0", "dotenv": "^17.2.0",
"express": "^5.1.0", "express": "^5.1.0",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"livekit-server-sdk": "^2.13.0", "livekit-server-sdk": "^2.13.1",
"node-cron": "^4.1.0", "node-cron": "^4.2.1",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.5",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"prom-client": "^15.1.3", "prom-client": "^15.1.3",
"react": "^19.1.0", "react": "^19.1.0",
"redis": "^5.1.1", "redis": "^5.6.0",
"socket.io": "^4.8.1", "socket.io": "^4.8.1",
"tsx": "^4.19.4" "tsx": "^4.20.3"
} }
} }

View File

@@ -261,9 +261,7 @@ router.post("/:id/hpg-validation-result", async (req, res) => {
}, },
} as NotificationPayload); } as NotificationPayload);
console.log("Got positiv validation Result", result.alertWhenValid);
if (result.alertWhenValid) { if (result.alertWhenValid) {
console.log(req.user);
sendAlert(Number(missionId), {}, "HPG"); sendAlert(Number(missionId), {}, "HPG");
} }
} else { } else {

View File

@@ -45,15 +45,10 @@ export const handleConnectDispatch =
}); });
} }
const [logoffHours, logoffMinutes] = logoffTime.split(":").map(Number);
const connectedDispatcherEntry = await prisma.connectedDispatcher.create({ const connectedDispatcherEntry = await prisma.connectedDispatcher.create({
data: { data: {
publicUser: getPublicUser(user) as any, publicUser: getPublicUser(user) as any,
esimatedLogoutTime: esimatedLogoutTime: logoffTime.length > 0 ? logoffTime : null,
logoffHours !== undefined && logoffMinutes !== undefined
? getNextDateWithTime(logoffHours, logoffMinutes)
: null,
lastHeartbeat: new Date().toISOString(), lastHeartbeat: new Date().toISOString(),
userId: user.id, userId: user.id,
zone: selectedZone, zone: selectedZone,

View File

@@ -73,15 +73,11 @@ export const handleConnectPilot =
} }
const randomPos = debug ? getRandomGermanPosition() : undefined; const randomPos = debug ? getRandomGermanPosition() : undefined;
const [logoffHours, logoffMinutes] = logoffTime.split(":").map(Number);
const connectedAircraftEntry = await prisma.connectedAircraft.create({ const connectedAircraftEntry = await prisma.connectedAircraft.create({
data: { data: {
publicUser: getPublicUser(user) as any, publicUser: getPublicUser(user) as any,
esimatedLogoutTime: esimatedLogoutTime: logoffTime.length > 0 ? logoffTime : null,
logoffHours !== undefined && logoffMinutes !== undefined
? getNextDateWithTime(logoffHours, logoffMinutes)
: null,
userId: userId, userId: userId,
stationId: parseInt(stationId), stationId: parseInt(stationId),
lastHeartbeat: debug ? nowPlus2h.toISOString() : undefined, lastHeartbeat: debug ? nowPlus2h.toISOString() : undefined,

View File

@@ -8,7 +8,6 @@ export const handleSendMessage =
{ userId, message }: { userId: string; message: string }, { userId, message }: { userId: string; message: string },
cb: (err: { error?: string }) => void, cb: (err: { error?: string }) => void,
) => { ) => {
console.log("send-message", userId, message);
const senderId = socket.data.user.id; const senderId = socket.data.user.id;
const senderUser = await prisma.user.findUnique({ const senderUser = await prisma.user.findUnique({

View File

@@ -107,7 +107,6 @@ export function StationsSelect({
menuPlacement={menuPlacement} menuPlacement={menuPlacement}
isMulti={isMulti} isMulti={isMulti}
onChange={(v) => { onChange={(v) => {
console.log("Selected values:", v);
setValue(v); setValue(v);
if (!isMulti) { if (!isMulti) {
const singleValue = v as string; const singleValue = v as string;

View File

@@ -31,9 +31,9 @@ export const ConnectionBtn = () => {
if (!uid) return null; if (!uid) return null;
return ( return (
<div className="rounded-box bg-base-200 flex justify-center items-center gap-2 p-1"> <div className="rounded-box bg-base-200 flex items-center justify-center gap-2 p-1">
{connection.message.length > 0 && ( {connection.message.length > 0 && (
<span className="mx-2 text-error">{connection.message}</span> <span className="text-error mx-2">{connection.message}</span>
)} )}
{connection.status == "connected" ? ( {connection.status == "connected" ? (
@@ -63,11 +63,11 @@ export const ConnectionBtn = () => {
<dialog ref={modalRef} className="modal"> <dialog ref={modalRef} className="modal">
<div className="modal-box flex flex-col items-center justify-center"> <div className="modal-box flex flex-col items-center justify-center">
{connection.status == "connected" ? ( {connection.status == "connected" ? (
<h3 className="text-lg font-bold mb-5"> <h3 className="mb-5 text-lg font-bold">
Verbunden als <span className="text-info">&lt;{connection.selectedZone}&gt;</span> Verbunden als <span className="text-info">&lt;{connection.selectedZone}&gt;</span>
</h3> </h3>
) : ( ) : (
<h3 className="text-lg font-bold mb-5">Als Disponent anmelden</h3> <h3 className="mb-5 text-lg font-bold">Als Disponent anmelden</h3>
)} )}
<fieldset className="fieldset w-full"> <fieldset className="fieldset w-full">
<label className="floating-label w-full text-base"> <label className="floating-label w-full text-base">
@@ -89,8 +89,8 @@ export const ConnectionBtn = () => {
<p className="fieldset-label">Du kannst diese Zeit später noch anpassen.</p> <p className="fieldset-label">Du kannst diese Zeit später noch anpassen.</p>
)} )}
</fieldset> </fieldset>
<div className="modal-action flex justify-between w-full"> <div className="modal-action flex w-full justify-between">
<form method="dialog" className="w-full flex justify-between"> <form method="dialog" className="flex w-full justify-between">
<button className="btn btn-soft">Zurück</button> <button className="btn btn-soft">Zurück</button>
{connection.status == "connected" ? ( {connection.status == "connected" ? (
<> <>
@@ -130,7 +130,15 @@ export const ConnectionBtn = () => {
type="submit" type="submit"
onSubmit={() => false} onSubmit={() => false}
onClick={() => { onClick={() => {
connection.connect(uid, form.selectedZone, form.logoffTime); const [logoffHours, logoffMinutes] =
form.logoffTime?.split(":").map(Number) || [];
connection.connect(
uid,
form.selectedZone,
form.logoffTime && logoffHours !== undefined && logoffMinutes !== undefined
? getNextDateWithTime(logoffHours, logoffMinutes).toISOString()
: "",
);
}} }}
className="btn btn-soft btn-info" className="btn btn-soft btn-info"
> >

View File

@@ -195,7 +195,7 @@ export const MissionForm = () => {
<form className="space-y-4"> <form className="space-y-4">
{/* Koorinaten Section */} {/* Koorinaten Section */}
<div className="form-control"> <div className="form-control">
<h2 className="text-lg font-bold mb-2">Koordinaten</h2> <h2 className="mb-2 text-lg font-bold">Koordinaten</h2>
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<input <input
type="text" type="text"
@@ -219,12 +219,12 @@ export const MissionForm = () => {
{/* Adresse Section */} {/* Adresse Section */}
<div className="form-control"> <div className="form-control">
<h2 className="text-lg font-bold mb-2">Adresse</h2> <h2 className="mb-2 text-lg font-bold">Adresse</h2>
<input <input
type="text" type="text"
{...form.register("addressStreet")} {...form.register("addressStreet")}
placeholder="Straße" placeholder="Straße"
className="input input-primary input-bordered w-full mb-4" className="input input-primary input-bordered mb-4 w-full"
/> />
<div className="grid grid-cols-2 gap-4"> <div className="grid grid-cols-2 gap-4">
<input <input
@@ -244,17 +244,16 @@ export const MissionForm = () => {
type="text" type="text"
{...form.register("addressAdditionalInfo")} {...form.register("addressAdditionalInfo")}
placeholder="Zusätzliche Adressinformationen" placeholder="Zusätzliche Adressinformationen"
className="input input-primary input-bordered w-full mt-4" className="input input-primary input-bordered mt-4 w-full"
/> />
</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="mb-2 text-lg font-bold">Rettungsmittel</h2>
<StationsSelect <StationsSelect
isMulti isMulti
selectedStations={form.watch("missionStationIds")} selectedStations={form.watch("missionStationIds")}
onChange={(v) => { onChange={(v) => {
console.log("Selected stations:", v);
form.setValue("missionStationIds", v.selectedStationIds); form.setValue("missionStationIds", v.selectedStationIds);
form.setValue("hpgAmbulanceState", v.hpgAmbulanceState); form.setValue("hpgAmbulanceState", v.hpgAmbulanceState);
form.setValue("hpgFireEngineState", v.hpgFireEngineState); form.setValue("hpgFireEngineState", v.hpgFireEngineState);
@@ -270,10 +269,10 @@ export const MissionForm = () => {
{/* Einsatzdaten Section */} {/* Einsatzdaten Section */}
<div className="form-control"> <div className="form-control">
<h2 className="text-lg font-bold mb-2">Einsatzdaten</h2> <h2 className="mb-2 text-lg font-bold">Einsatzdaten</h2>
<select <select
{...form.register("type")} {...form.register("type")}
className="select select-primary select-bordered w-full mb-4" className="select select-primary select-bordered mb-4 w-full"
onChange={(e) => { onChange={(e) => {
form.setValue("type", e.target.value as missionType); form.setValue("type", e.target.value as missionType);
if (e.target.value === "primary") { if (e.target.value === "primary") {
@@ -295,7 +294,7 @@ export const MissionForm = () => {
<> <>
<select <select
{...form.register("missionKeywordCategory")} {...form.register("missionKeywordCategory")}
className="select select-primary select-bordered w-full mb-4" className="select select-primary select-bordered mb-4 w-full"
onChange={(e) => { onChange={(e) => {
form.setValue("missionKeywordCategory", e.target.value as string); form.setValue("missionKeywordCategory", e.target.value as string);
form.setValue("missionKeywordName", null as any); form.setValue("missionKeywordName", null as any);
@@ -316,13 +315,13 @@ export const MissionForm = () => {
))} ))}
</select> </select>
{form.formState.errors.missionKeywordCategory && ( {form.formState.errors.missionKeywordCategory && (
<p className="text-error text-sm mb-4">Bitte wähle eine Kategorie aus.</p> <p className="text-error mb-4 text-sm">Bitte wähle eine Kategorie aus.</p>
)} )}
</> </>
)} )}
<select <select
{...form.register("missionKeywordAbbreviation")} {...form.register("missionKeywordAbbreviation")}
className="select select-primary select-bordered w-full mb-4" className="select select-primary select-bordered mb-4 w-full"
onChange={(e) => { onChange={(e) => {
const keyword = keywords?.find((k) => k.abreviation === e.target.value); const keyword = keywords?.find((k) => k.abreviation === e.target.value);
form.setValue("missionKeywordName", keyword?.name || (null as any)); form.setValue("missionKeywordName", keyword?.name || (null as any));
@@ -344,7 +343,7 @@ export const MissionForm = () => {
))} ))}
</select> </select>
{form.formState.errors.missionKeywordAbbreviation && ( {form.formState.errors.missionKeywordAbbreviation && (
<p className="text-error text-sm mb-4">Bitte wähle ein Stichwort aus.</p> <p className="text-error mb-4 text-sm">Bitte wähle ein Stichwort aus.</p>
)} )}
<div className="mb-4"> <div className="mb-4">
<select <select
@@ -364,7 +363,7 @@ export const MissionForm = () => {
form.setValue("missionAdditionalInfo", name || ""); form.setValue("missionAdditionalInfo", name || "");
} }
}} }}
className="select select-primary select-bordered w-full mb-2" className="select select-primary select-bordered mb-2 w-full"
value={form.watch("hpgMissionString") || "please_select"} value={form.watch("hpgMissionString") || "please_select"}
> >
<option disabled value="please_select"> <option disabled value="please_select">
@@ -383,14 +382,14 @@ export const MissionForm = () => {
})} })}
</select> </select>
{validationRequired && ( {validationRequired && (
<p className="text-sm text-warning">Szenario wird vor Alarmierung HPG-Validiert.</p> <p className="text-warning text-sm">Szenario wird vor Alarmierung HPG-Validiert.</p>
)} )}
</div> </div>
<textarea <textarea
{...form.register("missionAdditionalInfo")} {...form.register("missionAdditionalInfo")}
placeholder="Einsatzinformationen" placeholder="Einsatzinformationen"
className="textarea textarea-primary textarea-bordered w-full mb-4" className="textarea textarea-primary textarea-bordered mb-4 w-full"
/> />
{form.watch("type") === "sekundär" && ( {form.watch("type") === "sekundär" && (
<input <input
@@ -402,7 +401,7 @@ export const MissionForm = () => {
)} )}
</div> </div>
<div className="form-control"> <div className="form-control">
<h2 className="text-lg font-bold mb-2">Patienteninformationen</h2> <h2 className="mb-2 text-lg font-bold">Patienteninformationen</h2>
<textarea <textarea
{...form.register("missionPatientInfo")} {...form.register("missionPatientInfo")}
placeholder="Patienteninformationen" placeholder="Patienteninformationen"

View File

@@ -7,6 +7,7 @@ import dynamic from "next/dynamic";
import { Chat } from "../../_components/left/Chat"; import { Chat } from "../../_components/left/Chat";
import { Report } from "../../_components/left/Report"; import { Report } from "../../_components/left/Report";
import { SituationBoard } from "_components/left/SituationBoard"; import { SituationBoard } from "_components/left/SituationBoard";
import { BugReport } from "_components/left/BugReport";
const Map = dynamic(() => import("../../_components/map/Map"), { ssr: false }); const Map = dynamic(() => import("../../_components/map/Map"), { ssr: false });
@@ -14,16 +15,15 @@ const DispatchPage = () => {
const { isOpen } = usePannelStore(); const { isOpen } = usePannelStore();
/* return null; */ /* return null; */
return ( return (
<div className="relative flex-1 flex transition-all duration-500 ease w-full"> <div className="ease relative flex w-full flex-1 transition-all duration-500">
{/* <MapToastCard2 /> */} {/* <MapToastCard2 /> */}
<div className="flex flex-1 relative"> <div className="relative flex flex-1">
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 pl-4 z-999999"> <div className="z-999999 absolute left-0 top-1/2 flex -translate-y-1/2 transform flex-col space-y-2 pl-4">
<Chat /> <Chat />
<div className="mt-2"> <Report />
<Report /> <BugReport />
</div>
</div> </div>
<div className="absolute left-0 top-19/20 transform -translate-y-1/2 pl-4 z-999999"> <div className="top-19/20 z-999999 absolute left-0 -translate-y-1/2 transform pl-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<SituationBoard /> <SituationBoard />
</div> </div>
@@ -32,7 +32,7 @@ const DispatchPage = () => {
</div> </div>
<div <div
className={cn( className={cn(
"absolute right-0 w-[500px] z-999 transition-transform", "z-999 absolute right-0 w-[500px] transition-transform",
isOpen ? "translate-x-0" : "translate-x-full", isOpen ? "translate-x-0" : "translate-x-full",
)} )}
> >

View File

@@ -27,7 +27,6 @@ export default async function RootLayout({
}); });
if (!session) { if (!session) {
console.log(session);
return redirect("/logout"); return redirect("/logout");
} }

View File

@@ -31,7 +31,6 @@ export const useSounds = () => {
const timeouts: NodeJS.Timeout[] = []; const timeouts: NodeJS.Timeout[] = [];
if (page === "new-mission" && newMissionSound.current) { if (page === "new-mission" && newMissionSound.current) {
console.log("new-mission", mission);
newMissionSound.current.currentTime = 0; newMissionSound.current.currentTime = 0;
newMissionSound.current.play(); newMissionSound.current.play();
if (mission) { if (mission) {

Binary file not shown.

Before

Width:  |  Height:  |  Size: 194 KiB

After

Width:  |  Height:  |  Size: 252 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 322 KiB

After

Width:  |  Height:  |  Size: 366 KiB

View File

@@ -63,9 +63,9 @@ export const ConnectionBtn = () => {
const uid = session.data?.user?.id; const uid = session.data?.user?.id;
if (!uid) return null; if (!uid) return null;
return ( return (
<div className="rounded-box bg-base-200 flex justify-center items-center gap-2 p-1"> <div className="rounded-box bg-base-200 flex items-center justify-center gap-2 p-1">
{connection.message.length > 0 && ( {connection.message.length > 0 && (
<span className="mx-2 text-error">{connection.message}</span> <span className="text-error mx-2">{connection.message}</span>
)} )}
{connection.status == "connected" ? ( {connection.status == "connected" ? (
@@ -95,12 +95,12 @@ export const ConnectionBtn = () => {
<dialog ref={modalRef} className="modal"> <dialog ref={modalRef} className="modal">
<div className="modal-box flex flex-col items-center justify-center"> <div className="modal-box flex flex-col items-center justify-center">
{connection.status == "connected" ? ( {connection.status == "connected" ? (
<h3 className="text-lg font-bold mb-5"> <h3 className="mb-5 text-lg font-bold">
Verbunden als{" "} Verbunden als{" "}
<span className="text-info">&lt;{connection.selectedStation?.bosCallsign}&gt;</span> <span className="text-info">&lt;{connection.selectedStation?.bosCallsign}&gt;</span>
</h3> </h3>
) : ( ) : (
<h3 className="text-lg font-bold mb-5">Als Pilot anmelden</h3> <h3 className="mb-5 text-lg font-bold">Als Pilot anmelden</h3>
)} )}
{connection.status !== "connected" && ( {connection.status !== "connected" && (
<div className="w-full"> <div className="w-full">
@@ -135,7 +135,7 @@ export const ConnectionBtn = () => {
/> />
</div> </div>
)} )}
<fieldset className="fieldset w-full mt-2"> <fieldset className="fieldset mt-2 w-full">
<label className="floating-label w-full text-base"> <label className="floating-label w-full text-base">
<span>Logoff Zeit (LCL)</span> <span>Logoff Zeit (LCL)</span>
<input <input
@@ -171,8 +171,8 @@ export const ConnectionBtn = () => {
</label> </label>
</fieldset> </fieldset>
)} )}
<div className="modal-action flex justify-between w-full"> <div className="modal-action flex w-full justify-between">
<form method="dialog" className="w-full flex justify-between"> <form method="dialog" className="flex w-full justify-between">
<button className="btn btn-soft">Zurück</button> <button className="btn btn-soft">Zurück</button>
{connection.status == "connected" ? ( {connection.status == "connected" ? (
<> <>
@@ -183,7 +183,6 @@ export const ConnectionBtn = () => {
const [logoffHours, logoffMinutes] = const [logoffHours, logoffMinutes] =
form.logoffTime?.split(":").map(Number) || []; form.logoffTime?.split(":").map(Number) || [];
console.log(logoffHours, logoffMinutes, form.logoffTime);
await aircraftMutation.mutateAsync({ await aircraftMutation.mutateAsync({
sessionId: connection.connectedAircraft.id, sessionId: connection.connectedAircraft.id,
change: { change: {
@@ -220,10 +219,15 @@ export const ConnectionBtn = () => {
station.id === parseInt(form.selectedStationId?.toString() || ""), station.id === parseInt(form.selectedStationId?.toString() || ""),
); );
if (selectedStation) { if (selectedStation) {
const [logoffHours, logoffMinutes] =
form.logoffTime?.split(":").map(Number) || [];
await connection.connect( await connection.connect(
uid, uid,
form.selectedStationId?.toString() || "", form.selectedStationId?.toString() || "",
form.logoffTime || "", form.logoffTime && logoffHours !== undefined && logoffMinutes !== undefined
? getNextDateWithTime(logoffHours, logoffMinutes).toISOString()
: "",
selectedStation, selectedStation,
session.data!.user, session.data!.user,
form.debugPosition, form.debugPosition,

View File

@@ -12,6 +12,7 @@ import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { checkSimulatorConnected } from "@repo/shared-components"; import { checkSimulatorConnected } from "@repo/shared-components";
import { SimConnectionAlert } from "(app)/pilot/_components/SimConnectionAlert"; import { SimConnectionAlert } from "(app)/pilot/_components/SimConnectionAlert";
import { SettingsBoard } from "_components/left/SettingsBoard"; import { SettingsBoard } from "_components/left/SettingsBoard";
import { BugReport } from "_components/left/BugReport";
const Map = dynamic(() => import("_components/map/Map"), { const Map = dynamic(() => import("_components/map/Map"), {
ssr: false, ssr: false,
@@ -29,24 +30,23 @@ const PilotPage = () => {
const ownAircraft = aircrafts?.find((aircraft) => aircraft.id === connectedAircraft?.id); const ownAircraft = aircrafts?.find((aircraft) => aircraft.id === connectedAircraft?.id);
const simulatorConnected = ownAircraft ? checkSimulatorConnected(ownAircraft) : false; const simulatorConnected = ownAircraft ? checkSimulatorConnected(ownAircraft) : false;
return ( return (
<div className="relative flex-1 flex transition-all duration-500 ease w-full h-screen overflow-hidden"> <div className="ease relative flex h-screen w-full flex-1 overflow-hidden transition-all duration-500">
{/* <MapToastCard2 /> */} {/* <MapToastCard2 /> */}
<div className="flex flex-1 relative w-full h-full"> <div className="relative flex h-full w-full flex-1">
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 pl-4 z-999999"> <div className="z-999999 absolute left-0 top-1/2 flex -translate-y-1/2 transform flex-col space-y-2 pl-4">
<Chat /> <Chat />
<div className="mt-2"> <Report />
<Report /> <BugReport />
</div>
</div> </div>
<div className="flex w-2/3 h-full"> <div className="flex h-full w-2/3">
<div className="relative flex flex-1 h-full"> <div className="relative flex h-full flex-1">
<div className="absolute left-0 top-19/20 transform -translate-y-1/2 pl-4 z-999999"> <div className="top-19/20 z-999999 absolute left-0 -translate-y-1/2 transform pl-4">
<div className="flex items-center justify-between gap-4"> <div className="flex items-center justify-between gap-4">
<SettingsBoard /> <SettingsBoard />
</div> </div>
</div> </div>
<Map /> <Map />
<div className="absolute top-5 right-10 z-99999 space-y-2"> <div className="z-99999 absolute right-10 top-5 space-y-2">
{!simulatorConnected && status === "connected" && ( {!simulatorConnected && status === "connected" && (
<SimConnectionAlert lastUpdated={ownAircraft?.lastHeartbeat} /> <SimConnectionAlert lastUpdated={ownAircraft?.lastHeartbeat} />
)} )}
@@ -54,19 +54,19 @@ const PilotPage = () => {
</div> </div>
</div> </div>
</div> </div>
<div className="flex w-1/3 h-full"> <div className="flex h-full w-1/3">
<div className="flex flex-col w-full h-full p-4 bg-base-300"> <div className="bg-base-300 flex h-full w-full flex-col p-4">
<h2 className="card-title mb-2">MRT & DME</h2> <h2 className="card-title mb-2">MRT & DME</h2>
<div className="card bg-base-200 shadow-xl mb-4"> <div className="card bg-base-200 mb-4 shadow-xl">
<div className="card-body w-full h-full flex items-center justify-center"> <div className="card-body flex h-full w-full items-center justify-center">
<div className=" max-w-150"> <div className="max-w-150">
<Mrt /> <Mrt />
</div> </div>
</div> </div>
</div> </div>
<div className="card bg-base-200 shadow-xl h-1/2 flex"> <div className="card bg-base-200 flex h-1/2 shadow-xl">
<div className="card-body w-full h-full p-4 mb-0 flex items-center justify-center"> <div className="card-body mb-0 flex h-full w-full items-center justify-center p-4">
<div className=" max-w-140"> <div className="max-w-140">
<Dme /> <Dme />
</div> </div>
</div> </div>

View File

@@ -0,0 +1,16 @@
import { Bug } from "lucide-react";
export const BugReport = () => {
return (
<div className="indicator">
<a
className="btn btn-soft btn-sm btn-warning tooltip tooltip-right"
data-tip="Fehler melden"
href="https://discord.com/channels/1077269395019141140/1395892524404576367"
target="_blank"
>
<Bug className="h-4 w-4" />
</a>
</div>
);
};

View File

@@ -9,6 +9,7 @@ import { useQuery } from "@tanstack/react-query";
import { getConnectedDispatcherAPI } from "_querys/dispatcher"; import { getConnectedDispatcherAPI } from "_querys/dispatcher";
import { getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore"; import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
export const Chat = () => { export const Chat = () => {
const { const {
@@ -28,6 +29,7 @@ export const Chat = () => {
const [addTabValue, setAddTabValue] = useState<string>("default"); const [addTabValue, setAddTabValue] = useState<string>("default");
const [message, setMessage] = useState<string>(""); const [message, setMessage] = useState<string>("");
const dispatcherConnected = useDispatchConnectionStore((state) => state.status === "connected"); const dispatcherConnected = useDispatchConnectionStore((state) => state.status === "connected");
const pilotConnected = usePilotConnectionStore((state) => state.status === "connected");
const { data: dispatcher } = useQuery({ const { data: dispatcher } = useQuery({
queryKey: ["dispatcher"], queryKey: ["dispatcher"],
@@ -51,6 +53,14 @@ export const Chat = () => {
(a) => a.userId !== session.data?.user.id && dispatcherConnected, (a) => a.userId !== session.data?.user.id && dispatcherConnected,
); );
const btnActive = pilotConnected || dispatcherConnected;
useEffect(() => {
if (!btnActive) {
setChatOpen(false);
}
}, [btnActive, setChatOpen]);
return ( return (
<div className={cn("dropdown dropdown-right dropdown-center", chatOpen && "dropdown-open")}> <div className={cn("dropdown dropdown-right dropdown-center", chatOpen && "dropdown-open")}>
<div className="indicator"> <div className="indicator">
@@ -58,8 +68,12 @@ export const Chat = () => {
<span className="indicator-item status status-info animate-ping"></span> <span className="indicator-item status status-info animate-ping"></span>
)} )}
<button <button
className="btn btn-soft btn-sm btn-primary" className={cn(
"btn btn-soft btn-sm cursor-default",
btnActive && "btn-primary cursor-pointer",
)}
onClick={() => { onClick={() => {
if (!btnActive) return;
setReportTabOpen(false); setReportTabOpen(false);
setChatOpen(!chatOpen); setChatOpen(!chatOpen);
if (selectedChat) { if (selectedChat) {

View File

@@ -10,6 +10,8 @@ import { useQuery } from "@tanstack/react-query";
import { getConnectedDispatcherAPI } from "_querys/dispatcher"; import { getConnectedDispatcherAPI } from "_querys/dispatcher";
import { sendReportAPI } from "_querys/report"; import { sendReportAPI } from "_querys/report";
import { getConnectedAircraftsAPI } from "_querys/aircrafts"; import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
export const Report = () => { export const Report = () => {
const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } = useLeftMenuStore(); const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } = useLeftMenuStore();
@@ -18,6 +20,9 @@ export const Report = () => {
const [selectedPlayer, setSelectedPlayer] = useState<string>("default"); const [selectedPlayer, setSelectedPlayer] = useState<string>("default");
const [message, setMessage] = useState<string>(""); const [message, setMessage] = useState<string>("");
const dispatcherConnected = useDispatchConnectionStore((state) => state.status === "connected");
const pilotConnected = usePilotConnectionStore((state) => state.status === "connected");
useEffect(() => { useEffect(() => {
if (!session.data?.user.id) return; if (!session.data?.user.id) return;
setOwnId(session.data.user.id); setOwnId(session.data.user.id);
@@ -36,6 +41,13 @@ export const Report = () => {
const filteredDispatcher = dispatcher?.filter((d) => d.userId !== session.data?.user.id); const filteredDispatcher = dispatcher?.filter((d) => d.userId !== session.data?.user.id);
const filteredAircrafts = aircrafts?.filter((a) => a.userId !== session.data?.user.id); const filteredAircrafts = aircrafts?.filter((a) => a.userId !== session.data?.user.id);
const btnActive = pilotConnected || dispatcherConnected;
useEffect(() => {
if (!btnActive) {
setReportTabOpen(false);
}
}, [btnActive, setReportTabOpen]);
return ( return (
<div <div
@@ -43,8 +55,12 @@ export const Report = () => {
> >
<div className="indicator"> <div className="indicator">
<button <button
className="btn btn-soft btn-sm btn-error" className={cn(
"btn btn-soft btn-sm cursor-default",
btnActive && "cursor-pointer btn-error",
)}
onClick={() => { onClick={() => {
if (!btnActive) return;
setChatOpen(false); setChatOpen(false);
setReportTabOpen(!reportTabOpen); setReportTabOpen(!reportTabOpen);
}} }}

View File

@@ -61,8 +61,8 @@ const AircraftPopupContent = ({
return mission ? ( return mission ? (
<MissionTab mission={mission} /> <MissionTab mission={mission} />
) : ( ) : (
<div className="flex flex-col items-center justify-center min-h-full"> <div className="flex min-h-full flex-col items-center justify-center">
<span className="text-gray-500 my-10 font-semibold">Kein aktiver Einsatz</span> <span className="my-10 font-semibold text-gray-500">Kein aktiver Einsatz</span>
</div> </div>
); );
case "chat": case "chat":
@@ -77,7 +77,7 @@ const AircraftPopupContent = ({
return ( return (
<> <>
<div <div
className="absolute p-1 z-99 top-0 right-0 transform -translate-y-full bg-base-100 cursor-pointer" className="z-99 bg-base-100 absolute right-0 top-0 -translate-y-full transform cursor-pointer p-1"
onClick={() => { onClick={() => {
setOpenAircraftMarker({ setOpenAircraftMarker({
open: [], open: [],
@@ -90,7 +90,7 @@ const AircraftPopupContent = ({
<div <div
className={cn( className={cn(
"absolute w-[calc(100%+2px)] h-4 z-99", // As offset is 2px, we need to add 2px to the width "z-99 absolute h-4 w-[calc(100%+2px)]", // As offset is 2px, we need to add 2px to the width
anchor.includes("left") ? "-left-[2px]" : "-right-[2px]", anchor.includes("left") ? "-left-[2px]" : "-right-[2px]",
anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]", anchor.includes("top") ? "-top-[2px]" : "-bottom-[2px]",
)} )}
@@ -111,13 +111,13 @@ const AircraftPopupContent = ({
/> />
<div> <div>
<div <div
className="flex gap-[2px] text-white pb-0.5 overflow-auto" className="flex gap-[2px] overflow-auto pb-0.5 text-white"
style={{ style={{
backgroundColor: `${FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus]}`, backgroundColor: `${FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus]}`,
}} }}
> >
<div <div
className="px-3 flex justify-center items-center cursor-pointer" className="flex cursor-pointer items-center justify-center px-3"
style={{ style={{
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`, backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
borderBottom: borderBottom:
@@ -130,7 +130,7 @@ const AircraftPopupContent = ({
<House className="text-sm" /> <House className="text-sm" />
</div> </div>
<div <div
className="px-4 flex justify-center items-center cursor-pointer" className="flex cursor-pointer items-center justify-center px-4"
style={{ style={{
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`, backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
}} }}
@@ -145,7 +145,7 @@ const AircraftPopupContent = ({
<ChevronsRightLeft className="text-sm" /> <ChevronsRightLeft className="text-sm" />
</div> </div>
<div <div
className="flex justify-center items-center text-5xl font-bold px-6 cursor-pointer" className="flex cursor-pointer items-center justify-center px-6 text-5xl font-bold"
style={{ style={{
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`, backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
borderBottom: borderBottom:
@@ -159,7 +159,7 @@ const AircraftPopupContent = ({
{aircraft.fmsStatus} {aircraft.fmsStatus}
</div> </div>
<div <div
className="cursor-pointer px-2 min-w-[130px]" className="min-w-[130px] cursor-pointer px-2"
style={{ style={{
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`, backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
borderBottom: borderBottom:
@@ -169,8 +169,8 @@ const AircraftPopupContent = ({
}} }}
onClick={() => handleTabChange("aircraft")} onClick={() => handleTabChange("aircraft")}
> >
<span className="text-white text-base font-medium truncate"> <span className="truncate text-base font-medium text-white">
{aircraft.Station.bosCallsign.length > 16 {aircraft.Station.bosCallsign.length > 15
? aircraft.Station.bosCallsignShort ? aircraft.Station.bosCallsignShort
: aircraft.Station.bosCallsign} : aircraft.Station.bosCallsign}
</span> </span>
@@ -193,14 +193,14 @@ const AircraftPopupContent = ({
}} }}
onClick={() => handleTabChange("mission")} onClick={() => handleTabChange("mission")}
> >
<span className="text-white text-base font-medium">Einsatz</span> <span className="text-base font-medium text-white">Einsatz</span>
<br /> <br />
<span className="text-white text-sm font-medium"> <span className="text-sm font-medium text-white">
{mission?.publicId || "kein Einsatz"} {mission?.publicId || "kein Einsatz"}
</span> </span>
</div> </div>
<div <div
className="px-4 flex justify-center items-center cursor-pointer" className="flex cursor-pointer items-center justify-center px-4"
style={{ style={{
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`, backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
borderBottom: borderBottom:

View File

@@ -53,7 +53,7 @@ export const ContextMenu = () => {
if (!contextMenu || !dispatcherConnected) return null; if (!contextMenu || !dispatcherConnected) return null;
const einsatzBtnText = missionFormValues && isOpen ? "Position übernehmen" : "Einsatz erstellen"; const missionBtnText = missionFormValues && isOpen ? "Position übernehmen" : "Einsatz erstellen";
const addOSMobjects = async (ignorePreviosSelected?: boolean) => { const addOSMobjects = async (ignorePreviosSelected?: boolean) => {
const res = await fetch( const res = await fetch(
@@ -108,7 +108,7 @@ export const ContextMenu = () => {
{/* Top Button */} {/* Top Button */}
<button <button
className="btn btn-circle bg-rescuetrack w-10 h-10 absolute left-1/2 top-0 pointer-events-auto opacity-80 tooltip tooltip-top tooltip-accent" className="btn btn-circle bg-rescuetrack w-10 h-10 absolute left-1/2 top-0 pointer-events-auto opacity-80 tooltip tooltip-top tooltip-accent"
data-tip={einsatzBtnText} data-tip={missionBtnText}
style={{ transform: "translateX(-50%)" }} style={{ transform: "translateX(-50%)" }}
onClick={async () => { onClick={async () => {
const { parsed } = await getOsmAddress(contextMenu.lat, contextMenu.lng); const { parsed } = await getOsmAddress(contextMenu.lat, contextMenu.lng);

View File

@@ -12,6 +12,7 @@ import { MarkerCluster } from "_components/map/_components/MarkerCluster";
import { useEffect, useRef } from "react"; import { useEffect, useRef } from "react";
import { Map as TMap } from "leaflet"; import { Map as TMap } from "leaflet";
import { DistanceLayer } from "_components/map/Measurement"; import { DistanceLayer } from "_components/map/Measurement";
import { MapAdditionals } from "_components/map/MapAdditionals";
const Map = () => { const Map = () => {
const ref = useRef<TMap | null>(null); const ref = useRef<TMap | null>(null);
@@ -48,6 +49,7 @@ const Map = () => {
<MissionLayer /> <MissionLayer />
<AircraftLayer /> <AircraftLayer />
<DistanceLayer /> <DistanceLayer />
<MapAdditionals />
</MapContainer> </MapContainer>
); );
}; };

View File

@@ -0,0 +1,76 @@
"use client";
import { usePannelStore } from "_store/pannelStore";
import { Marker } from "react-leaflet";
import L from "leaflet";
import { useQuery } from "@tanstack/react-query";
import { getMissionsAPI } from "_querys/missions";
import { HPGValidationRequired } from "_helpers/hpgValidationRequired";
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
import { useMapStore } from "_store/mapStore";
export const MapAdditionals = () => {
const { isOpen, missionFormValues } = usePannelStore((state) => state);
const { data: missions = [] } = useQuery({
queryKey: ["missions"],
queryFn: () =>
getMissionsAPI({
OR: [{ state: "draft" }, { state: "running" }],
}),
refetchInterval: 10_000,
});
const mapStore = useMapStore((state) => state);
const { data: aircrafts } = useQuery({
queryKey: ["aircrafts"],
queryFn: () => getConnectedAircraftsAPI(),
refetchInterval: 10000,
});
const markersNeedingAttention = missions.filter(
(m) =>
HPGValidationRequired(m.missionStationIds, aircrafts, m.hpgMissionString) &&
m.hpgValidationState === "POSITION_AMANDED" &&
m.state === "draft" &&
m.hpgLocationLat &&
m.hpgLocationLng,
);
return (
<>
{missionFormValues?.addressLat && missionFormValues?.addressLng && isOpen && (
<Marker
position={[missionFormValues.addressLat, missionFormValues.addressLng]}
icon={L.icon({
iconUrl: "/icons/mapMarker.png",
iconSize: [40, 40],
iconAnchor: [20, 35],
})}
interactive={false}
/>
)}
{markersNeedingAttention.map((mission) => (
<Marker
key={mission.id}
position={[mission.hpgLocationLat!, mission.hpgLocationLng!]}
icon={L.icon({
iconUrl: "/icons/mapMarker.png",
iconSize: [40, 40],
iconAnchor: [20, 35],
})}
eventHandlers={{
click: () =>
mapStore.setOpenMissionMarker({
open: [
{
id: mission.id,
tab: "home",
},
],
close: [],
}),
}}
/>
))}
</>
);
};

View File

@@ -338,22 +338,15 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
return [ return [
editingMissionId === mission.id && missionFormValues?.addressLat editingMissionId === mission.id && missionFormValues?.addressLat
? missionFormValues.addressLat ? missionFormValues.addressLat
: mission.hpgValidationState !== "POSITION_AMANDED" && mission.hpgLocationLat : mission.addressLat,
? mission.hpgLocationLat
: mission.addressLat,
editingMissionId === mission.id && missionFormValues?.addressLng editingMissionId === mission.id && missionFormValues?.addressLng
? missionFormValues.addressLng ? missionFormValues.addressLng
: mission.hpgValidationState !== "POSITION_AMANDED" && mission.hpgLocationLng : mission.addressLng,
? mission.hpgLocationLng
: mission.addressLng,
]; ];
}, [ }, [
editingMissionId, editingMissionId,
mission.addressLat, mission.addressLat,
mission.addressLng, mission.addressLng,
mission.hpgLocationLat,
mission.hpgLocationLng,
mission.hpgValidationState,
mission.id, mission.id,
missionFormValues?.addressLat, missionFormValues?.addressLat,
missionFormValues?.addressLng, missionFormValues?.addressLng,

View File

@@ -64,9 +64,9 @@ const FMSStatusHistory = ({
return ( return (
<div className="p-4"> <div className="p-4">
<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="mb-1 flex items-center gap-2">
<p className="flex items-center gap-2 flex-1"> <p className="flex flex-1 items-center gap-2">
<PersonIcon className="w-5 h-5" /> {aircraftUser.fullName} ({aircraftUser.publicId}){" "} <PersonIcon className="h-5 w-5" /> {aircraftUser.fullName} ({aircraftUser.publicId}){" "}
{(() => { {(() => {
const badges = aircraftUser.badges const badges = aircraftUser.badges
.filter((b) => b.startsWith("P") && b.length == 2) .filter((b) => b.startsWith("P") && b.length == 2)
@@ -96,12 +96,12 @@ const FMSStatusHistory = ({
</p> </p>
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-0" />
<ul className="space-y-2"> <ul className="space-y-2">
{log.map((entry, index) => ( {log.map((entry, index) => (
<li key={index} className="flex items-center gap-2"> <li key={index} className="flex items-center gap-2">
<span <span
className="font-bold text-base" className="text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus], color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
}} }}
@@ -145,8 +145,8 @@ const FMSStatusSelector = ({
}); });
return ( return (
<div className="flex flex-col gap-2 mt-2 p-4 text-base-content"> <div className="text-base-content mt-2 flex flex-col gap-2 p-4">
<div className="flex gap-2 justify-center items-center h-full"> <div className="flex h-full items-center justify-center gap-2">
{Array.from({ length: 9 }, (_, i) => (i + 1).toString()) {Array.from({ length: 9 }, (_, i) => (i + 1).toString())
.filter((status) => status !== "5") // Exclude status 5 .filter((status) => status !== "5") // Exclude status 5
.map((status) => ( .map((status) => (
@@ -154,7 +154,7 @@ const FMSStatusSelector = ({
disabled={!dispatcherConnected} disabled={!dispatcherConnected}
key={status} key={status}
className={cn( className={cn(
"flex justify-center items-center min-w-13 min-h-13 cursor-pointer text-4xl font-bold", "min-w-13 min-h-13 flex cursor-pointer items-center justify-center text-4xl font-bold",
!dispatcherConnected && "cursor-not-allowed", !dispatcherConnected && "cursor-not-allowed",
)} )}
style={{ style={{
@@ -187,13 +187,13 @@ const FMSStatusSelector = ({
</button> </button>
))} ))}
</div> </div>
<div className="flex gap-1 p-2 justify-center items-center"> <div className="flex items-center justify-center gap-1 p-2">
{["E", "C", "F", "J", "L", "c", "d", "h", "o", "u"].map((status) => ( {["E", "C", "F", "J", "L", "c", "d", "h", "o", "u"].map((status) => (
<button <button
disabled={!dispatcherConnected} disabled={!dispatcherConnected}
key={status} key={status}
className={cn( className={cn(
"flex justify-center items-center min-w-10 min-h-10 cursor-pointer text-lg font-bold", "flex min-h-10 min-w-10 cursor-pointer items-center justify-center text-lg font-bold",
!dispatcherConnected && "cursor-not-allowed", !dispatcherConnected && "cursor-not-allowed",
)} )}
style={{ style={{
@@ -253,17 +253,17 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
}, [aircraft.posLng, aircraft.posLat, station.bosRadioArea]); }, [aircraft.posLng, aircraft.posLat, station.bosRadioArea]);
return ( return (
<div className="p-4 text-base-content"> <div className="text-base-content p-4">
<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="mb-1 flex items-center gap-2">
<Component size={16} /> Aktuelle Rufgruppe: {livekitUser?.roomName || "Nicht verbunden"} <Component size={16} /> Aktuelle Rufgruppe: {livekitUser?.roomName || "Nicht verbunden"}
</li> </li>
<li className="flex items-center gap-2 mb-1"> <li className="mb-1 flex items-center gap-2">
<RadioTower size={16} /> Leitstellenbereich: {lstName || station.bosRadioArea} <RadioTower size={16} /> Leitstellenbereich: {lstName || station.bosRadioArea}
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-0" />
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2 mb-2"> <div className="mb-2 mt-2 flex items-center justify-between pr-2 text-sm font-semibold">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Clock size={16} /> {station.is24h ? "24h Betrieb" : "Tagbetrieb"} <Clock size={16} /> {station.is24h ? "24h Betrieb" : "Tagbetrieb"}
</span> </span>
@@ -277,8 +277,8 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
<TextSearch size={16} /> {station.aircraftRegistration} <TextSearch size={16} /> {station.aircraftRegistration}
</span> </span>
</div> </div>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-0" />
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2"> <div className="mt-2 flex items-center justify-between pr-2 text-sm font-semibold">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<CompassIcon size={16} /> HDG: {aircraft.posHeading}° <CompassIcon size={16} /> HDG: {aircraft.posHeading}°
</span> </span>
@@ -289,7 +289,7 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
<CircleGaugeIcon size={16} /> ALT: {aircraft.posAlt} ft <CircleGaugeIcon size={16} /> ALT: {aircraft.posAlt} ft
</span> </span>
</div> </div>
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2"> <div className="mt-2 flex items-center justify-between pr-2 text-sm font-semibold">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<Lollipop size={16} />{" "} <Lollipop size={16} />{" "}
<span className={cn(aircraft.posH145active && "text-green-500")}> <span className={cn(aircraft.posH145active && "text-green-500")}>
@@ -303,22 +303,22 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
const MissionTab = ({ mission }: { mission: Mission }) => { const MissionTab = ({ mission }: { mission: Mission }) => {
return ( return (
<div className="p-4 text-base-content"> <div className="text-base-content p-4">
<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="mb-1 flex items-center gap-2">
<BellRing size={16} /> {mission.missionKeywordCategory} <BellRing size={16} /> {mission.missionKeywordCategory}
</li> </li>
<li className="flex items-center gap-2 mb-1"> <li className="mb-1 flex items-center gap-2">
<ListCollapse size={16} /> <ListCollapse size={16} />
{mission.missionKeywordName} {mission.missionKeywordName}
</li> </li>
<li className="flex items-center gap-2 mt-3"> <li className="mt-3 flex items-center gap-2">
<Hash size={16} /> <Hash size={16} />
__{new Date().toISOString().slice(0, 10).replace(/-/g, "")} __{new Date().toISOString().slice(0, 10).replace(/-/g, "")}
{mission.id} {mission.id}
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-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} /> {mission.addressLat} {mission.addressLng} <MapPin size={16} /> {mission.addressLat} {mission.addressLng}
@@ -416,7 +416,7 @@ const SDSTab = ({
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!isChatOpen ? ( {!isChatOpen ? (
<button <button
className="text-base-content text-base cursor-pointer" className="text-base-content cursor-pointer text-base"
onClick={() => setIsChatOpen(true)} onClick={() => setIsChatOpen(true)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@@ -424,7 +424,7 @@ const SDSTab = ({
</span> </span>
</button> </button>
) : ( ) : (
<div className="flex items-center gap-2 w-full"> <div className="flex w-full items-center gap-2">
<input <input
autoFocus autoFocus
type="text" type="text"
@@ -463,7 +463,7 @@ const SDSTab = ({
<div className="divider m-0" /> <div className="divider m-0" />
</div> </div>
)} )}
<ul className="space-y-2 max-h-[300px] overflow-y-auto overflow-x-auto"> <ul className="max-h-[300px] space-y-2 overflow-x-auto overflow-y-auto">
{log.map((entry, index) => { {log.map((entry, index) => {
const sdsEntry = entry as MissionSdsLog; const sdsEntry = entry as MissionSdsLog;
return ( return (
@@ -475,7 +475,7 @@ const SDSTab = ({
})} })}
</span> </span>
<span <span
className="font-bold text-base" className="text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[6], color: FMS_STATUS_TEXT_COLORS[6],
}} }}
@@ -488,7 +488,7 @@ const SDSTab = ({
); );
})} })}
{!log.length && ( {!log.length && (
<p className="text-gray-500 w-full text-center my-10 font-semibold"> <p className="my-10 w-full text-center font-semibold text-gray-500">
Kein SDS-Verlauf verfügbar Kein SDS-Verlauf verfügbar
</p> </p>
)} )}

View File

@@ -93,8 +93,8 @@ const Einsatzdetails = ({
const { setMissionFormValues, setOpen, setEditingMission } = usePannelStore((state) => state); const { setMissionFormValues, setOpen, setEditingMission } = usePannelStore((state) => state);
const [ignoreHpg, setIgnoreHpg] = useState(false); const [ignoreHpg, setIgnoreHpg] = useState(false);
return ( return (
<div className="p-4 text-base-content"> <div className="text-base-content p-4">
<div className="flex items-center justify-between mb-3"> <div className="mb-3 flex items-center justify-between">
<h2 className="flex items-center gap-2 text-lg font-bold"> <h2 className="flex items-center gap-2 text-lg font-bold">
<Flag /> Einsatzdetails <Flag /> Einsatzdetails
</h2> </h2>
@@ -126,7 +126,7 @@ const Einsatzdetails = ({
</button> </button>
</div> </div>
<div <div
className="tooltip tooltip-warning tooltip-left font-semibold z-[9999]" className="tooltip tooltip-warning tooltip-left z-[9999] font-semibold"
data-tip="Einsatz abschließen" data-tip="Einsatz abschließen"
> >
<button <button
@@ -161,19 +161,19 @@ const Einsatzdetails = ({
)} )}
</div> </div>
<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="mb-1 flex items-center gap-2">
<BellRing size={16} /> {mission.missionKeywordCategory} <BellRing size={16} /> {mission.missionKeywordCategory}
</li> </li>
<li className="flex items-center gap-2 mb-1"> <li className="mb-1 flex items-center gap-2">
<ListCollapse size={16} /> <ListCollapse size={16} />
{mission.missionKeywordName} {mission.missionKeywordName}
</li> </li>
<li className="flex items-center gap-2 mt-3"> <li className="mt-3 flex items-center gap-2">
<Hash size={16} /> <Hash size={16} />
{mission.publicId} {mission.publicId}
</li> </li>
</ul> </ul>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-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} /> {mission.addressLat} {mission.addressLng} <MapPin size={16} /> {mission.addressLat} {mission.addressLng}
@@ -192,7 +192,7 @@ const Einsatzdetails = ({
</div> </div>
{mission.type == "sekundär" && ( {mission.type == "sekundär" && (
<> <>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-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">
<Route size={16} /> {mission.addressMissionDestination} <Route size={16} /> {mission.addressMissionDestination}
@@ -202,11 +202,11 @@ const Einsatzdetails = ({
)} )}
{mission.state === "draft" && ( {mission.state === "draft" && (
<div> <div>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-0" />
{hpgNeedsAttention && mission.hpgValidationState !== "POSITION_AMANDED" && ( {hpgNeedsAttention && mission.hpgValidationState !== "POSITION_AMANDED" && (
<div className="form-control mb-2 flex justify-between items-center"> <div className="form-control mb-2 flex items-center justify-between">
<label className="flex items-center gap-2 cursor-pointer"> <label className="flex cursor-pointer items-center gap-2">
<input <input
type="checkbox" type="checkbox"
className="checkbox checkbox-sm checkbox-primary" className="checkbox checkbox-sm checkbox-primary"
@@ -214,7 +214,7 @@ const Einsatzdetails = ({
onChange={(e) => setIgnoreHpg(e.target.checked)} onChange={(e) => setIgnoreHpg(e.target.checked)}
/> />
<span <span
className="label-text font-semibold leading-6 tooltip" className="label-text tooltip font-semibold leading-6"
data-tip="Die HPG-Alarmierung wird trotzdem ausgeführt. Die Position des HPG-Einsatzes kann gravierend von der Einsatzposition abweichen" data-tip="Die HPG-Alarmierung wird trotzdem ausgeführt. Die Position des HPG-Einsatzes kann gravierend von der Einsatzposition abweichen"
> >
HPG-Fehler ignorieren HPG-Fehler ignorieren
@@ -235,7 +235,7 @@ const Einsatzdetails = ({
</div> </div>
)} )}
<div className="flex items-center gap-2 w-full"> <div className="flex w-full items-center gap-2">
{(!hpgNeedsAttention || ignoreHpg) && {(!hpgNeedsAttention || ignoreHpg) &&
mission.hpgValidationState !== HpgValidationState.POSITION_AMANDED && ( mission.hpgValidationState !== HpgValidationState.POSITION_AMANDED && (
<button <button
@@ -354,13 +354,13 @@ const Einsatzdetails = ({
const Patientdetails = ({ mission }: { mission: Mission }) => { const Patientdetails = ({ mission }: { mission: Mission }) => {
return ( return (
<div className="p-4 text-base-content"> <div className="text-base-content p-4">
<h2 className="flex items-center gap-2 text-lg font-bold mb-3"> <h2 className="mb-3 flex items-center gap-2 text-lg font-bold">
<User /> Patientendetails <User /> Patientendetails
</h2> </h2>
<p className="text-base-content font-semibold">{mission.missionPatientInfo}</p> <p className="text-base-content font-semibold">{mission.missionPatientInfo}</p>
<div className="divider my-2" /> <div className="divider my-2" />
<h2 className="flex items-center gap-2 text-lg font-bold mb-3"> <h2 className="mb-3 flex items-center gap-2 text-lg font-bold">
<Cross /> Einsatzinformationen <Cross /> Einsatzinformationen
</h2> </h2>
<p className="text-base-content font-semibold">{mission.missionAdditionalInfo}</p> <p className="text-base-content font-semibold">{mission.missionAdditionalInfo}</p>
@@ -440,7 +440,7 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => ( const HPGVehicle = ({ state, name }: { state: HpgState; name: string }) => (
<li className="flex items-center gap-2"> <li className="flex items-center gap-2">
<span <span
className="font-bold text-base" className="text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[hpgStateToFMSStatus(state)], color: FMS_STATUS_TEXT_COLORS[hpgStateToFMSStatus(state)],
}} }}
@@ -457,8 +457,8 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
); );
return ( return (
<div className="p-4 text-base-content"> <div className="text-base-content p-4">
<div className="flex items-center w-full justify-between mb-2"> <div className="mb-2 flex w-full items-center justify-between">
<h2 className="flex items-center gap-2 text-lg font-bold"> <h2 className="flex items-center gap-2 text-lg font-bold">
<SmartphoneNfc /> Rettungsmittel <SmartphoneNfc /> Rettungsmittel
</h2> </h2>
@@ -480,9 +480,9 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
</div> </div>
)} )}
</div> </div>
<ul className="space-y-2 h-[130px] overflow-y-auto overflow-x-auto flex-1"> <ul className="h-[130px] flex-1 space-y-2 overflow-x-auto overflow-y-auto">
{mission.missionStationIds.length === 0 && ( {mission.missionStationIds.length === 0 && (
<p className="text-gray-500 w-full text-center my-10 font-semibold"> <p className="my-10 w-full text-center font-semibold text-gray-500">
Keine Rettungsmittel zugewiesen Keine Rettungsmittel zugewiesen
</p> </p>
)} )}
@@ -494,17 +494,17 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
return ( return (
<li key={index} className="flex items-center gap-2"> <li key={index} className="flex items-center gap-2">
<span <span
className="font-bold text-base" className="text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[connectedAircraft?.fmsStatus || "6"], color: FMS_STATUS_TEXT_COLORS[connectedAircraft?.fmsStatus || "6"],
}} }}
> >
{connectedAircraft?.fmsStatus || "6"} {connectedAircraft?.fmsStatus || "6"}
</span> </span>
<span className="text-base-content flex flex-col "> <span className="text-base-content flex flex-col">
<span className="font-bold">{station.bosCallsign}</span> <span className="font-bold">{station.bosCallsign}</span>
{!connectedAircraft && ( {!connectedAircraft && (
<span className="text-gray-400 text-xs">Kein Benutzer verbunden</span> <span className="text-xs text-gray-400">Kein Benutzer verbunden</span>
)} )}
</span> </span>
</li> </li>
@@ -522,7 +522,7 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
</ul> </ul>
{dispatcherConnected && ( {dispatcherConnected && (
<div> <div>
<div className="divider mt-0 mb-0" /> <div className="divider mb-0 mt-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 */}
<StationsSelect <StationsSelect
@@ -530,7 +530,6 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
className="min-w-[320px] flex-1" className="min-w-[320px] flex-1"
isMulti={false} isMulti={false}
onChange={(v) => { onChange={(v) => {
console.log("Selected station:", v);
setSelectedStation({ setSelectedStation({
selectedStationId: v?.selectedStationIds[0], selectedStationId: v?.selectedStationIds[0],
hpgAmbulanceState: mission.hpgAmbulanceState || HpgState.NOT_REQUESTED, hpgAmbulanceState: mission.hpgAmbulanceState || HpgState.NOT_REQUESTED,
@@ -657,7 +656,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
<div className="flex items-center gap-2"> <div className="flex items-center gap-2">
{!isAddingNote ? ( {!isAddingNote ? (
<button <button
className="text-base-content text-base cursor-pointer" className="text-base-content cursor-pointer text-base"
onClick={() => setIsAddingNote(true)} onClick={() => setIsAddingNote(true)}
> >
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
@@ -665,7 +664,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
</span> </span>
</button> </button>
) : ( ) : (
<div className="flex items-center gap-2 w-full"> <div className="flex w-full items-center gap-2">
<input <input
type="text" type="text"
placeholder="" placeholder=""
@@ -697,7 +696,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
<div className="divider m-0" /> <div className="divider m-0" />
</div> </div>
)} )}
<ul className="space-y-1 max-h-[300px] overflow-y-auto overflow-x-auto"> <ul className="max-h-[300px] space-y-1 overflow-x-auto overflow-y-auto">
{(mission.missionLog as unknown as MissionLog[]) {(mission.missionLog as unknown as MissionLog[])
.slice() .slice()
.reverse() .reverse()
@@ -712,7 +711,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
})} })}
</span> </span>
<span <span
className="font-bold text-base" className="text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus], color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
}} }}
@@ -732,7 +731,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
})} })}
</span> </span>
<span <span
className="font-bold text-base flex items-center gap-0.5" className="flex items-center gap-0.5 text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[6], color: FMS_STATUS_TEXT_COLORS[6],
}} }}
@@ -781,7 +780,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
})} })}
</span> </span>
<span <span
className="font-bold text-base flex items-center gap-0.5" className="flex items-center gap-0.5 text-base font-bold"
style={{ style={{
color: FMS_STATUS_TEXT_COLORS[6], color: FMS_STATUS_TEXT_COLORS[6],
}} }}
@@ -830,7 +829,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
})} })}
</ul> </ul>
{!mission.missionLog.length && ( {!mission.missionLog.length && (
<p className="text-gray-500 w-full text-center my-10 font-semibold"> <p className="my-10 w-full text-center font-semibold text-gray-500">
Keine Notizen verfügbar Keine Notizen verfügbar
</p> </p>
)} )}

View File

@@ -91,16 +91,16 @@ export const SettingsBtn = () => {
modalRef.current?.showModal(); modalRef.current?.showModal();
}} }}
> >
<GearIcon className="w-5 h-5" /> <GearIcon className="h-5 w-5" />
</button> </button>
<dialog ref={modalRef} className="modal"> <dialog ref={modalRef} className="modal">
<div className="modal-box"> <div className="modal-box">
<h3 className="flex items-center gap-2 text-lg font-bold mb-5"> <h3 className="mb-5 flex items-center gap-2 text-lg font-bold">
<SettingsIcon size={20} /> Einstellungen <SettingsIcon size={20} /> Einstellungen
</h3> </h3>
<div className="flex flex-col items-center justify-center"> <div className="flex flex-col items-center justify-center">
<fieldset className="fieldset w-full mb-2"> <fieldset className="fieldset mb-2 w-full">
<label className="floating-label w-full text-base"> <label className="floating-label w-full text-base">
<span>Eingabegerät</span> <span>Eingabegerät</span>
<select <select
@@ -122,7 +122,7 @@ export const SettingsBtn = () => {
</select> </select>
</label> </label>
</fieldset> </fieldset>
<p className="flex items-center gap-2 text-base mb-2 justify-start w-full"> <p className="mb-2 flex w-full items-center justify-start gap-2 text-base">
<Volume2 size={20} /> Eingabelautstärke <Volume2 size={20} /> Eingabelautstärke
</p> </p>
<div className="w-full"> <div className="w-full">
@@ -139,7 +139,7 @@ export const SettingsBtn = () => {
value={settings.micVolume} value={settings.micVolume}
className="range range-xs range-accent w-full" className="range range-xs range-accent w-full"
/> />
<div className="flex justify-between px-2.5 mt-2 text-xs"> <div className="mt-2 flex justify-between px-2.5 text-xs">
<span>0%</span> <span>0%</span>
<span>25%</span> <span>25%</span>
<span>50%</span> <span>50%</span>
@@ -155,24 +155,23 @@ export const SettingsBtn = () => {
)} )}
<div className="divider w-full" /> <div className="divider w-full" />
</div> </div>
<p className="flex items-center gap-2 text-base mb-2"> <p className="mb-2 flex items-center gap-2 text-base">
<Volume2 size={20} /> Funk Lautstärke <Volume2 size={20} /> Funk Lautstärke
</p> </p>
<div className="w-full mb-2"> <div className="mb-2 w-full">
<input <input
type="range" type="range"
min={0} min={0}
max={1} max={1}
step={0.01} step={0.01}
onChange={(e) => { onChange={(e) => {
console.log("Radio Volume", e.target.value);
const value = parseFloat(e.target.value); const value = parseFloat(e.target.value);
setSettingsPartial({ radioVolume: value }); setSettingsPartial({ radioVolume: value });
}} }}
value={settings.radioVolume} value={settings.radioVolume}
className="range range-xs range-primary w-full" className="range range-xs range-primary w-full"
/> />
<div className="flex justify-between px-2.5 mt-2 text-xs"> <div className="mt-2 flex justify-between px-2.5 text-xs">
<span>0%</span> <span>0%</span>
<span>25%</span> <span>25%</span>
<span>50%</span> <span>50%</span>
@@ -181,7 +180,7 @@ export const SettingsBtn = () => {
</div> </div>
</div> </div>
<p className="flex items-center gap-2 text-base mb-2"> <p className="mb-2 flex items-center gap-2 text-base">
<Volume2 size={20} /> Melder Lautstärke <Volume2 size={20} /> Melder Lautstärke
</p> </p>
<div className="w-full"> <div className="w-full">
@@ -200,7 +199,7 @@ export const SettingsBtn = () => {
value={settings.dmeVolume} value={settings.dmeVolume}
className="range range-xs range-primary w-full" className="range range-xs range-primary w-full"
/> />
<div className="flex justify-between px-2.5 mt-2 text-xs"> <div className="mt-2 flex justify-between px-2.5 text-xs">
<span>0%</span> <span>0%</span>
<span>25%</span> <span>25%</span>
<span>50%</span> <span>50%</span>
@@ -208,12 +207,12 @@ export const SettingsBtn = () => {
<span>100%</span> <span>100%</span>
</div> </div>
</div> </div>
<div className="flex justify-center w-full"> <div className="flex w-full justify-center">
<div className="divider w-full" /> <div className="divider w-full" />
</div> </div>
<div className="w-full"> <div className="w-full">
<label className="floating-label w-full"> <label className="floating-label w-full">
<span className="text-lg flex items-center gap-2"> <span className="flex items-center gap-2 text-lg">
<Bell /> NTFY room <Bell /> NTFY room
</span> </span>
<input <input
@@ -226,7 +225,7 @@ export const SettingsBtn = () => {
<p className="label mt-2 w-full"> <p className="label mt-2 w-full">
<Link <Link
href="https://docs.virtualairrescue.com/docs/Leitstelle/App-Alarmierung#download" href="https://docs.virtualairrescue.com/pilotenbereich/app-alarmierung.html#download"
target="_blank" target="_blank"
rel="noopener noreferrer" rel="noopener noreferrer"
className="link link-hover link-primary" className="link link-hover link-primary"
@@ -237,7 +236,7 @@ export const SettingsBtn = () => {
</p> </p>
</div> </div>
<div className="flex justify-between modal-action"> <div className="modal-action flex justify-between">
<button <button
className="btn btn-soft" className="btn btn-soft"
type="submit" type="submit"

View File

@@ -4,33 +4,38 @@ import {
RemoteParticipant, RemoteParticipant,
RemoteTrack, RemoteTrack,
RemoteTrackPublication, RemoteTrackPublication,
Track,
} from "livekit-client"; } from "livekit-client";
const initialTrackTimeouts = new Map<string, NodeJS.Timeout>();
export const handleTrackSubscribed = ( export const handleTrackSubscribed = (
track: RemoteTrack, track: RemoteTrack,
publication: RemoteTrackPublication, publication: RemoteTrackPublication,
participant: RemoteParticipant, participant: RemoteParticipant,
) => { ) => {
const element = track.attach();
element.pause();
if (!track.isMuted) { if (!track.isMuted) {
useAudioStore.getState().addSpeakingParticipant(participant); initialTrackTimeouts.set(
participant.sid,
setTimeout(() => {
useAudioStore.getState().addSpeakingParticipant(participant);
}, 1000),
);
} }
setTimeout(() => {
if (track.kind === Track.Kind.Video || track.kind === Track.Kind.Audio) {
// attach it to a new HTMLVideoElement or HTMLAudioElement
const element = track.attach();
element.play(); element.play();
}, 1000);
track.on("unmuted", () => { track.on("unmuted", () => {
useAudioStore.getState().addSpeakingParticipant(participant); useAudioStore.getState().addSpeakingParticipant(participant);
element.volume = useAudioStore.getState().settings.radioVolume; element.volume = useAudioStore.getState().settings.radioVolume;
}); });
track.on("unmuted", () => {
useAudioStore.getState().addSpeakingParticipant(participant);
});
}
track.on("muted", () => { track.on("muted", () => {
clearTimeout(initialTrackTimeouts.get(participant.sid));
initialTrackTimeouts.get(participant.sid);
useAudioStore.getState().removeSpeakingParticipant(participant); useAudioStore.getState().removeSpeakingParticipant(participant);
}); });
}; };

View File

@@ -108,7 +108,6 @@ export const useAudioStore = create<TalkState>((set, get) => ({
) { ) {
const { room, disconnect, connect } = get(); const { room, disconnect, connect } = get();
const role = room?.localParticipant.attributes.role; const role = room?.localParticipant.attributes.role;
console.log(role);
if (room?.name || role) { if (room?.name || role) {
disconnect(); disconnect();
connect(room?.name || "", role || "user"); connect(room?.name || "", role || "user");
@@ -170,10 +169,8 @@ export const useAudioStore = create<TalkState>((set, get) => ({
const inputStream = await navigator.mediaDevices.getUserMedia({ const inputStream = await navigator.mediaDevices.getUserMedia({
audio: { audio: {
deviceId: get().settings.micDeviceId ?? undefined, deviceId: get().settings.micDeviceId ?? undefined,
noiseSuppression: true,
}, },
}); });
// Funk-Effekt anwenden // Funk-Effekt anwenden
const radioStream = getRadioStream(inputStream, get().settings.micVolume); const radioStream = getRadioStream(inputStream, get().settings.micVolume);
if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen"); if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen");
@@ -186,6 +183,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
source: Track.Source.Microphone, source: Track.Source.Microphone,
}); });
await publishedTrack.mute(); await publishedTrack.mute();
set({ localRadioTrack: publishedTrack }); set({ localRadioTrack: publishedTrack });
set({ state: "connected", room, message: null }); set({ state: "connected", room, message: null });

View File

@@ -116,14 +116,12 @@ dispatchSocket.on(
"chat-message", "chat-message",
({ userId, message }: { userId: string; message: ChatMessage }) => { ({ userId, message }: { userId: string; message: ChatMessage }) => {
const store = useLeftMenuStore.getState(); const store = useLeftMenuStore.getState();
console.log("chat-message", userId, message);
// Update the chat store with the new message // Update the chat store with the new message
store.addMessage(userId, message); store.addMessage(userId, message);
}, },
); );
pilotSocket.on("chat-message", ({ userId, message }: { userId: string; message: ChatMessage }) => { pilotSocket.on("chat-message", ({ userId, message }: { userId: string; message: ChatMessage }) => {
const store = useLeftMenuStore.getState(); const store = useLeftMenuStore.getState();
console.log("chat-message", userId, message);
// Update the chat store with the new message // Update the chat store with the new message
store.addMessage(userId, message); store.addMessage(userId, message);
}); });

View File

@@ -3,7 +3,7 @@
"version": "0.1.0", "version": "0.1.0",
"type": "module", "type": "module",
"private": true, "private": true,
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.13.1",
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 3001", "dev": "next dev --turbopack -p 3001",
"build": "next build", "build": "next build",
@@ -14,9 +14,9 @@
"dependencies": { "dependencies": {
"@eslint/eslintrc": "^3.3.1", "@eslint/eslintrc": "^3.3.1",
"@hookform/resolvers": "^5.1.1", "@hookform/resolvers": "^5.1.1",
"@livekit/components-react": "^2.9.12", "@livekit/components-react": "^2.9.14",
"@livekit/components-styles": "^1.1.6", "@livekit/components-styles": "^1.1.6",
"@livekit/track-processors": "^0.5.7", "@livekit/track-processors": "^0.5.8",
"@next-auth/prisma-adapter": "^1.0.7", "@next-auth/prisma-adapter": "^1.0.7",
"@radix-ui/react-icons": "^1.3.2", "@radix-ui/react-icons": "^1.3.2",
"@repo/db": "workspace:*", "@repo/db": "workspace:*",
@@ -24,37 +24,37 @@
"@repo/shared-components": "workspace:*", "@repo/shared-components": "workspace:*",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.83.0",
"@turf/turf": "^7.2.0", "@turf/turf": "^7.2.0",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/leaflet": "^1.9.19", "@types/leaflet": "^1.9.20",
"@types/node": "^22.15.34", "@types/node": "^22.15.34",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"axios": "^1.10.0", "axios": "^1.10.0",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"daisyui": "^5.0.43", "daisyui": "^5.0.46",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eslint-config-next": "^15.3.4", "eslint-config-next": "^15.4.2",
"geojson": "^0.5.0", "geojson": "^0.5.0",
"i": "^0.3.7", "i": "^0.3.7",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"leaflet": "^1.9.4", "leaflet": "^1.9.4",
"leaflet.polylinemeasure": "^3.0.0", "leaflet.polylinemeasure": "^3.0.0",
"livekit-client": "^2.14.0", "livekit-client": "^2.15.3",
"livekit-server-sdk": "^2.13.1", "livekit-server-sdk": "^2.13.1",
"lucide-react": "^0.511.0", "lucide-react": "^0.525.0",
"next": "^15.3.4", "next": "^15.4.2",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"npm": "^11.4.2", "npm": "^11.4.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hook-form": "^7.59.0", "react-hook-form": "^7.60.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-leaflet": "^5.0.0", "react-leaflet": "^5.0.0",
"react-select": "^5.10.1", "react-select": "^5.10.2",
"socket.io-client": "^4.8.1", "socket.io-client": "^4.8.1",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",

Binary file not shown.

After

Width:  |  Height:  |  Size: 14 KiB

View File

@@ -14,14 +14,6 @@ const initTransporter = () => {
if (!process.env.MAIL_USER) return console.error("MAIL_USER is not defined"); if (!process.env.MAIL_USER) return console.error("MAIL_USER is not defined");
if (!process.env.MAIL_PASSWORD) return console.error("MAIL_PASSWORD is not defined"); if (!process.env.MAIL_PASSWORD) return console.error("MAIL_PASSWORD is not defined");
console.log("Initializing mail transporter...", {
host: process.env.MAIL_SERVER,
port: process.env.MAIL_PORT,
user: process.env.MAIL_USER,
password: process.env.MAIL_PASSWORD,
secure: process.env.MAIL_SECURE === "true",
});
transporter = nodemailer.createTransport({ transporter = nodemailer.createTransport({
host: process.env.MAIL_SERVER, host: process.env.MAIL_SERVER,
port: parseInt(process.env.MAIL_PORT), port: parseInt(process.env.MAIL_PORT),

View File

@@ -8,30 +8,30 @@
"start": "tsx index.ts --transpile-only", "start": "tsx index.ts --transpile-only",
"build": "tsc" "build": "tsc"
}, },
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.13.1",
"devDependencies": { "devDependencies": {
"concurrently": "^9.1.2", "concurrently": "^9.2.0",
"typescript": "latest" "typescript": "latest"
}, },
"dependencies": { "dependencies": {
"@react-email/components": "^0.0.41", "@react-email/components": "^0.3.2",
"@repo/shared-components": "workspace:*", "@repo/shared-components": "workspace:*",
"@repo/db": "workspace:*", "@repo/db": "workspace:*",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@types/cors": "^2.8.18", "@types/cors": "^2.8.19",
"@types/express": "^5.0.2", "@types/express": "^5.0.3",
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
"@types/nodemailer": "^6.4.17", "@types/nodemailer": "^6.4.17",
"@types/react": "^19.1.6", "@types/react": "^19.1.8",
"axios": "^1.9.0", "axios": "^1.10.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"cron": "^4.3.1", "cron": "^4.3.2",
"dotenv": "^16.5.0", "dotenv": "^17.2.0",
"express": "^5.1.0", "express": "^5.1.0",
"nodemailer": "^7.0.3", "nodemailer": "^7.0.5",
"nodemon": "^3.1.10", "nodemon": "^3.1.10",
"react": "^19.1.0", "react": "^19.1.0",
"tsconfig-paths": "^4.2.0", "tsconfig-paths": "^4.2.0",
"tsx": "^4.19.4" "tsx": "^4.20.3"
} }
} }

View File

@@ -34,8 +34,8 @@ export const AppointmentModal = ({
const participantTableRef = useRef<PaginatedTableRef>(null); const participantTableRef = useRef<PaginatedTableRef>(null);
return ( return (
<dialog ref={ref} className="modal "> <dialog ref={ref} className="modal">
<div className="modal-box min-w-[900px] min-h-[500px]"> <div className="modal-box min-h-[500px] min-w-[900px]">
<form method="dialog"> <form method="dialog">
{/* if there is a button in form, it will close the modal */} {/* if there is a button in form, it will close the modal */}
<button <button
@@ -55,8 +55,8 @@ export const AppointmentModal = ({
})} })}
className="flex flex-col" className="flex flex-col"
> >
<div className="flex justify-between mr-7"> <div className="mr-7 flex justify-between">
<h3 className="font-bold text-lg">Termin {appointmentForm.watch("id")}</h3> <h3 className="text-lg font-bold">Termin {appointmentForm.watch("id")}</h3>
<DateInput <DateInput
value={new Date(appointmentForm.watch("appointmentDate") || Date.now())} value={new Date(appointmentForm.watch("appointmentDate") || Date.now())}
onChange={(date) => appointmentForm.setValue("appointmentDate", date)} onChange={(date) => appointmentForm.setValue("appointmentDate", date)}
@@ -121,11 +121,6 @@ export const AppointmentModal = ({
attended: true, attended: true,
appointmentCancelled: false, appointmentCancelled: false,
}); });
console.log(
"Participant attended",
event.finisherMoodleCourseId,
!event.finisherMoodleCourseId?.length,
);
if (!event.finisherMoodleCourseId?.length) { if (!event.finisherMoodleCourseId?.length) {
toast( toast(
"Teilnehmer hat das event abgeschlossen, workflow ausgeführt", "Teilnehmer hat das event abgeschlossen, workflow ausgeführt",

View File

@@ -11,7 +11,7 @@ import {
import { Bot, Calendar, FileText, UserIcon } from "lucide-react"; import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import { useRef, useState } from "react"; import { useRef } from "react";
import "react-datepicker/dist/react-datepicker.css"; import "react-datepicker/dist/react-datepicker.css";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable"; import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
@@ -24,6 +24,7 @@ import { deleteEvent, upsertEvent } from "../action";
import { AppointmentModal } from "./AppointmentModal"; import { AppointmentModal } from "./AppointmentModal";
import { ParticipantModal } from "./ParticipantModal"; import { ParticipantModal } from "./ParticipantModal";
import { ColumnDef } from "@tanstack/react-table"; import { ColumnDef } from "@tanstack/react-table";
import toast from "react-hot-toast";
export const Form = ({ event }: { event?: Event }) => { export const Form = ({ event }: { event?: Event }) => {
const { data: session } = useSession(); const { data: session } = useSession();
@@ -66,6 +67,7 @@ export const Form = ({ event }: { event?: Event }) => {
<form <form
onSubmit={form.handleSubmit(async (values) => { onSubmit={form.handleSubmit(async (values) => {
await upsertEvent(values, event?.id); await upsertEvent(values, event?.id);
toast.success("Event erfolgreich gespeichert");
if (!event) redirect(`/admin/event`); if (!event) redirect(`/admin/event`);
})} })}
className="grid grid-cols-6 gap-3" className="grid grid-cols-6 gap-3"
@@ -75,17 +77,6 @@ export const Form = ({ event }: { event?: Event }) => {
<h2 className="card-title"> <h2 className="card-title">
<FileText className="w-5 h-5" /> Allgemeines <FileText className="w-5 h-5" /> Allgemeines
</h2> </h2>
<Input form={form} label="Name" name="name" className="input-sm" />
<MarkdownEditor form={form} name="description" />
<Input
form={form}
label="Maximale Teilnehmer (Nur für live Events)"
className="input-sm"
{...form.register("maxParticipants", {
valueAsNumber: true,
})}
/>
<Switch form={form} name="hidden" label="Versteckt" />
<Select <Select
form={form} form={form}
name="type" name="type"
@@ -95,6 +86,9 @@ export const Form = ({ event }: { event?: Event }) => {
value: value, value: value,
}))} }))}
/> />
<Input form={form} label="Name" name="name" className="input-sm" />
<MarkdownEditor form={form} name="description" />
<MarkdownEditor form={form} name="descriptionShort" />
</div> </div>
</div> </div>
<div className="card bg-base-200 shadow-xl col-span-3 max-xl:col-span-6"> <div className="card bg-base-200 shadow-xl col-span-3 max-xl:col-span-6">
@@ -144,7 +138,18 @@ export const Form = ({ event }: { event?: Event }) => {
label="Discord Rolle für eingeschriebene Teilnehmer" label="Discord Rolle für eingeschriebene Teilnehmer"
className="input-sm" className="input-sm"
/> />
<Input
form={form}
label="Maximale Teilnehmer (Nur für live Events)"
className="input-sm"
{...form.register("maxParticipants", {
valueAsNumber: true,
})}
/>
<Switch form={form} name="hasPresenceEvents" label="Hat Live Event" /> <Switch form={form} name="hasPresenceEvents" label="Hat Live Event" />
<div className="divider w-full" />
<Switch form={form} name="hidden" label="Event verstecken" />
</div> </div>
</div> </div>
{form.watch("hasPresenceEvents") ? ( {form.watch("hasPresenceEvents") ? (

View File

@@ -11,7 +11,7 @@ const page = () => {
<PaginatedTable <PaginatedTable
stickyHeaders stickyHeaders
prismaModel="heliport" prismaModel="heliport"
searchFields={["siteName", "info", "hospital"]} searchFields={["siteName", "info", "hospital", "designator"]}
columns={ columns={
[ [
{ {
@@ -45,11 +45,11 @@ const page = () => {
} }
leftOfSearch={ leftOfSearch={
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Heliports <DatabaseBackupIcon className="h-5 w-5" /> Heliports
</span> </span>
} }
rightOfSearch={ rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between"> <p className="flex items-center justify-between gap-2 text-left text-2xl font-semibold">
<Link href={"/admin/heliport/new"}> <Link href={"/admin/heliport/new"}>
<button className="btn btn-sm btn-outline btn-primary">Erstellen</button> <button className="btn btn-sm btn-outline btn-primary">Erstellen</button>
</Link> </Link>

View File

@@ -2,20 +2,26 @@
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { StationOptionalDefaultsSchema } from "@repo/db/zod"; import { StationOptionalDefaultsSchema } from "@repo/db/zod";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { BosUse, Country, Station } from "@repo/db"; import { BosUse, ConnectedAircraft, Country, Station, User } from "@repo/db";
import { FileText, LocateIcon, PlaneIcon } from "lucide-react"; import { FileText, LocateIcon, PlaneIcon, UserIcon } from "lucide-react";
import { Input } from "../../../../_components/ui/Input"; import { Input } from "../../../../_components/ui/Input";
import { useState } from "react";
import { deleteStation, upsertStation } from "../action"; import { deleteStation, upsertStation } from "../action";
import { Button } from "../../../../_components/ui/Button"; import { Button } from "../../../../_components/ui/Button";
import { redirect } from "next/navigation"; import { redirect } from "next/navigation";
import toast from "react-hot-toast"; import toast from "react-hot-toast";
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
import { ColumnDef } from "@tanstack/react-table";
import Link from "next/link";
import { deletePilotHistory } from "(app)/admin/user/action";
import { useRef } from "react";
import { cn } from "@repo/shared-components";
export const StationForm = ({ station }: { station?: Station }) => { export const StationForm = ({ station }: { station?: Station }) => {
const form = useForm({ const form = useForm({
resolver: zodResolver(StationOptionalDefaultsSchema), resolver: zodResolver(StationOptionalDefaultsSchema),
defaultValues: station, defaultValues: station,
}); });
const dispoTableRef = useRef<PaginatedTableRef>(null);
// const [deleteLoading, setDeleteLoading] = useState(false); // const [deleteLoading, setDeleteLoading] = useState(false);
return ( return (
<> <>
@@ -27,10 +33,10 @@ export const StationForm = ({ station }: { station?: Station }) => {
})} })}
className="grid grid-cols-6 gap-3" className="grid grid-cols-6 gap-3"
> >
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6"> <div className="card bg-base-200 col-span-2 shadow-xl max-xl:col-span-6">
<div className="card-body"> <div className="card-body">
<h2 className="card-title"> <h2 className="card-title">
<FileText className="w-5 h-5" /> Allgemeines <FileText className="h-5 w-5" /> Allgemeines
</h2> </h2>
<Input form={form} label="BOS Rufname" name="bosCallsign" className="input-sm" /> <Input form={form} label="BOS Rufname" name="bosCallsign" className="input-sm" />
<Input <Input
@@ -55,7 +61,7 @@ export const StationForm = ({ station }: { station?: Station }) => {
/> />
<label className="form-control w-full"> <label className="form-control w-full">
<span className="label-text text-lg flex items-center gap-2">BOS Nutzung</span> <span className="label-text flex items-center gap-2 text-lg">BOS Nutzung</span>
<select <select
className="input-sm select select-bordered select-sm" className="input-sm select select-bordered select-sm"
{...form.register("bosUse")} {...form.register("bosUse")}
@@ -69,13 +75,13 @@ export const StationForm = ({ station }: { station?: Station }) => {
</label> </label>
</div> </div>
</div> </div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6"> <div className="card bg-base-200 col-span-2 shadow-xl max-xl:col-span-6">
<div className="card-body"> <div className="card-body">
<h2 className="card-title"> <h2 className="card-title">
<LocateIcon className="w-5 h-5" /> Standort + Ausrüstung <LocateIcon className="h-5 w-5" /> Standort + Ausrüstung
</h2> </h2>
<label className="form-control w-full"> <label className="form-control w-full">
<span className="label-text text-lg flex items-center gap-2">Land</span> <span className="label-text flex items-center gap-2 text-lg">Land</span>
<select <select
className="input-sm select select-bordered select-sm" className="input-sm select select-bordered select-sm"
{...form.register("country", {})} {...form.register("country", {})}
@@ -94,21 +100,21 @@ export const StationForm = ({ station }: { station?: Station }) => {
name="locationStateShort" name="locationStateShort"
className="input-sm" className="input-sm"
/> />
<span className="label-text text-lg flex items-center gap-2">Ausgerüstet mit:</span> <span className="label-text flex items-center gap-2 text-lg">Ausgerüstet mit:</span>
<div className="form-control space-y-2"> <div className="form-control space-y-2">
<label className="label cursor-pointer flex"> <label className="label flex cursor-pointer">
<span className="flex-1 text-left">Winde</span> <span className="flex-1 text-left">Winde</span>
<input type="checkbox" className="toggle" {...form.register("hasWinch")} /> <input type="checkbox" className="toggle" {...form.register("hasWinch")} />
</label> </label>
<label className="label cursor-pointer flex"> <label className="label flex cursor-pointer">
<span className="flex-1 text-left">Nachtsicht-Gerät</span> <span className="flex-1 text-left">Nachtsicht-Gerät</span>
<input type="checkbox" className="toggle" {...form.register("hasNvg")} /> <input type="checkbox" className="toggle" {...form.register("hasNvg")} />
</label> </label>
<label className="label cursor-pointer flex"> <label className="label flex cursor-pointer">
<span className="flex-1 text-left">24-Stunden Einsatzfähig</span> <span className="flex-1 text-left">24-Stunden Einsatzfähig</span>
<input type="checkbox" className="toggle" {...form.register("is24h")} /> <input type="checkbox" className="toggle" {...form.register("is24h")} />
</label> </label>
<label className="label cursor-pointer flex"> <label className="label flex cursor-pointer">
<span className="flex-1 text-left">Bergetau</span> <span className="flex-1 text-left">Bergetau</span>
<input type="checkbox" className="toggle" {...form.register("hasRope")} /> <input type="checkbox" className="toggle" {...form.register("hasRope")} />
</label> </label>
@@ -132,16 +138,16 @@ export const StationForm = ({ station }: { station?: Station }) => {
type="number" type="number"
step="any" step="any"
/> />
<label className="label cursor-pointer flex"> <label className="label flex cursor-pointer">
<span className="text-lg flex-1 text-left">Reichweiten ausblenden</span> <span className="flex-1 text-left text-lg">Reichweiten ausblenden</span>
<input type="checkbox" className="toggle" {...form.register("hideRangeRings")} /> <input type="checkbox" className="toggle" {...form.register("hideRangeRings")} />
</label> </label>
</div> </div>
</div> </div>
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6"> <div className="card bg-base-200 col-span-2 shadow-xl max-xl:col-span-6">
<div className="card-body"> <div className="card-body">
<h2 className="card-title"> <h2 className="card-title">
<PlaneIcon className="w-5 h-5" /> Hubschrauber <PlaneIcon className="h-5 w-5" /> Hubschrauber
</h2> </h2>
<Input form={form} label="Hubschrauber Typ" name="aircraft" className="input-sm" /> <Input form={form} label="Hubschrauber Typ" name="aircraft" className="input-sm" />
<Input <Input
@@ -160,8 +166,8 @@ export const StationForm = ({ station }: { station?: Station }) => {
/> />
</div> </div>
</div> </div>
<div className="card bg-base-200 shadow-xl col-span-6"> <div className="card bg-base-200 col-span-6 shadow-xl">
<div className="card-body "> <div className="card-body">
<div className="flex w-full gap-4"> <div className="flex w-full gap-4">
<Button <Button
isLoading={form.formState.isSubmitting} isLoading={form.formState.isSubmitting}
@@ -184,6 +190,93 @@ export const StationForm = ({ station }: { station?: Station }) => {
</div> </div>
</div> </div>
</div> </div>
<div className="card bg-base-200 col-span-6 shadow-xl">
<PaginatedTable
leftOfSearch={
<div className="flex items-center gap-2 text-lg font-bold">
<UserIcon className="h-5 w-5" />
Verbundene Piloten
</div>
}
filter={{
stationId: station?.id,
}}
searchFields={["User.firstname", "User.lastname", "User.publicId"]}
prismaModel={"connectedAircraft"}
include={{ Station: true, User: true }}
columns={
[
{
accessorKey: "User.firstname",
header: "Nutzer",
cell: ({ row }) => {
return (
<Link
className="link link-hover"
href={`/admin/user/${row.original.User.id}`}
>
{row.original.User.firstname} {row.original.User.lastname} (
{row.original.User.publicId})
</Link>
);
},
},
{
accessorKey: "loginTime",
header: "Login",
cell: ({ row }) => {
return new Date(row.getValue("loginTime")).toLocaleString("de-DE");
},
},
{
accessorKey: "logoutTime",
header: "Logout",
cell: ({ row }) => {
return new Date(row.getValue("logoutTime")).toLocaleString("de-DE");
},
},
{
header: "Time Online",
cell: ({ row }) => {
if (!row.original.logoutTime) {
return <span className="text-success">Online</span>;
}
const loginTime = new Date(row.original.loginTime).getTime();
const logoutTime = new Date(row.original.logoutTime).getTime();
const timeOnline = logoutTime - loginTime;
const hours = Math.floor(timeOnline / 1000 / 60 / 60);
const minutes = Math.floor((timeOnline / 1000 / 60) % 60);
return (
<span className={cn(hours > 2 && "text-error")}>
{hours}h {minutes}min
</span>
);
},
},
{
header: "Aktionen",
cell: ({ row }) => {
return (
<div>
<button
className="btn btn-sm btn-error"
onClick={async () => {
await deletePilotHistory(row.original.id);
dispoTableRef.current?.refresh();
}}
>
löschen
</button>
</div>
);
},
},
] as ColumnDef<ConnectedAircraft & { Station: Station; User: User }>[]
}
/>
</div>
</form> </form>
</> </>
); );

View File

@@ -10,7 +10,7 @@ import {
Station, Station,
User, User,
} from "@repo/db"; } from "@repo/db";
import { useRef, useState } from "react"; import { useRef } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { import {
deleteDispoHistory, deleteDispoHistory,
@@ -84,11 +84,11 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
})} })}
> >
<h2 className="card-title"> <h2 className="card-title">
<MixerHorizontalIcon className="w-5 h-5" /> User bearbeiten <MixerHorizontalIcon className="h-5 w-5" /> User bearbeiten
</h2> </h2>
<div className="text-left"> <div className="text-left">
<label className="floating-label w-full mb-5 mt-5"> <label className="floating-label mb-5 mt-5 w-full">
<span className="text-lg flex items-center gap-2"> <span className="flex items-center gap-2 text-lg">
<PersonIcon /> Vorname <PersonIcon /> Vorname
</span> </span>
<input <input
@@ -102,8 +102,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
{form.formState.errors.firstname && ( {form.formState.errors.firstname && (
<p className="text-error">{form.formState.errors.firstname.message}</p> <p className="text-error">{form.formState.errors.firstname.message}</p>
)} )}
<label className="floating-label w-full mb-5"> <label className="floating-label mb-5 w-full">
<span className="text-lg flex items-center gap-2"> <span className="flex items-center gap-2 text-lg">
<PersonIcon /> Nachname <PersonIcon /> Nachname
</span> </span>
<input <input
@@ -120,13 +120,13 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && ( {session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<> <>
<label className="floating-label w-full"> <label className="floating-label w-full">
<span className="text-lg flex items-center gap-2"> <span className="flex items-center gap-2 text-lg">
<EnvelopeClosedIcon /> E-Mail <EnvelopeClosedIcon /> E-Mail
</span> </span>
<input <input
{...form.register("email")} {...form.register("email")}
type="text" type="text"
className="input input-bordered w-full mb-2" className="input input-bordered mb-2 w-full"
defaultValue={user?.email} defaultValue={user?.email}
placeholder="E-Mail" placeholder="E-Mail"
/> />
@@ -166,7 +166,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && ( {session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<div className="mt-2 space-y-1"> <div className="mt-2 space-y-1">
<p className="text-gray-400">Berechtigung Schnellauswahl</p> <p className="text-gray-400">Berechtigung Schnellauswahl</p>
<div className="flex gap-2 justify-evenly"> <div className="flex justify-evenly gap-2">
<button <button
type="button" type="button"
className="btn btn-sm btn-outline" className="btn btn-sm btn-outline"
@@ -251,7 +251,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
<div className="card-body flex-row flex-wrap"> <div className="card-body flex-row flex-wrap">
<div className="flex-1"> <div className="flex-1">
<h2 className="card-title"> <h2 className="card-title">
<MixerHorizontalIcon className="w-5 h-5" /> Dispo-Verbindungs Historie <MixerHorizontalIcon className="h-5 w-5" /> Dispo-Verbindungs Historie
</h2> </h2>
<PaginatedTable <PaginatedTable
ref={dispoTableRef} ref={dispoTableRef}
@@ -313,7 +313,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
</div> </div>
<div className="flex-1"> <div className="flex-1">
<h2 className="card-title"> <h2 className="card-title">
<PlaneIcon className="w-5 h-5" /> Pilot-Verbindungs Historie <PlaneIcon className="h-5 w-5" /> Pilot-Verbindungs Historie
</h2> </h2>
<PaginatedTable <PaginatedTable
ref={dispoTableRef} ref={dispoTableRef}
@@ -396,7 +396,7 @@ export const UserPenalties = ({ user }: { user: User }) => {
<div className="card-body"> <div className="card-body">
<h2 className="card-title flex justify-between"> <h2 className="card-title flex justify-between">
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<ExclamationTriangleIcon className="w-5 h-5" /> Audit-log <ExclamationTriangleIcon className="h-5 w-5" /> Audit-log
</span> </span>
<div className="flex gap-2"> <div className="flex gap-2">
<PenaltyDropdown <PenaltyDropdown
@@ -478,7 +478,7 @@ export const UserReports = ({ user }: { user: User }) => {
return ( return (
<div className="card-body"> <div className="card-body">
<h2 className="card-title"> <h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Nutzer Reports <ExclamationTriangleIcon className="h-5 w-5" /> Nutzer Reports
</h2> </h2>
<PaginatedTable <PaginatedTable
prismaModel="report" prismaModel="report"
@@ -528,10 +528,10 @@ export const AdminForm = ({
return ( return (
<div className="card-body"> <div className="card-body">
<h2 className="card-title"> <h2 className="card-title">
<LightningBoltIcon className="w-5 h-5" /> Administration <LightningBoltIcon className="h-5 w-5" /> Administration
</h2> </h2>
<div className="text-left"> <div className="text-left">
<div className="card-actions pt-6 flex flex-wrap gap-2"> <div className="card-actions flex flex-wrap gap-2 pt-6">
<Button <Button
onClick={async () => { onClick={async () => {
const { password } = await resetPassword(user.id); const { password } = await resetPassword(user.id);
@@ -547,13 +547,13 @@ export const AdminForm = ({
}, },
); );
}} }}
className="btn-sm flex-1 min-w-[250px] btn-outline btn-success" className="btn-sm btn-outline btn-success min-w-[250px] flex-1"
> >
<LockOpen1Icon /> Passwort zurücksetzen <LockOpen1Icon /> Passwort zurücksetzen
</Button> </Button>
{session?.user.permissions.includes("ADMIN_USER_ADVANCED") && ( {session?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<div <div
className="tooltip flex-1 min-w-[250px] tooltip-warning" className="tooltip tooltip-warning min-w-[250px] flex-1"
data-tip="Dies löscht den Nutzer sofort, außerdem alle Reports, Verbindungs-Verlauf und Chat-Nachrichten" data-tip="Dies löscht den Nutzer sofort, außerdem alle Reports, Verbindungs-Verlauf und Chat-Nachrichten"
> >
<Button <Button
@@ -581,14 +581,14 @@ export const AdminForm = ({
router.refresh(); router.refresh();
}} }}
role="submit" role="submit"
className="btn-sm flex-1 min-w-[250px] btn-outline btn-warning" className="btn-sm btn-outline btn-warning min-w-[250px] flex-1"
> >
<HobbyKnifeIcon /> Account entsperren <HobbyKnifeIcon /> Account entsperren
</Button> </Button>
)} )}
{discordAccount && ( {discordAccount && (
<div <div
className="tooltip flex-1 min-w-[250px]" className="tooltip min-w-[250px] flex-1"
data-tip={`Name: ${discordAccount.username}`} data-tip={`Name: ${discordAccount.username}`}
> >
<Button <Button
@@ -604,7 +604,7 @@ export const AdminForm = ({
}, },
}); });
}} }}
className="btn-sm w-full btn-outline btn-info" className="btn-sm btn-outline btn-info w-full"
> >
<DiscordLogoIcon /> Name und Berechtigungen setzen <DiscordLogoIcon /> Name und Berechtigungen setzen
</Button> </Button>
@@ -613,12 +613,12 @@ export const AdminForm = ({
</div> </div>
</div> </div>
<h2 className="card-title"> <h2 className="card-title">
<ChartBarBigIcon className="w-5 h-5" /> Aktivität <ChartBarBigIcon className="h-5 w-5" /> Aktivität
</h2> </h2>
<div className="stats flex"> <div className="stats flex">
<div className="stat"> <div className="stat">
<div className="stat-figure text-primary"> <div className="stat-figure text-primary">
<LightningBoltIcon className="w-8 h-8" /> <LightningBoltIcon className="h-8 w-8" />
</div> </div>
<div className="stat-value text-primary"> <div className="stat-value text-primary">
{dispoTime.hours}h {dispoTime.minutes}min {dispoTime.hours}h {dispoTime.minutes}min
@@ -630,7 +630,7 @@ export const AdminForm = ({
</div> </div>
<div className="stat"> <div className="stat">
<div className="stat-figure text-primary"> <div className="stat-figure text-primary">
<PlaneIcon className="w-8 h-8" /> <PlaneIcon className="h-8 w-8" />
</div> </div>
<div className="stat-value text-primary"> <div className="stat-value text-primary">
{pilotTime.hours}h {pilotTime.minutes}min {pilotTime.hours}h {pilotTime.minutes}min
@@ -642,12 +642,12 @@ export const AdminForm = ({
</div> </div>
</div> </div>
<h2 className="card-title"> <h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Reports <ExclamationTriangleIcon className="h-5 w-5" /> Reports
</h2> </h2>
<div className="stats flex"> <div className="stats flex">
<div className="stat"> <div className="stat">
<div className="stat-figure text-primary"> <div className="stat-figure text-primary">
<ExclamationTriangleIcon className="w-8 h-8" /> <ExclamationTriangleIcon className="h-8 w-8" />
</div> </div>
<div className={cn("stat-value text-primary", reports.open && "text-warning")}> <div className={cn("stat-value text-primary", reports.open && "text-warning")}>
{reports.open} {reports.open}
@@ -656,7 +656,7 @@ export const AdminForm = ({
</div> </div>
<div className="stat"> <div className="stat">
<div className="stat-figure text-primary"> <div className="stat-figure text-primary">
<Timer className="w-8 h-8" /> <Timer className="h-8 w-8" />
</div> </div>
<div className="stat-value text-primary">{reports.total60Days}</div> <div className="stat-value text-primary">{reports.total60Days}</div>
<div className="stat-title">in den letzten 60 Tagen</div> <div className="stat-title">in den letzten 60 Tagen</div>

View File

@@ -34,6 +34,23 @@ const AdminUserPage = () => {
header: "Nachname", header: "Nachname",
accessorKey: "lastname", accessorKey: "lastname",
}, },
{
header: "Berechtigungen",
cell(props) {
if (props.row.original.permissions.length === 0) {
return <span className="text-gray-700">Keine</span>;
} else if (props.row.original.permissions.includes("ADMIN_USER_ADVANCED")) {
return <span className="text-primary">Admin</span>;
}
return (
<span className="text-secondary">
{props.row.original.permissions
.filter((p) => p === "PILOT" || p === "DISPO")
.join(", ")}
</span>
);
},
},
...(session?.user.permissions.includes("ADMIN_USER_ADVANCED") ...(session?.user.permissions.includes("ADMIN_USER_ADVANCED")
? [ ? [
{ {
@@ -55,8 +72,8 @@ const AdminUserPage = () => {
] as ColumnDef<User>[] ] as ColumnDef<User>[]
} // Define the columns for the user table } // Define the columns for the user table
leftOfSearch={ leftOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2"> <p className="flex items-center gap-2 text-left text-2xl font-semibold">
<User2 className="w-5 h-5" /> Benutzer <User2 className="h-5 w-5" /> Benutzer
</p> </p>
} }
/> />

View File

@@ -38,8 +38,7 @@ export const EventCard = ({
<div className="col-span-4"> <div className="col-span-4">
<div className="text-left text-balance"> <div className="text-left text-balance">
<MDEditor.Markdown <MDEditor.Markdown
source={event.description} source={event.descriptionShort}
className="whitespace-pre-wrap"
style={{ style={{
backgroundColor: "transparent", backgroundColor: "transparent",
}} }}

View File

@@ -157,7 +157,6 @@ const ModalBtn = ({
<div className="text-left text-balance"> <div className="text-left text-balance">
<MDEditor.Markdown <MDEditor.Markdown
source={event.description} source={event.description}
className="whitespace-pre-wrap"
style={{ style={{
backgroundColor: "transparent", backgroundColor: "transparent",
}} }}

View File

@@ -13,10 +13,8 @@ export default function Page() {
const verifyCode = useCallback( const verifyCode = useCallback(
async (code: string) => { async (code: string) => {
console.log("Verifying code:", code);
if (!code) return; if (!code) return;
const res = await checkEmailCode(code); const res = await checkEmailCode(code);
console.log("Verification response:", res);
if (res.error) { if (res.error) {
console.log("Verification error:", res.error); console.log("Verification error:", res.error);
toast.error(res.error); toast.error(res.error);
@@ -34,12 +32,12 @@ export default function Page() {
}, [paramsCode, verifyCode]); }, [paramsCode, verifyCode]);
return ( return (
<div className="card bg-base-200 shadow-xl mb-4 "> <div className="card bg-base-200 mb-4 shadow-xl">
<div className="card-body"> <div className="card-body">
<p className="text-2xl font-semibold text-left flex items-center gap-2"> <p className="flex items-center gap-2 text-left text-2xl font-semibold">
<Check className="w-5 h-5" /> E-Mail Bestätigung <Check className="h-5 w-5" /> E-Mail Bestätigung
</p> </p>
<div className="flex justify-center gap-3 w-full"> <div className="flex w-full justify-center gap-3">
<input <input
className="input flex-1" className="input flex-1"
placeholder="Bestätigungscode" placeholder="Bestätigungscode"

View File

@@ -21,13 +21,8 @@ export const options: AuthOptions = {
where: { email: credentials.email }, where: { email: credentials.email },
}); });
const v1User = (oldUser as OldUser[]).find((u) => u.email === credentials.email); const v1User = (oldUser as OldUser[]).find((u) => u.email === credentials.email);
console.log("V1 User", v1User?.publicId);
if (!user && v1User) { if (!user && v1User) {
if (bcrypt.compareSync(credentials.password, v1User.password)) { if (bcrypt.compareSync(credentials.password, v1User.password)) {
console.log(
"v1 User Passwords match:",
bcrypt.compareSync(credentials.password, v1User.password),
);
const newUser = await createNewUserFromOld(v1User); const newUser = await createNewUserFromOld(v1User);
await sendVerificationLink(newUser.id); await sendVerificationLink(newUser.id);
return newUser; return newUser;

View File

@@ -49,7 +49,6 @@ export const getMoodleUserById = async (id: string) => {
}, },
}, },
); );
console.log("Moodle User", user);
const u = user[0]; const u = user[0];
return ( return (
(u as { (u as {

View File

@@ -1,14 +1,11 @@
export const generateUUID = (length: number) => { export const generateUUID = (length: number) => {
// Base62-Version (a-z, A-Z, 0-9) // Base62-Version (a-z, A-Z, 0-9)
const base62 = const base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
let string = ""; let string = "";
for (let i = 0; i < length; i++) { for (let i = 0; i < length; i++) {
string += base62.charAt(Math.floor(Math.random() * base62.length)); string += base62.charAt(Math.floor(Math.random() * base62.length));
} }
console.log(string);
return string; return string;
}; };

View File

@@ -2,7 +2,7 @@
"name": "hub", "name": "hub",
"version": "0.1.0", "version": "0.1.0",
"private": true, "private": true,
"packageManager": "pnpm@10.11.0", "packageManager": "pnpm@10.13.1",
"scripts": { "scripts": {
"dev": "next dev --turbopack -p 3000", "dev": "next dev --turbopack -p 3000",
"build": "next build", "build": "next build",
@@ -19,45 +19,45 @@
"@repo/shared-components": "workspace:*", "@repo/shared-components": "workspace:*",
"@repo/typescript-config": "workspace:*", "@repo/typescript-config": "workspace:*",
"@tailwindcss/postcss": "^4.1.11", "@tailwindcss/postcss": "^4.1.11",
"@tanstack/react-query": "^5.81.5", "@tanstack/react-query": "^5.83.0",
"@tanstack/react-table": "^8.21.3", "@tanstack/react-table": "^8.21.3",
"@types/jsonwebtoken": "^9.0.10", "@types/jsonwebtoken": "^9.0.10",
"@types/node": "^22.15.34", "@types/node": "^22.15.34",
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.6", "@types/react-dom": "^19.1.6",
"@uiw/react-md-editor": "^4.0.7", "@uiw/react-md-editor": "^4.0.8",
"axios": "^1.10.0", "axios": "^1.10.0",
"bcryptjs": "^3.0.2", "bcryptjs": "^3.0.2",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"daisyui": "^5.0.43", "daisyui": "^5.0.46",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"eslint-config-next": "^15.3.4", "eslint-config-next": "^15.4.2",
"i": "^0.3.7", "i": "^0.3.7",
"jsonwebtoken": "^9.0.2", "jsonwebtoken": "^9.0.2",
"lodash": "^4.17.21", "lodash": "^4.17.21",
"lucide-react": "^0.511.0", "lucide-react": "^0.525.0",
"next": "^15.3.4", "next": "^15.4.2",
"next-auth": "^4.24.11", "next-auth": "^4.24.11",
"next-remove-imports": "^1.0.12", "next-remove-imports": "^1.0.12",
"npm": "^11.4.2", "npm": "^11.4.2",
"postcss": "^8.5.6", "postcss": "^8.5.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-datepicker": "^8.4.0", "react-datepicker": "^8.4.0",
"react-day-picker": "^9.7.0", "react-day-picker": "^9.8.0",
"react-dom": "^19.1.0", "react-dom": "^19.1.0",
"react-error-boundary": "^6.0.0", "react-error-boundary": "^6.0.0",
"react-hook-form": "^7.59.0", "react-hook-form": "^7.60.0",
"react-hot-toast": "^2.5.2", "react-hot-toast": "^2.5.2",
"react-select": "^5.10.1", "react-select": "^5.10.2",
"tailwind-merge": "^3.3.1", "tailwind-merge": "^3.3.1",
"tailwindcss": "^4.1.11", "tailwindcss": "^4.1.11",
"zod": "^3.25.67", "zod": "^3.25.67",
"zustand": "^5.0.6" "zustand": "^5.0.6"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.30.0", "@eslint/js": "^9.31.0",
"eslint": "^9.30.0", "eslint": "^9.31.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.33.1" "typescript-eslint": "^8.37.0"
} }
} }

View File

@@ -12,20 +12,21 @@
"format": "prettier --write \"**/*.{ts,tsx,md}\"" "format": "prettier --write \"**/*.{ts,tsx,md}\""
}, },
"devDependencies": { "devDependencies": {
"prettier": "^3.5.3", "prettier": "^3.6.2",
"turbo": "^2.5.4", "prettier-plugin-tailwindcss": "^0.6.14",
"turbo": "^2.5.5",
"typescript": "^5.8.3" "typescript": "^5.8.3"
}, },
"engines": { "engines": {
"node": ">=18", "node": ">=18",
"pnpm": ">=10" "pnpm": ">=10"
}, },
"packageManager": "pnpm@10.12.1", "packageManager": "pnpm@10.13.1",
"workspaces": [ "workspaces": [
"apps/*", "apps/*",
"packages/*" "packages/*"
], ],
"dependencies": { "dependencies": {
"eslint": "^9.30.1" "eslint": "^9.31.0"
} }
} }

View File

@@ -20,11 +20,11 @@
"author": "", "author": "",
"license": "ISC", "license": "ISC",
"dependencies": { "dependencies": {
"@prisma/client": "^6.8.2", "@prisma/client": "^6.12.0",
"zod": "^3.25.46", "zod": "^3.25.46",
"zod-prisma-types": "^3.2.4" "zod-prisma-types": "^3.2.4"
}, },
"devDependencies": { "devDependencies": {
"prisma": "^6.8.2" "prisma": "^6.12.0"
} }
} }

View File

@@ -37,6 +37,7 @@ model Participant {
model Event { model Event {
id Int @id @default(autoincrement()) id Int @id @default(autoincrement())
name String name String
descriptionShort String @default("")
description String description String
type EVENT_TYPE @default(EVENT) type EVENT_TYPE @default(EVENT)
discordRoleId String? @default("") discordRoleId String? @default("")

View File

@@ -0,0 +1,2 @@
-- AlterTable
ALTER TABLE "Event" ADD COLUMN "descriptionShort" TEXT NOT NULL DEFAULT '';

View File

@@ -8,19 +8,19 @@
"./react-internal": "./react-internal.js" "./react-internal": "./react-internal.js"
}, },
"devDependencies": { "devDependencies": {
"@eslint/js": "^9.17.0", "@eslint/js": "^9.31.0",
"@next/eslint-plugin-next": "^15.3.3", "@next/eslint-plugin-next": "^15.4.2",
"eslint": "^9.15.0", "eslint": "^9.31.0",
"eslint-config-prettier": "^9.1.0", "eslint-config-prettier": "^10.1.8",
"eslint-plugin-only-warn": "^1.1.0", "eslint-plugin-only-warn": "^1.1.0",
"eslint-plugin-react": "^7.37.5", "eslint-plugin-react": "^7.37.5",
"eslint-plugin-react-hooks": "^5.2.0", "eslint-plugin-react-hooks": "^5.2.0",
"eslint-plugin-turbo": "^2.3.0", "eslint-plugin-turbo": "^2.5.5",
"globals": "^15.12.0", "globals": "^16.3.0",
"typescript": "^5.8.3", "typescript": "^5.8.3",
"typescript-eslint": "^8.15.0" "typescript-eslint": "^8.37.0"
}, },
"dependencies": { "dependencies": {
"@typescript-eslint/eslint-plugin": "^8.36.0" "@typescript-eslint/eslint-plugin": "^8.37.0"
} }
} }

View File

@@ -1,3 +1,4 @@
"use client";
import { set, isBefore, addDays } from "date-fns"; import { set, isBefore, addDays } from "date-fns";
export function getNextDateWithTime(targetHour: number, targetMinute: number): Date { export function getNextDateWithTime(targetHour: number, targetMinute: number): Date {

View File

@@ -12,11 +12,11 @@
"@types/node": "^22.15.29", "@types/node": "^22.15.29",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"date-fns": "^4.1.0", "date-fns": "^4.1.0",
"tailwind-merge": "^3.3.0" "tailwind-merge": "^3.3.1"
}, },
"devDependencies": { "devDependencies": {
"@types/react": "^19.1.8", "@types/react": "^19.1.8",
"@types/react-dom": "^19.1.5", "@types/react-dom": "^19.1.6",
"react": "^19.1.0", "react": "^19.1.0",
"react-dom": "^19.1.0" "react-dom": "^19.1.0"
} }

3622
pnpm-lock.yaml generated

File diff suppressed because it is too large Load Diff