Merge pull request #74 from VAR-Virtual-Air-Rescue/staging
release V2.0
This commit was merged in pull request #74.
This commit is contained in:
@@ -2,5 +2,6 @@
|
||||
"tabWidth": 2,
|
||||
"useTabs": true,
|
||||
"printWidth": 100,
|
||||
"singleQuote": false
|
||||
"singleQuote": false,
|
||||
"plugins": ["prettier-plugin-tailwindcss"]
|
||||
}
|
||||
|
||||
@@ -2,6 +2,39 @@ import { MissionLog, NotificationPayload, prisma } from "@repo/db";
|
||||
import { io } from "index";
|
||||
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 oldMissions = await prisma.mission.findMany({
|
||||
where: {
|
||||
@@ -15,18 +48,6 @@ const removeClosedMissions = async () => {
|
||||
|
||||
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(
|
||||
(stationId) => {
|
||||
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",
|
||||
);
|
||||
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;
|
||||
|
||||
// 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
|
||||
if (
|
||||
!(
|
||||
now.getTime() - lastAlertTime.getTime() > 1000 * 60 * 180 ||
|
||||
allStationsInMissionChangedFromStatus4to1Or8to1
|
||||
) ||
|
||||
missionHastManualReactivation
|
||||
)
|
||||
return;
|
||||
const now = new Date();
|
||||
if (now.getTime() - lastAlertTime.getTime() > 1000 * 60 * 180)
|
||||
return removeMission(mission.id, "inaktivität");
|
||||
|
||||
const log: MissionLog = {
|
||||
type: "completed-log",
|
||||
auto: true,
|
||||
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.`);
|
||||
// Case 2: All stations in mission changed from status 4 to 1/6 or from status 8 to 1/6
|
||||
if (allStationsInMissionChangedFromStatus4to1Or8to1)
|
||||
return removeMission(mission.id, "dem freimelden aller Stationen");
|
||||
});
|
||||
};
|
||||
|
||||
const removeConnectedAircrafts = async () => {
|
||||
const connectedAircrafts = await prisma.connectedAircraft.findMany({
|
||||
where: {
|
||||
|
||||
@@ -8,31 +8,30 @@
|
||||
"start": "tsx index.ts --transpile-only",
|
||||
"build": "tsc"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"@repo/db": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.0",
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"axios": "^1.9.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.3.1",
|
||||
"discord.js": "^14.19.3",
|
||||
"dotenv": "^16.5.0",
|
||||
"cron": "^4.3.2",
|
||||
"discord.js": "^14.21.0",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"node-cron": "^4.1.0",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemon": "^3.1.10",
|
||||
"prom-client": "^15.1.3",
|
||||
"react": "^19.1.0",
|
||||
"redis": "^5.1.1",
|
||||
"redis": "^5.6.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"tsx": "^4.19.4"
|
||||
"tsx": "^4.20.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -19,7 +19,6 @@ router.post("/set-standard-name", async (req, res) => {
|
||||
id: userId,
|
||||
},
|
||||
});
|
||||
console.log(`Setting standard name for user ${userId} (${user?.publicId}) to member ${memberId}`);
|
||||
if (!user) {
|
||||
res.status(404).json({ error: "User not found" });
|
||||
return;
|
||||
|
||||
@@ -8,39 +8,39 @@
|
||||
"start": "tsx index.ts --transpile-only",
|
||||
"build": "tsc"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"@repo/db": "workspace:*",
|
||||
"@repo/shared-components": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/cookie-parser": "^1.4.8",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/cookie-parser": "^1.4.9",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.0",
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@redis/json": "^5.1.1",
|
||||
"@react-email/components": "^0.3.2",
|
||||
"@redis/json": "^5.6.0",
|
||||
"@socket.io/redis-adapter": "^8.3.0",
|
||||
"@types/jsonwebtoken": "^9.0.9",
|
||||
"axios": "^1.9.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"axios": "^1.10.0",
|
||||
"cookie-parser": "^1.4.7",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.3.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"cron": "^4.3.2",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"livekit-server-sdk": "^2.13.0",
|
||||
"node-cron": "^4.1.0",
|
||||
"nodemailer": "^7.0.3",
|
||||
"livekit-server-sdk": "^2.13.1",
|
||||
"node-cron": "^4.2.1",
|
||||
"nodemailer": "^7.0.5",
|
||||
"nodemon": "^3.1.10",
|
||||
"prom-client": "^15.1.3",
|
||||
"react": "^19.1.0",
|
||||
"redis": "^5.1.1",
|
||||
"redis": "^5.6.0",
|
||||
"socket.io": "^4.8.1",
|
||||
"tsx": "^4.19.4"
|
||||
"tsx": "^4.20.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -261,9 +261,7 @@ router.post("/:id/hpg-validation-result", async (req, res) => {
|
||||
},
|
||||
} as NotificationPayload);
|
||||
|
||||
console.log("Got positiv validation Result", result.alertWhenValid);
|
||||
if (result.alertWhenValid) {
|
||||
console.log(req.user);
|
||||
sendAlert(Number(missionId), {}, "HPG");
|
||||
}
|
||||
} else {
|
||||
|
||||
@@ -45,15 +45,10 @@ export const handleConnectDispatch =
|
||||
});
|
||||
}
|
||||
|
||||
const [logoffHours, logoffMinutes] = logoffTime.split(":").map(Number);
|
||||
|
||||
const connectedDispatcherEntry = await prisma.connectedDispatcher.create({
|
||||
data: {
|
||||
publicUser: getPublicUser(user) as any,
|
||||
esimatedLogoutTime:
|
||||
logoffHours !== undefined && logoffMinutes !== undefined
|
||||
? getNextDateWithTime(logoffHours, logoffMinutes)
|
||||
: null,
|
||||
esimatedLogoutTime: logoffTime.length > 0 ? logoffTime : null,
|
||||
lastHeartbeat: new Date().toISOString(),
|
||||
userId: user.id,
|
||||
zone: selectedZone,
|
||||
|
||||
@@ -73,15 +73,11 @@ export const handleConnectPilot =
|
||||
}
|
||||
|
||||
const randomPos = debug ? getRandomGermanPosition() : undefined;
|
||||
const [logoffHours, logoffMinutes] = logoffTime.split(":").map(Number);
|
||||
|
||||
const connectedAircraftEntry = await prisma.connectedAircraft.create({
|
||||
data: {
|
||||
publicUser: getPublicUser(user) as any,
|
||||
esimatedLogoutTime:
|
||||
logoffHours !== undefined && logoffMinutes !== undefined
|
||||
? getNextDateWithTime(logoffHours, logoffMinutes)
|
||||
: null,
|
||||
esimatedLogoutTime: logoffTime.length > 0 ? logoffTime : null,
|
||||
userId: userId,
|
||||
stationId: parseInt(stationId),
|
||||
lastHeartbeat: debug ? nowPlus2h.toISOString() : undefined,
|
||||
|
||||
@@ -8,7 +8,6 @@ export const handleSendMessage =
|
||||
{ userId, message }: { userId: string; message: string },
|
||||
cb: (err: { error?: string }) => void,
|
||||
) => {
|
||||
console.log("send-message", userId, message);
|
||||
const senderId = socket.data.user.id;
|
||||
|
||||
const senderUser = await prisma.user.findUnique({
|
||||
|
||||
@@ -107,7 +107,6 @@ export function StationsSelect({
|
||||
menuPlacement={menuPlacement}
|
||||
isMulti={isMulti}
|
||||
onChange={(v) => {
|
||||
console.log("Selected values:", v);
|
||||
setValue(v);
|
||||
if (!isMulti) {
|
||||
const singleValue = v as string;
|
||||
|
||||
@@ -31,9 +31,9 @@ export const ConnectionBtn = () => {
|
||||
|
||||
if (!uid) return null;
|
||||
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 && (
|
||||
<span className="mx-2 text-error">{connection.message}</span>
|
||||
<span className="text-error mx-2">{connection.message}</span>
|
||||
)}
|
||||
|
||||
{connection.status == "connected" ? (
|
||||
@@ -63,11 +63,11 @@ export const ConnectionBtn = () => {
|
||||
<dialog ref={modalRef} className="modal">
|
||||
<div className="modal-box flex flex-col items-center justify-center">
|
||||
{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"><{connection.selectedZone}></span>
|
||||
</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">
|
||||
<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>
|
||||
)}
|
||||
</fieldset>
|
||||
<div className="modal-action flex justify-between w-full">
|
||||
<form method="dialog" className="w-full flex justify-between">
|
||||
<div className="modal-action flex w-full justify-between">
|
||||
<form method="dialog" className="flex w-full justify-between">
|
||||
<button className="btn btn-soft">Zurück</button>
|
||||
{connection.status == "connected" ? (
|
||||
<>
|
||||
@@ -130,7 +130,15 @@ export const ConnectionBtn = () => {
|
||||
type="submit"
|
||||
onSubmit={() => false}
|
||||
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"
|
||||
>
|
||||
|
||||
@@ -195,7 +195,7 @@ export const MissionForm = () => {
|
||||
<form className="space-y-4">
|
||||
{/* Koorinaten Section */}
|
||||
<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">
|
||||
<input
|
||||
type="text"
|
||||
@@ -219,12 +219,12 @@ export const MissionForm = () => {
|
||||
|
||||
{/* Adresse Section */}
|
||||
<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
|
||||
type="text"
|
||||
{...form.register("addressStreet")}
|
||||
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">
|
||||
<input
|
||||
@@ -244,17 +244,16 @@ export const MissionForm = () => {
|
||||
type="text"
|
||||
{...form.register("addressAdditionalInfo")}
|
||||
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>
|
||||
{/* Rettungsmittel Section */}
|
||||
<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
|
||||
isMulti
|
||||
selectedStations={form.watch("missionStationIds")}
|
||||
onChange={(v) => {
|
||||
console.log("Selected stations:", v);
|
||||
form.setValue("missionStationIds", v.selectedStationIds);
|
||||
form.setValue("hpgAmbulanceState", v.hpgAmbulanceState);
|
||||
form.setValue("hpgFireEngineState", v.hpgFireEngineState);
|
||||
@@ -270,10 +269,10 @@ export const MissionForm = () => {
|
||||
|
||||
{/* Einsatzdaten Section */}
|
||||
<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
|
||||
{...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) => {
|
||||
form.setValue("type", e.target.value as missionType);
|
||||
if (e.target.value === "primary") {
|
||||
@@ -295,7 +294,7 @@ export const MissionForm = () => {
|
||||
<>
|
||||
<select
|
||||
{...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) => {
|
||||
form.setValue("missionKeywordCategory", e.target.value as string);
|
||||
form.setValue("missionKeywordName", null as any);
|
||||
@@ -316,13 +315,13 @@ export const MissionForm = () => {
|
||||
))}
|
||||
</select>
|
||||
{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
|
||||
{...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) => {
|
||||
const keyword = keywords?.find((k) => k.abreviation === e.target.value);
|
||||
form.setValue("missionKeywordName", keyword?.name || (null as any));
|
||||
@@ -344,7 +343,7 @@ export const MissionForm = () => {
|
||||
))}
|
||||
</select>
|
||||
{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">
|
||||
<select
|
||||
@@ -364,7 +363,7 @@ export const MissionForm = () => {
|
||||
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"}
|
||||
>
|
||||
<option disabled value="please_select">
|
||||
@@ -383,14 +382,14 @@ export const MissionForm = () => {
|
||||
})}
|
||||
</select>
|
||||
{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>
|
||||
|
||||
<textarea
|
||||
{...form.register("missionAdditionalInfo")}
|
||||
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" && (
|
||||
<input
|
||||
@@ -402,7 +401,7 @@ export const MissionForm = () => {
|
||||
)}
|
||||
</div>
|
||||
<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
|
||||
{...form.register("missionPatientInfo")}
|
||||
placeholder="Patienteninformationen"
|
||||
|
||||
@@ -7,6 +7,7 @@ import dynamic from "next/dynamic";
|
||||
import { Chat } from "../../_components/left/Chat";
|
||||
import { Report } from "../../_components/left/Report";
|
||||
import { SituationBoard } from "_components/left/SituationBoard";
|
||||
import { BugReport } from "_components/left/BugReport";
|
||||
|
||||
const Map = dynamic(() => import("../../_components/map/Map"), { ssr: false });
|
||||
|
||||
@@ -14,16 +15,15 @@ const DispatchPage = () => {
|
||||
const { isOpen } = usePannelStore();
|
||||
/* return null; */
|
||||
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 /> */}
|
||||
<div className="flex flex-1 relative">
|
||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 pl-4 z-999999">
|
||||
<div className="relative flex flex-1">
|
||||
<div className="z-999999 absolute left-0 top-1/2 flex -translate-y-1/2 transform flex-col space-y-2 pl-4">
|
||||
<Chat />
|
||||
<div className="mt-2">
|
||||
<Report />
|
||||
<BugReport />
|
||||
</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">
|
||||
<SituationBoard />
|
||||
</div>
|
||||
@@ -32,7 +32,7 @@ const DispatchPage = () => {
|
||||
</div>
|
||||
<div
|
||||
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",
|
||||
)}
|
||||
>
|
||||
|
||||
@@ -27,7 +27,6 @@ export default async function RootLayout({
|
||||
});
|
||||
|
||||
if (!session) {
|
||||
console.log(session);
|
||||
return redirect("/logout");
|
||||
}
|
||||
|
||||
|
||||
@@ -31,7 +31,6 @@ export const useSounds = () => {
|
||||
const timeouts: NodeJS.Timeout[] = [];
|
||||
|
||||
if (page === "new-mission" && newMissionSound.current) {
|
||||
console.log("new-mission", mission);
|
||||
newMissionSound.current.currentTime = 0;
|
||||
newMissionSound.current.play();
|
||||
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 |
@@ -63,9 +63,9 @@ export const ConnectionBtn = () => {
|
||||
const uid = session.data?.user?.id;
|
||||
if (!uid) return null;
|
||||
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 && (
|
||||
<span className="mx-2 text-error">{connection.message}</span>
|
||||
<span className="text-error mx-2">{connection.message}</span>
|
||||
)}
|
||||
|
||||
{connection.status == "connected" ? (
|
||||
@@ -95,12 +95,12 @@ export const ConnectionBtn = () => {
|
||||
<dialog ref={modalRef} className="modal">
|
||||
<div className="modal-box flex flex-col items-center justify-center">
|
||||
{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"><{connection.selectedStation?.bosCallsign}></span>
|
||||
</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" && (
|
||||
<div className="w-full">
|
||||
@@ -135,7 +135,7 @@ export const ConnectionBtn = () => {
|
||||
/>
|
||||
</div>
|
||||
)}
|
||||
<fieldset className="fieldset w-full mt-2">
|
||||
<fieldset className="fieldset mt-2 w-full">
|
||||
<label className="floating-label w-full text-base">
|
||||
<span>Logoff Zeit (LCL)</span>
|
||||
<input
|
||||
@@ -171,8 +171,8 @@ export const ConnectionBtn = () => {
|
||||
</label>
|
||||
</fieldset>
|
||||
)}
|
||||
<div className="modal-action flex justify-between w-full">
|
||||
<form method="dialog" className="w-full flex justify-between">
|
||||
<div className="modal-action flex w-full justify-between">
|
||||
<form method="dialog" className="flex w-full justify-between">
|
||||
<button className="btn btn-soft">Zurück</button>
|
||||
{connection.status == "connected" ? (
|
||||
<>
|
||||
@@ -183,7 +183,6 @@ export const ConnectionBtn = () => {
|
||||
const [logoffHours, logoffMinutes] =
|
||||
form.logoffTime?.split(":").map(Number) || [];
|
||||
|
||||
console.log(logoffHours, logoffMinutes, form.logoffTime);
|
||||
await aircraftMutation.mutateAsync({
|
||||
sessionId: connection.connectedAircraft.id,
|
||||
change: {
|
||||
@@ -220,10 +219,15 @@ export const ConnectionBtn = () => {
|
||||
station.id === parseInt(form.selectedStationId?.toString() || ""),
|
||||
);
|
||||
if (selectedStation) {
|
||||
const [logoffHours, logoffMinutes] =
|
||||
form.logoffTime?.split(":").map(Number) || [];
|
||||
|
||||
await connection.connect(
|
||||
uid,
|
||||
form.selectedStationId?.toString() || "",
|
||||
form.logoffTime || "",
|
||||
form.logoffTime && logoffHours !== undefined && logoffMinutes !== undefined
|
||||
? getNextDateWithTime(logoffHours, logoffMinutes).toISOString()
|
||||
: "",
|
||||
selectedStation,
|
||||
session.data!.user,
|
||||
form.debugPosition,
|
||||
|
||||
@@ -12,6 +12,7 @@ import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { checkSimulatorConnected } from "@repo/shared-components";
|
||||
import { SimConnectionAlert } from "(app)/pilot/_components/SimConnectionAlert";
|
||||
import { SettingsBoard } from "_components/left/SettingsBoard";
|
||||
import { BugReport } from "_components/left/BugReport";
|
||||
|
||||
const Map = dynamic(() => import("_components/map/Map"), {
|
||||
ssr: false,
|
||||
@@ -29,24 +30,23 @@ const PilotPage = () => {
|
||||
const ownAircraft = aircrafts?.find((aircraft) => aircraft.id === connectedAircraft?.id);
|
||||
const simulatorConnected = ownAircraft ? checkSimulatorConnected(ownAircraft) : false;
|
||||
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 /> */}
|
||||
<div className="flex flex-1 relative w-full h-full">
|
||||
<div className="absolute left-0 top-1/2 transform -translate-y-1/2 pl-4 z-999999">
|
||||
<div className="relative flex h-full w-full flex-1">
|
||||
<div className="z-999999 absolute left-0 top-1/2 flex -translate-y-1/2 transform flex-col space-y-2 pl-4">
|
||||
<Chat />
|
||||
<div className="mt-2">
|
||||
<Report />
|
||||
<BugReport />
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-2/3 h-full">
|
||||
<div className="relative flex flex-1 h-full">
|
||||
<div className="absolute left-0 top-19/20 transform -translate-y-1/2 pl-4 z-999999">
|
||||
<div className="flex h-full w-2/3">
|
||||
<div className="relative flex h-full flex-1">
|
||||
<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">
|
||||
<SettingsBoard />
|
||||
</div>
|
||||
</div>
|
||||
<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" && (
|
||||
<SimConnectionAlert lastUpdated={ownAircraft?.lastHeartbeat} />
|
||||
)}
|
||||
@@ -54,19 +54,19 @@ const PilotPage = () => {
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex w-1/3 h-full">
|
||||
<div className="flex flex-col w-full h-full p-4 bg-base-300">
|
||||
<div className="flex h-full w-1/3">
|
||||
<div className="bg-base-300 flex h-full w-full flex-col p-4">
|
||||
<h2 className="card-title mb-2">MRT & DME</h2>
|
||||
<div className="card bg-base-200 shadow-xl mb-4">
|
||||
<div className="card-body w-full h-full flex items-center justify-center">
|
||||
<div className=" max-w-150">
|
||||
<div className="card bg-base-200 mb-4 shadow-xl">
|
||||
<div className="card-body flex h-full w-full items-center justify-center">
|
||||
<div className="max-w-150">
|
||||
<Mrt />
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card bg-base-200 shadow-xl h-1/2 flex">
|
||||
<div className="card-body w-full h-full p-4 mb-0 flex items-center justify-center">
|
||||
<div className=" max-w-140">
|
||||
<div className="card bg-base-200 flex h-1/2 shadow-xl">
|
||||
<div className="card-body mb-0 flex h-full w-full items-center justify-center p-4">
|
||||
<div className="max-w-140">
|
||||
<Dme />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
16
apps/dispatch/app/_components/left/BugReport.tsx
Normal file
16
apps/dispatch/app/_components/left/BugReport.tsx
Normal 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>
|
||||
);
|
||||
};
|
||||
@@ -9,6 +9,7 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { getConnectedDispatcherAPI } from "_querys/dispatcher";
|
||||
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
|
||||
export const Chat = () => {
|
||||
const {
|
||||
@@ -28,6 +29,7 @@ export const Chat = () => {
|
||||
const [addTabValue, setAddTabValue] = useState<string>("default");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
const dispatcherConnected = useDispatchConnectionStore((state) => state.status === "connected");
|
||||
const pilotConnected = usePilotConnectionStore((state) => state.status === "connected");
|
||||
|
||||
const { data: dispatcher } = useQuery({
|
||||
queryKey: ["dispatcher"],
|
||||
@@ -51,6 +53,14 @@ export const Chat = () => {
|
||||
(a) => a.userId !== session.data?.user.id && dispatcherConnected,
|
||||
);
|
||||
|
||||
const btnActive = pilotConnected || dispatcherConnected;
|
||||
|
||||
useEffect(() => {
|
||||
if (!btnActive) {
|
||||
setChatOpen(false);
|
||||
}
|
||||
}, [btnActive, setChatOpen]);
|
||||
|
||||
return (
|
||||
<div className={cn("dropdown dropdown-right dropdown-center", chatOpen && "dropdown-open")}>
|
||||
<div className="indicator">
|
||||
@@ -58,8 +68,12 @@ export const Chat = () => {
|
||||
<span className="indicator-item status status-info animate-ping"></span>
|
||||
)}
|
||||
<button
|
||||
className="btn btn-soft btn-sm btn-primary"
|
||||
className={cn(
|
||||
"btn btn-soft btn-sm cursor-default",
|
||||
btnActive && "btn-primary cursor-pointer",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!btnActive) return;
|
||||
setReportTabOpen(false);
|
||||
setChatOpen(!chatOpen);
|
||||
if (selectedChat) {
|
||||
|
||||
@@ -10,6 +10,8 @@ import { useQuery } from "@tanstack/react-query";
|
||||
import { getConnectedDispatcherAPI } from "_querys/dispatcher";
|
||||
import { sendReportAPI } from "_querys/report";
|
||||
import { getConnectedAircraftsAPI } from "_querys/aircrafts";
|
||||
import { useDispatchConnectionStore } from "_store/dispatch/connectionStore";
|
||||
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
|
||||
|
||||
export const Report = () => {
|
||||
const { setChatOpen, setReportTabOpen, reportTabOpen, setOwnId } = useLeftMenuStore();
|
||||
@@ -18,6 +20,9 @@ export const Report = () => {
|
||||
const [selectedPlayer, setSelectedPlayer] = useState<string>("default");
|
||||
const [message, setMessage] = useState<string>("");
|
||||
|
||||
const dispatcherConnected = useDispatchConnectionStore((state) => state.status === "connected");
|
||||
const pilotConnected = usePilotConnectionStore((state) => state.status === "connected");
|
||||
|
||||
useEffect(() => {
|
||||
if (!session.data?.user.id) return;
|
||||
setOwnId(session.data.user.id);
|
||||
@@ -36,6 +41,13 @@ export const Report = () => {
|
||||
|
||||
const filteredDispatcher = dispatcher?.filter((d) => d.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 (
|
||||
<div
|
||||
@@ -43,8 +55,12 @@ export const Report = () => {
|
||||
>
|
||||
<div className="indicator">
|
||||
<button
|
||||
className="btn btn-soft btn-sm btn-error"
|
||||
className={cn(
|
||||
"btn btn-soft btn-sm cursor-default",
|
||||
btnActive && "cursor-pointer btn-error",
|
||||
)}
|
||||
onClick={() => {
|
||||
if (!btnActive) return;
|
||||
setChatOpen(false);
|
||||
setReportTabOpen(!reportTabOpen);
|
||||
}}
|
||||
|
||||
@@ -61,8 +61,8 @@ const AircraftPopupContent = ({
|
||||
return mission ? (
|
||||
<MissionTab mission={mission} />
|
||||
) : (
|
||||
<div className="flex flex-col items-center justify-center min-h-full">
|
||||
<span className="text-gray-500 my-10 font-semibold">Kein aktiver Einsatz</span>
|
||||
<div className="flex min-h-full flex-col items-center justify-center">
|
||||
<span className="my-10 font-semibold text-gray-500">Kein aktiver Einsatz</span>
|
||||
</div>
|
||||
);
|
||||
case "chat":
|
||||
@@ -77,7 +77,7 @@ const AircraftPopupContent = ({
|
||||
return (
|
||||
<>
|
||||
<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={() => {
|
||||
setOpenAircraftMarker({
|
||||
open: [],
|
||||
@@ -90,7 +90,7 @@ const AircraftPopupContent = ({
|
||||
|
||||
<div
|
||||
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("top") ? "-top-[2px]" : "-bottom-[2px]",
|
||||
)}
|
||||
@@ -111,13 +111,13 @@ const AircraftPopupContent = ({
|
||||
/>
|
||||
<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={{
|
||||
backgroundColor: `${FMS_STATUS_TEXT_COLORS[aircraft.fmsStatus]}`,
|
||||
}}
|
||||
>
|
||||
<div
|
||||
className="px-3 flex justify-center items-center cursor-pointer"
|
||||
className="flex cursor-pointer items-center justify-center px-3"
|
||||
style={{
|
||||
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
|
||||
borderBottom:
|
||||
@@ -130,7 +130,7 @@ const AircraftPopupContent = ({
|
||||
<House className="text-sm" />
|
||||
</div>
|
||||
<div
|
||||
className="px-4 flex justify-center items-center cursor-pointer"
|
||||
className="flex cursor-pointer items-center justify-center px-4"
|
||||
style={{
|
||||
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
|
||||
}}
|
||||
@@ -145,7 +145,7 @@ const AircraftPopupContent = ({
|
||||
<ChevronsRightLeft className="text-sm" />
|
||||
</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={{
|
||||
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
|
||||
borderBottom:
|
||||
@@ -159,7 +159,7 @@ const AircraftPopupContent = ({
|
||||
{aircraft.fmsStatus}
|
||||
</div>
|
||||
<div
|
||||
className="cursor-pointer px-2 min-w-[130px]"
|
||||
className="min-w-[130px] cursor-pointer px-2"
|
||||
style={{
|
||||
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
|
||||
borderBottom:
|
||||
@@ -169,8 +169,8 @@ const AircraftPopupContent = ({
|
||||
}}
|
||||
onClick={() => handleTabChange("aircraft")}
|
||||
>
|
||||
<span className="text-white text-base font-medium truncate">
|
||||
{aircraft.Station.bosCallsign.length > 16
|
||||
<span className="truncate text-base font-medium text-white">
|
||||
{aircraft.Station.bosCallsign.length > 15
|
||||
? aircraft.Station.bosCallsignShort
|
||||
: aircraft.Station.bosCallsign}
|
||||
</span>
|
||||
@@ -193,14 +193,14 @@ const AircraftPopupContent = ({
|
||||
}}
|
||||
onClick={() => handleTabChange("mission")}
|
||||
>
|
||||
<span className="text-white text-base font-medium">Einsatz</span>
|
||||
<span className="text-base font-medium text-white">Einsatz</span>
|
||||
<br />
|
||||
<span className="text-white text-sm font-medium">
|
||||
<span className="text-sm font-medium text-white">
|
||||
{mission?.publicId || "kein Einsatz"}
|
||||
</span>
|
||||
</div>
|
||||
<div
|
||||
className="px-4 flex justify-center items-center cursor-pointer"
|
||||
className="flex cursor-pointer items-center justify-center px-4"
|
||||
style={{
|
||||
backgroundColor: `${FMS_STATUS_COLORS[aircraft.fmsStatus]}`,
|
||||
borderBottom:
|
||||
|
||||
@@ -53,7 +53,7 @@ export const ContextMenu = () => {
|
||||
|
||||
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 res = await fetch(
|
||||
@@ -108,7 +108,7 @@ export const ContextMenu = () => {
|
||||
{/* Top 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"
|
||||
data-tip={einsatzBtnText}
|
||||
data-tip={missionBtnText}
|
||||
style={{ transform: "translateX(-50%)" }}
|
||||
onClick={async () => {
|
||||
const { parsed } = await getOsmAddress(contextMenu.lat, contextMenu.lng);
|
||||
|
||||
@@ -12,6 +12,7 @@ import { MarkerCluster } from "_components/map/_components/MarkerCluster";
|
||||
import { useEffect, useRef } from "react";
|
||||
import { Map as TMap } from "leaflet";
|
||||
import { DistanceLayer } from "_components/map/Measurement";
|
||||
import { MapAdditionals } from "_components/map/MapAdditionals";
|
||||
|
||||
const Map = () => {
|
||||
const ref = useRef<TMap | null>(null);
|
||||
@@ -48,6 +49,7 @@ const Map = () => {
|
||||
<MissionLayer />
|
||||
<AircraftLayer />
|
||||
<DistanceLayer />
|
||||
<MapAdditionals />
|
||||
</MapContainer>
|
||||
);
|
||||
};
|
||||
|
||||
76
apps/dispatch/app/_components/map/MapAdditionals.tsx
Normal file
76
apps/dispatch/app/_components/map/MapAdditionals.tsx
Normal 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: [],
|
||||
}),
|
||||
}}
|
||||
/>
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
@@ -338,22 +338,15 @@ const MissionMarker = ({ mission }: { mission: Mission }) => {
|
||||
return [
|
||||
editingMissionId === mission.id && missionFormValues?.addressLat
|
||||
? missionFormValues.addressLat
|
||||
: mission.hpgValidationState !== "POSITION_AMANDED" && mission.hpgLocationLat
|
||||
? mission.hpgLocationLat
|
||||
: mission.addressLat,
|
||||
editingMissionId === mission.id && missionFormValues?.addressLng
|
||||
? missionFormValues.addressLng
|
||||
: mission.hpgValidationState !== "POSITION_AMANDED" && mission.hpgLocationLng
|
||||
? mission.hpgLocationLng
|
||||
: mission.addressLng,
|
||||
];
|
||||
}, [
|
||||
editingMissionId,
|
||||
mission.addressLat,
|
||||
mission.addressLng,
|
||||
mission.hpgLocationLat,
|
||||
mission.hpgLocationLng,
|
||||
mission.hpgValidationState,
|
||||
mission.id,
|
||||
missionFormValues?.addressLat,
|
||||
missionFormValues?.addressLng,
|
||||
|
||||
@@ -64,9 +64,9 @@ const FMSStatusHistory = ({
|
||||
return (
|
||||
<div className="p-4">
|
||||
<ul className="text-base-content font-semibold">
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<p className="flex items-center gap-2 flex-1">
|
||||
<PersonIcon className="w-5 h-5" /> {aircraftUser.fullName} ({aircraftUser.publicId}){" "}
|
||||
<li className="mb-1 flex items-center gap-2">
|
||||
<p className="flex flex-1 items-center gap-2">
|
||||
<PersonIcon className="h-5 w-5" /> {aircraftUser.fullName} ({aircraftUser.publicId}){" "}
|
||||
{(() => {
|
||||
const badges = aircraftUser.badges
|
||||
.filter((b) => b.startsWith("P") && b.length == 2)
|
||||
@@ -96,12 +96,12 @@ const FMSStatusHistory = ({
|
||||
</p>
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<ul className="space-y-2">
|
||||
{log.map((entry, index) => (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
className="text-base font-bold"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
|
||||
}}
|
||||
@@ -145,8 +145,8 @@ const FMSStatusSelector = ({
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-2 mt-2 p-4 text-base-content">
|
||||
<div className="flex gap-2 justify-center items-center h-full">
|
||||
<div className="text-base-content mt-2 flex flex-col gap-2 p-4">
|
||||
<div className="flex h-full items-center justify-center gap-2">
|
||||
{Array.from({ length: 9 }, (_, i) => (i + 1).toString())
|
||||
.filter((status) => status !== "5") // Exclude status 5
|
||||
.map((status) => (
|
||||
@@ -154,7 +154,7 @@ const FMSStatusSelector = ({
|
||||
disabled={!dispatcherConnected}
|
||||
key={status}
|
||||
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",
|
||||
)}
|
||||
style={{
|
||||
@@ -187,13 +187,13 @@ const FMSStatusSelector = ({
|
||||
</button>
|
||||
))}
|
||||
</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) => (
|
||||
<button
|
||||
disabled={!dispatcherConnected}
|
||||
key={status}
|
||||
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",
|
||||
)}
|
||||
style={{
|
||||
@@ -253,17 +253,17 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
|
||||
}, [aircraft.posLng, aircraft.posLat, station.bosRadioArea]);
|
||||
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="text-base-content p-4">
|
||||
<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"}
|
||||
</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}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2 mb-2">
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<div className="mb-2 mt-2 flex items-center justify-between pr-2 text-sm font-semibold">
|
||||
<span className="flex items-center gap-2">
|
||||
<Clock size={16} /> {station.is24h ? "24h Betrieb" : "Tagbetrieb"}
|
||||
</span>
|
||||
@@ -277,8 +277,8 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
|
||||
<TextSearch size={16} /> {station.aircraftRegistration}
|
||||
</span>
|
||||
</div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="flex items-center text-sm font-semibold justify-between pr-2 mt-2">
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<div className="mt-2 flex items-center justify-between pr-2 text-sm font-semibold">
|
||||
<span className="flex items-center gap-2">
|
||||
<CompassIcon size={16} /> HDG: {aircraft.posHeading}°
|
||||
</span>
|
||||
@@ -289,7 +289,7 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
|
||||
<CircleGaugeIcon size={16} /> ALT: {aircraft.posAlt} ft
|
||||
</span>
|
||||
</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">
|
||||
<Lollipop size={16} />{" "}
|
||||
<span className={cn(aircraft.posH145active && "text-green-500")}>
|
||||
@@ -303,22 +303,22 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
|
||||
|
||||
const MissionTab = ({ mission }: { mission: Mission }) => {
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="text-base-content p-4">
|
||||
<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}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<li className="mb-1 flex items-center gap-2">
|
||||
<ListCollapse size={16} />
|
||||
{mission.missionKeywordName}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mt-3">
|
||||
<li className="mt-3 flex items-center gap-2">
|
||||
<Hash size={16} />
|
||||
__{new Date().toISOString().slice(0, 10).replace(/-/g, "")}
|
||||
{mission.id}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<div className="text-sm font-semibold">
|
||||
<p className="flex items-center gap-2">
|
||||
<MapPin size={16} /> {mission.addressLat} {mission.addressLng}
|
||||
@@ -416,7 +416,7 @@ const SDSTab = ({
|
||||
<div className="flex items-center gap-2">
|
||||
{!isChatOpen ? (
|
||||
<button
|
||||
className="text-base-content text-base cursor-pointer"
|
||||
className="text-base-content cursor-pointer text-base"
|
||||
onClick={() => setIsChatOpen(true)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -424,7 +424,7 @@ const SDSTab = ({
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<input
|
||||
autoFocus
|
||||
type="text"
|
||||
@@ -463,7 +463,7 @@ const SDSTab = ({
|
||||
<div className="divider m-0" />
|
||||
</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) => {
|
||||
const sdsEntry = entry as MissionSdsLog;
|
||||
return (
|
||||
@@ -475,7 +475,7 @@ const SDSTab = ({
|
||||
})}
|
||||
</span>
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
className="text-base font-bold"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[6],
|
||||
}}
|
||||
@@ -488,7 +488,7 @@ const SDSTab = ({
|
||||
);
|
||||
})}
|
||||
{!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
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -93,8 +93,8 @@ const Einsatzdetails = ({
|
||||
const { setMissionFormValues, setOpen, setEditingMission } = usePannelStore((state) => state);
|
||||
const [ignoreHpg, setIgnoreHpg] = useState(false);
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="flex items-center justify-between mb-3">
|
||||
<div className="text-base-content p-4">
|
||||
<div className="mb-3 flex items-center justify-between">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<Flag /> Einsatzdetails
|
||||
</h2>
|
||||
@@ -126,7 +126,7 @@ const Einsatzdetails = ({
|
||||
</button>
|
||||
</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"
|
||||
>
|
||||
<button
|
||||
@@ -161,19 +161,19 @@ const Einsatzdetails = ({
|
||||
)}
|
||||
</div>
|
||||
<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}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mb-1">
|
||||
<li className="mb-1 flex items-center gap-2">
|
||||
<ListCollapse size={16} />
|
||||
{mission.missionKeywordName}
|
||||
</li>
|
||||
<li className="flex items-center gap-2 mt-3">
|
||||
<li className="mt-3 flex items-center gap-2">
|
||||
<Hash size={16} />
|
||||
{mission.publicId}
|
||||
</li>
|
||||
</ul>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<div className="text-sm font-semibold">
|
||||
<p className="flex items-center gap-2">
|
||||
<MapPin size={16} /> {mission.addressLat} {mission.addressLng}
|
||||
@@ -192,7 +192,7 @@ const Einsatzdetails = ({
|
||||
</div>
|
||||
{mission.type == "sekundär" && (
|
||||
<>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<div className="text-sm font-semibold">
|
||||
<p className="flex items-center gap-2">
|
||||
<Route size={16} /> {mission.addressMissionDestination}
|
||||
@@ -202,11 +202,11 @@ const Einsatzdetails = ({
|
||||
)}
|
||||
{mission.state === "draft" && (
|
||||
<div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="divider mb-0 mt-0" />
|
||||
|
||||
{hpgNeedsAttention && mission.hpgValidationState !== "POSITION_AMANDED" && (
|
||||
<div className="form-control mb-2 flex justify-between items-center">
|
||||
<label className="flex items-center gap-2 cursor-pointer">
|
||||
<div className="form-control mb-2 flex items-center justify-between">
|
||||
<label className="flex cursor-pointer items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
className="checkbox checkbox-sm checkbox-primary"
|
||||
@@ -214,7 +214,7 @@ const Einsatzdetails = ({
|
||||
onChange={(e) => setIgnoreHpg(e.target.checked)}
|
||||
/>
|
||||
<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"
|
||||
>
|
||||
HPG-Fehler ignorieren
|
||||
@@ -235,7 +235,7 @@ const Einsatzdetails = ({
|
||||
</div>
|
||||
)}
|
||||
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
{(!hpgNeedsAttention || ignoreHpg) &&
|
||||
mission.hpgValidationState !== HpgValidationState.POSITION_AMANDED && (
|
||||
<button
|
||||
@@ -354,13 +354,13 @@ const Einsatzdetails = ({
|
||||
|
||||
const Patientdetails = ({ mission }: { mission: Mission }) => {
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold mb-3">
|
||||
<div className="text-base-content p-4">
|
||||
<h2 className="mb-3 flex items-center gap-2 text-lg font-bold">
|
||||
<User /> Patientendetails
|
||||
</h2>
|
||||
<p className="text-base-content font-semibold">{mission.missionPatientInfo}</p>
|
||||
<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
|
||||
</h2>
|
||||
<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 }) => (
|
||||
<li className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
className="text-base font-bold"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[hpgStateToFMSStatus(state)],
|
||||
}}
|
||||
@@ -457,8 +457,8 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
);
|
||||
|
||||
return (
|
||||
<div className="p-4 text-base-content">
|
||||
<div className="flex items-center w-full justify-between mb-2">
|
||||
<div className="text-base-content p-4">
|
||||
<div className="mb-2 flex w-full items-center justify-between">
|
||||
<h2 className="flex items-center gap-2 text-lg font-bold">
|
||||
<SmartphoneNfc /> Rettungsmittel
|
||||
</h2>
|
||||
@@ -480,9 +480,9 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
</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 && (
|
||||
<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
|
||||
</p>
|
||||
)}
|
||||
@@ -494,17 +494,17 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
return (
|
||||
<li key={index} className="flex items-center gap-2">
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
className="text-base font-bold"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[connectedAircraft?.fmsStatus || "6"],
|
||||
}}
|
||||
>
|
||||
{connectedAircraft?.fmsStatus || "6"}
|
||||
</span>
|
||||
<span className="text-base-content flex flex-col ">
|
||||
<span className="text-base-content flex flex-col">
|
||||
<span className="font-bold">{station.bosCallsign}</span>
|
||||
{!connectedAircraft && (
|
||||
<span className="text-gray-400 text-xs">Kein Benutzer verbunden</span>
|
||||
<span className="text-xs text-gray-400">Kein Benutzer verbunden</span>
|
||||
)}
|
||||
</span>
|
||||
</li>
|
||||
@@ -522,7 +522,7 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
</ul>
|
||||
{dispatcherConnected && (
|
||||
<div>
|
||||
<div className="divider mt-0 mb-0" />
|
||||
<div className="divider mb-0 mt-0" />
|
||||
<div className="flex items-center gap-2">
|
||||
{/* TODO: make it a small multiselect */}
|
||||
<StationsSelect
|
||||
@@ -530,7 +530,6 @@ const Rettungsmittel = ({ mission }: { mission: Mission }) => {
|
||||
className="min-w-[320px] flex-1"
|
||||
isMulti={false}
|
||||
onChange={(v) => {
|
||||
console.log("Selected station:", v);
|
||||
setSelectedStation({
|
||||
selectedStationId: v?.selectedStationIds[0],
|
||||
hpgAmbulanceState: mission.hpgAmbulanceState || HpgState.NOT_REQUESTED,
|
||||
@@ -657,7 +656,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
<div className="flex items-center gap-2">
|
||||
{!isAddingNote ? (
|
||||
<button
|
||||
className="text-base-content text-base cursor-pointer"
|
||||
className="text-base-content cursor-pointer text-base"
|
||||
onClick={() => setIsAddingNote(true)}
|
||||
>
|
||||
<span className="flex items-center gap-2">
|
||||
@@ -665,7 +664,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
</span>
|
||||
</button>
|
||||
) : (
|
||||
<div className="flex items-center gap-2 w-full">
|
||||
<div className="flex w-full items-center gap-2">
|
||||
<input
|
||||
type="text"
|
||||
placeholder=""
|
||||
@@ -697,7 +696,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
<div className="divider m-0" />
|
||||
</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[])
|
||||
.slice()
|
||||
.reverse()
|
||||
@@ -712,7 +711,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
})}
|
||||
</span>
|
||||
<span
|
||||
className="font-bold text-base"
|
||||
className="text-base font-bold"
|
||||
style={{
|
||||
color: FMS_STATUS_TEXT_COLORS[entry.data.newFMSstatus],
|
||||
}}
|
||||
@@ -732,7 +731,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
})}
|
||||
</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={{
|
||||
color: FMS_STATUS_TEXT_COLORS[6],
|
||||
}}
|
||||
@@ -781,7 +780,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
})}
|
||||
</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={{
|
||||
color: FMS_STATUS_TEXT_COLORS[6],
|
||||
}}
|
||||
@@ -830,7 +829,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
||||
})}
|
||||
</ul>
|
||||
{!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
|
||||
</p>
|
||||
)}
|
||||
|
||||
@@ -91,16 +91,16 @@ export const SettingsBtn = () => {
|
||||
modalRef.current?.showModal();
|
||||
}}
|
||||
>
|
||||
<GearIcon className="w-5 h-5" />
|
||||
<GearIcon className="h-5 w-5" />
|
||||
</button>
|
||||
|
||||
<dialog ref={modalRef} className="modal">
|
||||
<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
|
||||
</h3>
|
||||
<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">
|
||||
<span>Eingabegerät</span>
|
||||
<select
|
||||
@@ -122,7 +122,7 @@ export const SettingsBtn = () => {
|
||||
</select>
|
||||
</label>
|
||||
</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
|
||||
</p>
|
||||
<div className="w-full">
|
||||
@@ -139,7 +139,7 @@ export const SettingsBtn = () => {
|
||||
value={settings.micVolume}
|
||||
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>25%</span>
|
||||
<span>50%</span>
|
||||
@@ -155,24 +155,23 @@ export const SettingsBtn = () => {
|
||||
)}
|
||||
<div className="divider w-full" />
|
||||
</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
|
||||
</p>
|
||||
<div className="w-full mb-2">
|
||||
<div className="mb-2 w-full">
|
||||
<input
|
||||
type="range"
|
||||
min={0}
|
||||
max={1}
|
||||
step={0.01}
|
||||
onChange={(e) => {
|
||||
console.log("Radio Volume", e.target.value);
|
||||
const value = parseFloat(e.target.value);
|
||||
setSettingsPartial({ radioVolume: value });
|
||||
}}
|
||||
value={settings.radioVolume}
|
||||
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>25%</span>
|
||||
<span>50%</span>
|
||||
@@ -181,7 +180,7 @@ export const SettingsBtn = () => {
|
||||
</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
|
||||
</p>
|
||||
<div className="w-full">
|
||||
@@ -200,7 +199,7 @@ export const SettingsBtn = () => {
|
||||
value={settings.dmeVolume}
|
||||
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>25%</span>
|
||||
<span>50%</span>
|
||||
@@ -208,12 +207,12 @@ export const SettingsBtn = () => {
|
||||
<span>100%</span>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex justify-center w-full">
|
||||
<div className="flex w-full justify-center">
|
||||
<div className="divider w-full" />
|
||||
</div>
|
||||
<div className="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
|
||||
</span>
|
||||
<input
|
||||
@@ -226,7 +225,7 @@ export const SettingsBtn = () => {
|
||||
|
||||
<p className="label mt-2 w-full">
|
||||
<Link
|
||||
href="https://docs.virtualairrescue.com/docs/Leitstelle/App-Alarmierung#download"
|
||||
href="https://docs.virtualairrescue.com/pilotenbereich/app-alarmierung.html#download"
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
className="link link-hover link-primary"
|
||||
@@ -237,7 +236,7 @@ export const SettingsBtn = () => {
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<div className="flex justify-between modal-action">
|
||||
<div className="modal-action flex justify-between">
|
||||
<button
|
||||
className="btn btn-soft"
|
||||
type="submit"
|
||||
|
||||
@@ -4,33 +4,38 @@ import {
|
||||
RemoteParticipant,
|
||||
RemoteTrack,
|
||||
RemoteTrackPublication,
|
||||
Track,
|
||||
} from "livekit-client";
|
||||
|
||||
const initialTrackTimeouts = new Map<string, NodeJS.Timeout>();
|
||||
|
||||
export const handleTrackSubscribed = (
|
||||
track: RemoteTrack,
|
||||
publication: RemoteTrackPublication,
|
||||
participant: RemoteParticipant,
|
||||
) => {
|
||||
if (!track.isMuted) {
|
||||
useAudioStore.getState().addSpeakingParticipant(participant);
|
||||
}
|
||||
|
||||
if (track.kind === Track.Kind.Video || track.kind === Track.Kind.Audio) {
|
||||
// attach it to a new HTMLVideoElement or HTMLAudioElement
|
||||
const element = track.attach();
|
||||
element.pause();
|
||||
|
||||
if (!track.isMuted) {
|
||||
initialTrackTimeouts.set(
|
||||
participant.sid,
|
||||
setTimeout(() => {
|
||||
useAudioStore.getState().addSpeakingParticipant(participant);
|
||||
}, 1000),
|
||||
);
|
||||
}
|
||||
setTimeout(() => {
|
||||
element.play();
|
||||
}, 1000);
|
||||
|
||||
track.on("unmuted", () => {
|
||||
useAudioStore.getState().addSpeakingParticipant(participant);
|
||||
element.volume = useAudioStore.getState().settings.radioVolume;
|
||||
});
|
||||
track.on("unmuted", () => {
|
||||
useAudioStore.getState().addSpeakingParticipant(participant);
|
||||
});
|
||||
}
|
||||
|
||||
track.on("muted", () => {
|
||||
clearTimeout(initialTrackTimeouts.get(participant.sid));
|
||||
initialTrackTimeouts.get(participant.sid);
|
||||
useAudioStore.getState().removeSpeakingParticipant(participant);
|
||||
});
|
||||
};
|
||||
|
||||
@@ -108,7 +108,6 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
) {
|
||||
const { room, disconnect, connect } = get();
|
||||
const role = room?.localParticipant.attributes.role;
|
||||
console.log(role);
|
||||
if (room?.name || role) {
|
||||
disconnect();
|
||||
connect(room?.name || "", role || "user");
|
||||
@@ -170,10 +169,8 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
const inputStream = await navigator.mediaDevices.getUserMedia({
|
||||
audio: {
|
||||
deviceId: get().settings.micDeviceId ?? undefined,
|
||||
noiseSuppression: true,
|
||||
},
|
||||
});
|
||||
|
||||
// Funk-Effekt anwenden
|
||||
const radioStream = getRadioStream(inputStream, get().settings.micVolume);
|
||||
if (!radioStream) throw new Error("Konnte Funkstream nicht erzeugen");
|
||||
@@ -186,6 +183,7 @@ export const useAudioStore = create<TalkState>((set, get) => ({
|
||||
source: Track.Source.Microphone,
|
||||
});
|
||||
await publishedTrack.mute();
|
||||
|
||||
set({ localRadioTrack: publishedTrack });
|
||||
|
||||
set({ state: "connected", room, message: null });
|
||||
|
||||
@@ -116,14 +116,12 @@ dispatchSocket.on(
|
||||
"chat-message",
|
||||
({ userId, message }: { userId: string; message: ChatMessage }) => {
|
||||
const store = useLeftMenuStore.getState();
|
||||
console.log("chat-message", userId, message);
|
||||
// Update the chat store with the new message
|
||||
store.addMessage(userId, message);
|
||||
},
|
||||
);
|
||||
pilotSocket.on("chat-message", ({ userId, message }: { userId: string; message: ChatMessage }) => {
|
||||
const store = useLeftMenuStore.getState();
|
||||
console.log("chat-message", userId, message);
|
||||
// Update the chat store with the new message
|
||||
store.addMessage(userId, message);
|
||||
});
|
||||
|
||||
@@ -3,7 +3,7 @@
|
||||
"version": "0.1.0",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 3001",
|
||||
"build": "next build",
|
||||
@@ -14,9 +14,9 @@
|
||||
"dependencies": {
|
||||
"@eslint/eslintrc": "^3.3.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/track-processors": "^0.5.7",
|
||||
"@livekit/track-processors": "^0.5.8",
|
||||
"@next-auth/prisma-adapter": "^1.0.7",
|
||||
"@radix-ui/react-icons": "^1.3.2",
|
||||
"@repo/db": "workspace:*",
|
||||
@@ -24,37 +24,37 @@
|
||||
"@repo/shared-components": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@turf/turf": "^7.2.0",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/leaflet": "^1.9.19",
|
||||
"@types/leaflet": "^1.9.20",
|
||||
"@types/node": "^22.15.34",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"axios": "^1.10.0",
|
||||
"clsx": "^2.1.1",
|
||||
"daisyui": "^5.0.43",
|
||||
"daisyui": "^5.0.46",
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint-config-next": "^15.3.4",
|
||||
"eslint-config-next": "^15.4.2",
|
||||
"geojson": "^0.5.0",
|
||||
"i": "^0.3.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"leaflet": "^1.9.4",
|
||||
"leaflet.polylinemeasure": "^3.0.0",
|
||||
"livekit-client": "^2.14.0",
|
||||
"livekit-client": "^2.15.3",
|
||||
"livekit-server-sdk": "^2.13.1",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "^15.3.4",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^15.4.2",
|
||||
"next-auth": "^4.24.11",
|
||||
"npm": "^11.4.2",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.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-leaflet": "^5.0.0",
|
||||
"react-select": "^5.10.1",
|
||||
"react-select": "^5.10.2",
|
||||
"socket.io-client": "^4.8.1",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
|
||||
BIN
apps/dispatch/public/icons/mapMarker.png
Normal file
BIN
apps/dispatch/public/icons/mapMarker.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
@@ -14,14 +14,6 @@ const initTransporter = () => {
|
||||
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");
|
||||
|
||||
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({
|
||||
host: process.env.MAIL_SERVER,
|
||||
port: parseInt(process.env.MAIL_PORT),
|
||||
|
||||
@@ -8,30 +8,30 @@
|
||||
"start": "tsx index.ts --transpile-only",
|
||||
"build": "tsc"
|
||||
},
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"devDependencies": {
|
||||
"concurrently": "^9.1.2",
|
||||
"concurrently": "^9.2.0",
|
||||
"typescript": "latest"
|
||||
},
|
||||
"dependencies": {
|
||||
"@react-email/components": "^0.0.41",
|
||||
"@react-email/components": "^0.3.2",
|
||||
"@repo/shared-components": "workspace:*",
|
||||
"@repo/db": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@types/cors": "^2.8.18",
|
||||
"@types/express": "^5.0.2",
|
||||
"@types/cors": "^2.8.19",
|
||||
"@types/express": "^5.0.3",
|
||||
"@types/node": "^22.15.29",
|
||||
"@types/nodemailer": "^6.4.17",
|
||||
"@types/react": "^19.1.6",
|
||||
"axios": "^1.9.0",
|
||||
"@types/react": "^19.1.8",
|
||||
"axios": "^1.10.0",
|
||||
"cors": "^2.8.5",
|
||||
"cron": "^4.3.1",
|
||||
"dotenv": "^16.5.0",
|
||||
"cron": "^4.3.2",
|
||||
"dotenv": "^17.2.0",
|
||||
"express": "^5.1.0",
|
||||
"nodemailer": "^7.0.3",
|
||||
"nodemailer": "^7.0.5",
|
||||
"nodemon": "^3.1.10",
|
||||
"react": "^19.1.0",
|
||||
"tsconfig-paths": "^4.2.0",
|
||||
"tsx": "^4.19.4"
|
||||
"tsx": "^4.20.3"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -34,8 +34,8 @@ export const AppointmentModal = ({
|
||||
const participantTableRef = useRef<PaginatedTableRef>(null);
|
||||
|
||||
return (
|
||||
<dialog ref={ref} className="modal ">
|
||||
<div className="modal-box min-w-[900px] min-h-[500px]">
|
||||
<dialog ref={ref} className="modal">
|
||||
<div className="modal-box min-h-[500px] min-w-[900px]">
|
||||
<form method="dialog">
|
||||
{/* if there is a button in form, it will close the modal */}
|
||||
<button
|
||||
@@ -55,8 +55,8 @@ export const AppointmentModal = ({
|
||||
})}
|
||||
className="flex flex-col"
|
||||
>
|
||||
<div className="flex justify-between mr-7">
|
||||
<h3 className="font-bold text-lg">Termin {appointmentForm.watch("id")}</h3>
|
||||
<div className="mr-7 flex justify-between">
|
||||
<h3 className="text-lg font-bold">Termin {appointmentForm.watch("id")}</h3>
|
||||
<DateInput
|
||||
value={new Date(appointmentForm.watch("appointmentDate") || Date.now())}
|
||||
onChange={(date) => appointmentForm.setValue("appointmentDate", date)}
|
||||
@@ -121,11 +121,6 @@ export const AppointmentModal = ({
|
||||
attended: true,
|
||||
appointmentCancelled: false,
|
||||
});
|
||||
console.log(
|
||||
"Participant attended",
|
||||
event.finisherMoodleCourseId,
|
||||
!event.finisherMoodleCourseId?.length,
|
||||
);
|
||||
if (!event.finisherMoodleCourseId?.length) {
|
||||
toast(
|
||||
"Teilnehmer hat das event abgeschlossen, workflow ausgeführt",
|
||||
|
||||
@@ -11,7 +11,7 @@ import {
|
||||
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { redirect } from "next/navigation";
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import "react-datepicker/dist/react-datepicker.css";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { PaginatedTable, PaginatedTableRef } from "../../../../_components/PaginatedTable";
|
||||
@@ -24,6 +24,7 @@ import { deleteEvent, upsertEvent } from "../action";
|
||||
import { AppointmentModal } from "./AppointmentModal";
|
||||
import { ParticipantModal } from "./ParticipantModal";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import toast from "react-hot-toast";
|
||||
|
||||
export const Form = ({ event }: { event?: Event }) => {
|
||||
const { data: session } = useSession();
|
||||
@@ -66,6 +67,7 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
<form
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
await upsertEvent(values, event?.id);
|
||||
toast.success("Event erfolgreich gespeichert");
|
||||
if (!event) redirect(`/admin/event`);
|
||||
})}
|
||||
className="grid grid-cols-6 gap-3"
|
||||
@@ -75,17 +77,6 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
<h2 className="card-title">
|
||||
<FileText className="w-5 h-5" /> Allgemeines
|
||||
</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
|
||||
form={form}
|
||||
name="type"
|
||||
@@ -95,6 +86,9 @@ export const Form = ({ event }: { event?: Event }) => {
|
||||
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 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"
|
||||
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" />
|
||||
<div className="divider w-full" />
|
||||
|
||||
<Switch form={form} name="hidden" label="Event verstecken" />
|
||||
</div>
|
||||
</div>
|
||||
{form.watch("hasPresenceEvents") ? (
|
||||
|
||||
@@ -11,7 +11,7 @@ const page = () => {
|
||||
<PaginatedTable
|
||||
stickyHeaders
|
||||
prismaModel="heliport"
|
||||
searchFields={["siteName", "info", "hospital"]}
|
||||
searchFields={["siteName", "info", "hospital", "designator"]}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
@@ -45,11 +45,11 @@ const page = () => {
|
||||
}
|
||||
leftOfSearch={
|
||||
<span className="flex items-center gap-2">
|
||||
<DatabaseBackupIcon className="w-5 h-5" /> Heliports
|
||||
<DatabaseBackupIcon className="h-5 w-5" /> Heliports
|
||||
</span>
|
||||
}
|
||||
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"}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">Erstellen</button>
|
||||
</Link>
|
||||
|
||||
@@ -2,20 +2,26 @@
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
import { StationOptionalDefaultsSchema } from "@repo/db/zod";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { BosUse, Country, Station } from "@repo/db";
|
||||
import { FileText, LocateIcon, PlaneIcon } from "lucide-react";
|
||||
import { BosUse, ConnectedAircraft, Country, Station, User } from "@repo/db";
|
||||
import { FileText, LocateIcon, PlaneIcon, UserIcon } from "lucide-react";
|
||||
import { Input } from "../../../../_components/ui/Input";
|
||||
import { useState } from "react";
|
||||
import { deleteStation, upsertStation } from "../action";
|
||||
import { Button } from "../../../../_components/ui/Button";
|
||||
import { redirect } from "next/navigation";
|
||||
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 }) => {
|
||||
const form = useForm({
|
||||
resolver: zodResolver(StationOptionalDefaultsSchema),
|
||||
defaultValues: station,
|
||||
});
|
||||
const dispoTableRef = useRef<PaginatedTableRef>(null);
|
||||
// const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
return (
|
||||
<>
|
||||
@@ -27,10 +33,10 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
})}
|
||||
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">
|
||||
<h2 className="card-title">
|
||||
<FileText className="w-5 h-5" /> Allgemeines
|
||||
<FileText className="h-5 w-5" /> Allgemeines
|
||||
</h2>
|
||||
<Input form={form} label="BOS Rufname" name="bosCallsign" className="input-sm" />
|
||||
<Input
|
||||
@@ -55,7 +61,7 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
/>
|
||||
|
||||
<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
|
||||
className="input-sm select select-bordered select-sm"
|
||||
{...form.register("bosUse")}
|
||||
@@ -69,13 +75,13 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
</label>
|
||||
</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">
|
||||
<h2 className="card-title">
|
||||
<LocateIcon className="w-5 h-5" /> Standort + Ausrüstung
|
||||
<LocateIcon className="h-5 w-5" /> Standort + Ausrüstung
|
||||
</h2>
|
||||
<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
|
||||
className="input-sm select select-bordered select-sm"
|
||||
{...form.register("country", {})}
|
||||
@@ -94,21 +100,21 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
name="locationStateShort"
|
||||
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">
|
||||
<label className="label cursor-pointer flex">
|
||||
<label className="label flex cursor-pointer">
|
||||
<span className="flex-1 text-left">Winde</span>
|
||||
<input type="checkbox" className="toggle" {...form.register("hasWinch")} />
|
||||
</label>
|
||||
<label className="label cursor-pointer flex">
|
||||
<label className="label flex cursor-pointer">
|
||||
<span className="flex-1 text-left">Nachtsicht-Gerät</span>
|
||||
<input type="checkbox" className="toggle" {...form.register("hasNvg")} />
|
||||
</label>
|
||||
<label className="label cursor-pointer flex">
|
||||
<label className="label flex cursor-pointer">
|
||||
<span className="flex-1 text-left">24-Stunden Einsatzfähig</span>
|
||||
<input type="checkbox" className="toggle" {...form.register("is24h")} />
|
||||
</label>
|
||||
<label className="label cursor-pointer flex">
|
||||
<label className="label flex cursor-pointer">
|
||||
<span className="flex-1 text-left">Bergetau</span>
|
||||
<input type="checkbox" className="toggle" {...form.register("hasRope")} />
|
||||
</label>
|
||||
@@ -132,16 +138,16 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
type="number"
|
||||
step="any"
|
||||
/>
|
||||
<label className="label cursor-pointer flex">
|
||||
<span className="text-lg flex-1 text-left">Reichweiten ausblenden</span>
|
||||
<label className="label flex cursor-pointer">
|
||||
<span className="flex-1 text-left text-lg">Reichweiten ausblenden</span>
|
||||
<input type="checkbox" className="toggle" {...form.register("hideRangeRings")} />
|
||||
</label>
|
||||
</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">
|
||||
<h2 className="card-title">
|
||||
<PlaneIcon className="w-5 h-5" /> Hubschrauber
|
||||
<PlaneIcon className="h-5 w-5" /> Hubschrauber
|
||||
</h2>
|
||||
<Input form={form} label="Hubschrauber Typ" name="aircraft" className="input-sm" />
|
||||
<Input
|
||||
@@ -160,8 +166,8 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card bg-base-200 shadow-xl col-span-6">
|
||||
<div className="card-body ">
|
||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
||||
<div className="card-body">
|
||||
<div className="flex w-full gap-4">
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
@@ -184,6 +190,93 @@ export const StationForm = ({ station }: { station?: Station }) => {
|
||||
</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>
|
||||
</>
|
||||
);
|
||||
|
||||
@@ -10,7 +10,7 @@ import {
|
||||
Station,
|
||||
User,
|
||||
} from "@repo/db";
|
||||
import { useRef, useState } from "react";
|
||||
import { useRef } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import {
|
||||
deleteDispoHistory,
|
||||
@@ -84,11 +84,11 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
|
||||
})}
|
||||
>
|
||||
<h2 className="card-title">
|
||||
<MixerHorizontalIcon className="w-5 h-5" /> User bearbeiten
|
||||
<MixerHorizontalIcon className="h-5 w-5" /> User bearbeiten
|
||||
</h2>
|
||||
<div className="text-left">
|
||||
<label className="floating-label w-full mb-5 mt-5">
|
||||
<span className="text-lg flex items-center gap-2">
|
||||
<label className="floating-label mb-5 mt-5 w-full">
|
||||
<span className="flex items-center gap-2 text-lg">
|
||||
<PersonIcon /> Vorname
|
||||
</span>
|
||||
<input
|
||||
@@ -102,8 +102,8 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
|
||||
{form.formState.errors.firstname && (
|
||||
<p className="text-error">{form.formState.errors.firstname.message}</p>
|
||||
)}
|
||||
<label className="floating-label w-full mb-5">
|
||||
<span className="text-lg flex items-center gap-2">
|
||||
<label className="floating-label mb-5 w-full">
|
||||
<span className="flex items-center gap-2 text-lg">
|
||||
<PersonIcon /> Nachname
|
||||
</span>
|
||||
<input
|
||||
@@ -120,13 +120,13 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
|
||||
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
|
||||
<>
|
||||
<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
|
||||
</span>
|
||||
<input
|
||||
{...form.register("email")}
|
||||
type="text"
|
||||
className="input input-bordered w-full mb-2"
|
||||
className="input input-bordered mb-2 w-full"
|
||||
defaultValue={user?.email}
|
||||
placeholder="E-Mail"
|
||||
/>
|
||||
@@ -166,7 +166,7 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
|
||||
{session.data?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
|
||||
<div className="mt-2 space-y-1">
|
||||
<p className="text-gray-400">Berechtigung Schnellauswahl</p>
|
||||
<div className="flex gap-2 justify-evenly">
|
||||
<div className="flex justify-evenly gap-2">
|
||||
<button
|
||||
type="button"
|
||||
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="flex-1">
|
||||
<h2 className="card-title">
|
||||
<MixerHorizontalIcon className="w-5 h-5" /> Dispo-Verbindungs Historie
|
||||
<MixerHorizontalIcon className="h-5 w-5" /> Dispo-Verbindungs Historie
|
||||
</h2>
|
||||
<PaginatedTable
|
||||
ref={dispoTableRef}
|
||||
@@ -313,7 +313,7 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
|
||||
</div>
|
||||
<div className="flex-1">
|
||||
<h2 className="card-title">
|
||||
<PlaneIcon className="w-5 h-5" /> Pilot-Verbindungs Historie
|
||||
<PlaneIcon className="h-5 w-5" /> Pilot-Verbindungs Historie
|
||||
</h2>
|
||||
<PaginatedTable
|
||||
ref={dispoTableRef}
|
||||
@@ -396,7 +396,7 @@ export const UserPenalties = ({ user }: { user: User }) => {
|
||||
<div className="card-body">
|
||||
<h2 className="card-title flex justify-between">
|
||||
<span className="flex items-center gap-2">
|
||||
<ExclamationTriangleIcon className="w-5 h-5" /> Audit-log
|
||||
<ExclamationTriangleIcon className="h-5 w-5" /> Audit-log
|
||||
</span>
|
||||
<div className="flex gap-2">
|
||||
<PenaltyDropdown
|
||||
@@ -478,7 +478,7 @@ export const UserReports = ({ user }: { user: User }) => {
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<ExclamationTriangleIcon className="w-5 h-5" /> Nutzer Reports
|
||||
<ExclamationTriangleIcon className="h-5 w-5" /> Nutzer Reports
|
||||
</h2>
|
||||
<PaginatedTable
|
||||
prismaModel="report"
|
||||
@@ -528,10 +528,10 @@ export const AdminForm = ({
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<LightningBoltIcon className="w-5 h-5" /> Administration
|
||||
<LightningBoltIcon className="h-5 w-5" /> Administration
|
||||
</h2>
|
||||
<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
|
||||
onClick={async () => {
|
||||
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
|
||||
</Button>
|
||||
{session?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
|
||||
<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"
|
||||
>
|
||||
<Button
|
||||
@@ -581,14 +581,14 @@ export const AdminForm = ({
|
||||
router.refresh();
|
||||
}}
|
||||
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
|
||||
</Button>
|
||||
)}
|
||||
{discordAccount && (
|
||||
<div
|
||||
className="tooltip flex-1 min-w-[250px]"
|
||||
className="tooltip min-w-[250px] flex-1"
|
||||
data-tip={`Name: ${discordAccount.username}`}
|
||||
>
|
||||
<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
|
||||
</Button>
|
||||
@@ -613,12 +613,12 @@ export const AdminForm = ({
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="card-title">
|
||||
<ChartBarBigIcon className="w-5 h-5" /> Aktivität
|
||||
<ChartBarBigIcon className="h-5 w-5" /> Aktivität
|
||||
</h2>
|
||||
<div className="stats flex">
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-primary">
|
||||
<LightningBoltIcon className="w-8 h-8" />
|
||||
<LightningBoltIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="stat-value text-primary">
|
||||
{dispoTime.hours}h {dispoTime.minutes}min
|
||||
@@ -630,7 +630,7 @@ export const AdminForm = ({
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-primary">
|
||||
<PlaneIcon className="w-8 h-8" />
|
||||
<PlaneIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="stat-value text-primary">
|
||||
{pilotTime.hours}h {pilotTime.minutes}min
|
||||
@@ -642,12 +642,12 @@ export const AdminForm = ({
|
||||
</div>
|
||||
</div>
|
||||
<h2 className="card-title">
|
||||
<ExclamationTriangleIcon className="w-5 h-5" /> Reports
|
||||
<ExclamationTriangleIcon className="h-5 w-5" /> Reports
|
||||
</h2>
|
||||
<div className="stats flex">
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-primary">
|
||||
<ExclamationTriangleIcon className="w-8 h-8" />
|
||||
<ExclamationTriangleIcon className="h-8 w-8" />
|
||||
</div>
|
||||
<div className={cn("stat-value text-primary", reports.open && "text-warning")}>
|
||||
{reports.open}
|
||||
@@ -656,7 +656,7 @@ export const AdminForm = ({
|
||||
</div>
|
||||
<div className="stat">
|
||||
<div className="stat-figure text-primary">
|
||||
<Timer className="w-8 h-8" />
|
||||
<Timer className="h-8 w-8" />
|
||||
</div>
|
||||
<div className="stat-value text-primary">{reports.total60Days}</div>
|
||||
<div className="stat-title">in den letzten 60 Tagen</div>
|
||||
|
||||
@@ -34,6 +34,23 @@ const AdminUserPage = () => {
|
||||
header: "Nachname",
|
||||
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")
|
||||
? [
|
||||
{
|
||||
@@ -55,8 +72,8 @@ const AdminUserPage = () => {
|
||||
] as ColumnDef<User>[]
|
||||
} // Define the columns for the user table
|
||||
leftOfSearch={
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<User2 className="w-5 h-5" /> Benutzer
|
||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
||||
<User2 className="h-5 w-5" /> Benutzer
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
|
||||
@@ -38,8 +38,7 @@ export const EventCard = ({
|
||||
<div className="col-span-4">
|
||||
<div className="text-left text-balance">
|
||||
<MDEditor.Markdown
|
||||
source={event.description}
|
||||
className="whitespace-pre-wrap"
|
||||
source={event.descriptionShort}
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
|
||||
@@ -157,7 +157,6 @@ const ModalBtn = ({
|
||||
<div className="text-left text-balance">
|
||||
<MDEditor.Markdown
|
||||
source={event.description}
|
||||
className="whitespace-pre-wrap"
|
||||
style={{
|
||||
backgroundColor: "transparent",
|
||||
}}
|
||||
|
||||
@@ -13,10 +13,8 @@ export default function Page() {
|
||||
|
||||
const verifyCode = useCallback(
|
||||
async (code: string) => {
|
||||
console.log("Verifying code:", code);
|
||||
if (!code) return;
|
||||
const res = await checkEmailCode(code);
|
||||
console.log("Verification response:", res);
|
||||
if (res.error) {
|
||||
console.log("Verification error:", res.error);
|
||||
toast.error(res.error);
|
||||
@@ -34,12 +32,12 @@ export default function Page() {
|
||||
}, [paramsCode, verifyCode]);
|
||||
|
||||
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">
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<Check className="w-5 h-5" /> E-Mail Bestätigung
|
||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
||||
<Check className="h-5 w-5" /> E-Mail Bestätigung
|
||||
</p>
|
||||
<div className="flex justify-center gap-3 w-full">
|
||||
<div className="flex w-full justify-center gap-3">
|
||||
<input
|
||||
className="input flex-1"
|
||||
placeholder="Bestätigungscode"
|
||||
|
||||
@@ -21,13 +21,8 @@ export const options: AuthOptions = {
|
||||
where: { email: credentials.email },
|
||||
});
|
||||
const v1User = (oldUser as OldUser[]).find((u) => u.email === credentials.email);
|
||||
console.log("V1 User", v1User?.publicId);
|
||||
if (!user && v1User) {
|
||||
if (bcrypt.compareSync(credentials.password, v1User.password)) {
|
||||
console.log(
|
||||
"v1 User Passwords match:",
|
||||
bcrypt.compareSync(credentials.password, v1User.password),
|
||||
);
|
||||
const newUser = await createNewUserFromOld(v1User);
|
||||
await sendVerificationLink(newUser.id);
|
||||
return newUser;
|
||||
|
||||
@@ -49,7 +49,6 @@ export const getMoodleUserById = async (id: string) => {
|
||||
},
|
||||
},
|
||||
);
|
||||
console.log("Moodle User", user);
|
||||
const u = user[0];
|
||||
return (
|
||||
(u as {
|
||||
|
||||
@@ -1,14 +1,11 @@
|
||||
export const generateUUID = (length: number) => {
|
||||
// Base62-Version (a-z, A-Z, 0-9)
|
||||
const base62 =
|
||||
"0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
const base62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
|
||||
let string = "";
|
||||
|
||||
for (let i = 0; i < length; i++) {
|
||||
string += base62.charAt(Math.floor(Math.random() * base62.length));
|
||||
}
|
||||
|
||||
console.log(string);
|
||||
|
||||
return string;
|
||||
};
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
"name": "hub",
|
||||
"version": "0.1.0",
|
||||
"private": true,
|
||||
"packageManager": "pnpm@10.11.0",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"scripts": {
|
||||
"dev": "next dev --turbopack -p 3000",
|
||||
"build": "next build",
|
||||
@@ -19,45 +19,45 @@
|
||||
"@repo/shared-components": "workspace:*",
|
||||
"@repo/typescript-config": "workspace:*",
|
||||
"@tailwindcss/postcss": "^4.1.11",
|
||||
"@tanstack/react-query": "^5.81.5",
|
||||
"@tanstack/react-query": "^5.83.0",
|
||||
"@tanstack/react-table": "^8.21.3",
|
||||
"@types/jsonwebtoken": "^9.0.10",
|
||||
"@types/node": "^22.15.34",
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"@uiw/react-md-editor": "^4.0.7",
|
||||
"@uiw/react-md-editor": "^4.0.8",
|
||||
"axios": "^1.10.0",
|
||||
"bcryptjs": "^3.0.2",
|
||||
"clsx": "^2.1.1",
|
||||
"daisyui": "^5.0.43",
|
||||
"daisyui": "^5.0.46",
|
||||
"date-fns": "^4.1.0",
|
||||
"eslint-config-next": "^15.3.4",
|
||||
"eslint-config-next": "^15.4.2",
|
||||
"i": "^0.3.7",
|
||||
"jsonwebtoken": "^9.0.2",
|
||||
"lodash": "^4.17.21",
|
||||
"lucide-react": "^0.511.0",
|
||||
"next": "^15.3.4",
|
||||
"lucide-react": "^0.525.0",
|
||||
"next": "^15.4.2",
|
||||
"next-auth": "^4.24.11",
|
||||
"next-remove-imports": "^1.0.12",
|
||||
"npm": "^11.4.2",
|
||||
"postcss": "^8.5.6",
|
||||
"react": "^19.1.0",
|
||||
"react-datepicker": "^8.4.0",
|
||||
"react-day-picker": "^9.7.0",
|
||||
"react-day-picker": "^9.8.0",
|
||||
"react-dom": "^19.1.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-select": "^5.10.1",
|
||||
"react-select": "^5.10.2",
|
||||
"tailwind-merge": "^3.3.1",
|
||||
"tailwindcss": "^4.1.11",
|
||||
"zod": "^3.25.67",
|
||||
"zustand": "^5.0.6"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.30.0",
|
||||
"eslint": "^9.30.0",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"eslint": "^9.31.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.33.1"
|
||||
"typescript-eslint": "^8.37.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -12,20 +12,21 @@
|
||||
"format": "prettier --write \"**/*.{ts,tsx,md}\""
|
||||
},
|
||||
"devDependencies": {
|
||||
"prettier": "^3.5.3",
|
||||
"turbo": "^2.5.4",
|
||||
"prettier": "^3.6.2",
|
||||
"prettier-plugin-tailwindcss": "^0.6.14",
|
||||
"turbo": "^2.5.5",
|
||||
"typescript": "^5.8.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18",
|
||||
"pnpm": ">=10"
|
||||
},
|
||||
"packageManager": "pnpm@10.12.1",
|
||||
"packageManager": "pnpm@10.13.1",
|
||||
"workspaces": [
|
||||
"apps/*",
|
||||
"packages/*"
|
||||
],
|
||||
"dependencies": {
|
||||
"eslint": "^9.30.1"
|
||||
"eslint": "^9.31.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,11 +20,11 @@
|
||||
"author": "",
|
||||
"license": "ISC",
|
||||
"dependencies": {
|
||||
"@prisma/client": "^6.8.2",
|
||||
"@prisma/client": "^6.12.0",
|
||||
"zod": "^3.25.46",
|
||||
"zod-prisma-types": "^3.2.4"
|
||||
},
|
||||
"devDependencies": {
|
||||
"prisma": "^6.8.2"
|
||||
"prisma": "^6.12.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,6 +37,7 @@ model Participant {
|
||||
model Event {
|
||||
id Int @id @default(autoincrement())
|
||||
name String
|
||||
descriptionShort String @default("")
|
||||
description String
|
||||
type EVENT_TYPE @default(EVENT)
|
||||
discordRoleId String? @default("")
|
||||
|
||||
@@ -0,0 +1,2 @@
|
||||
-- AlterTable
|
||||
ALTER TABLE "Event" ADD COLUMN "descriptionShort" TEXT NOT NULL DEFAULT '';
|
||||
@@ -8,19 +8,19 @@
|
||||
"./react-internal": "./react-internal.js"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.17.0",
|
||||
"@next/eslint-plugin-next": "^15.3.3",
|
||||
"eslint": "^9.15.0",
|
||||
"eslint-config-prettier": "^9.1.0",
|
||||
"@eslint/js": "^9.31.0",
|
||||
"@next/eslint-plugin-next": "^15.4.2",
|
||||
"eslint": "^9.31.0",
|
||||
"eslint-config-prettier": "^10.1.8",
|
||||
"eslint-plugin-only-warn": "^1.1.0",
|
||||
"eslint-plugin-react": "^7.37.5",
|
||||
"eslint-plugin-react-hooks": "^5.2.0",
|
||||
"eslint-plugin-turbo": "^2.3.0",
|
||||
"globals": "^15.12.0",
|
||||
"eslint-plugin-turbo": "^2.5.5",
|
||||
"globals": "^16.3.0",
|
||||
"typescript": "^5.8.3",
|
||||
"typescript-eslint": "^8.15.0"
|
||||
"typescript-eslint": "^8.37.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@typescript-eslint/eslint-plugin": "^8.36.0"
|
||||
"@typescript-eslint/eslint-plugin": "^8.37.0"
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
"use client";
|
||||
import { set, isBefore, addDays } from "date-fns";
|
||||
|
||||
export function getNextDateWithTime(targetHour: number, targetMinute: number): Date {
|
||||
|
||||
@@ -12,11 +12,11 @@
|
||||
"@types/node": "^22.15.29",
|
||||
"clsx": "^2.1.1",
|
||||
"date-fns": "^4.1.0",
|
||||
"tailwind-merge": "^3.3.0"
|
||||
"tailwind-merge": "^3.3.1"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@types/react": "^19.1.8",
|
||||
"@types/react-dom": "^19.1.5",
|
||||
"@types/react-dom": "^19.1.6",
|
||||
"react": "^19.1.0",
|
||||
"react-dom": "^19.1.0"
|
||||
}
|
||||
|
||||
3622
pnpm-lock.yaml
generated
3622
pnpm-lock.yaml
generated
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user