added logbook
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { ConnectedAircraft, Mission, prisma } from "@repo/db";
|
import { ConnectedAircraft, getPublicUser, Mission, prisma, User } from "@repo/db";
|
||||||
import { io } from "index";
|
import { io } from "index";
|
||||||
import { sendNtfyMission } from "modules/ntfy";
|
import { sendNtfyMission } from "modules/ntfy";
|
||||||
|
|
||||||
@@ -9,6 +9,7 @@ export const sendAlert = async (
|
|||||||
}: {
|
}: {
|
||||||
stationId?: number;
|
stationId?: number;
|
||||||
},
|
},
|
||||||
|
user: User,
|
||||||
): Promise<{
|
): Promise<{
|
||||||
connectedAircrafts: ConnectedAircraft[];
|
connectedAircrafts: ConnectedAircraft[];
|
||||||
mission: Mission;
|
mission: Mission;
|
||||||
@@ -44,7 +45,6 @@ export const sendAlert = async (
|
|||||||
});
|
});
|
||||||
|
|
||||||
for (const aircraft of connectedAircrafts) {
|
for (const aircraft of connectedAircrafts) {
|
||||||
console.log(`Sending mission to: station:${aircraft.stationId}`);
|
|
||||||
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
|
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
|
||||||
...mission,
|
...mission,
|
||||||
Stations,
|
Stations,
|
||||||
@@ -54,21 +54,15 @@ export const sendAlert = async (
|
|||||||
});
|
});
|
||||||
if (!user) continue;
|
if (!user) continue;
|
||||||
if (user.settingsNtfyRoom) {
|
if (user.settingsNtfyRoom) {
|
||||||
await sendNtfyMission(
|
await sendNtfyMission(mission, Stations, aircraft.Station, user.settingsNtfyRoom);
|
||||||
mission,
|
|
||||||
Stations,
|
|
||||||
aircraft.Station,
|
|
||||||
user.settingsNtfyRoom,
|
|
||||||
);
|
|
||||||
}
|
}
|
||||||
const existingMissionOnStationUser =
|
const existingMissionOnStationUser = await prisma.missionOnStationUsers.findFirst({
|
||||||
await prisma.missionOnStationUsers.findFirst({
|
where: {
|
||||||
where: {
|
missionId: mission.id,
|
||||||
missionId: mission.id,
|
userId: aircraft.userId,
|
||||||
userId: aircraft.userId,
|
stationId: aircraft.stationId,
|
||||||
stationId: aircraft.stationId,
|
},
|
||||||
},
|
});
|
||||||
});
|
|
||||||
if (!existingMissionOnStationUser)
|
if (!existingMissionOnStationUser)
|
||||||
await prisma.missionOnStationUsers.create({
|
await prisma.missionOnStationUsers.create({
|
||||||
data: {
|
data: {
|
||||||
@@ -95,6 +89,17 @@ export const sendAlert = async (
|
|||||||
where: { id: Number(id) },
|
where: { id: Number(id) },
|
||||||
data: {
|
data: {
|
||||||
state: "running",
|
state: "running",
|
||||||
|
missionLog: {
|
||||||
|
push: {
|
||||||
|
type: "alert-log",
|
||||||
|
auto: false,
|
||||||
|
timeStamp: new Date().toISOString(),
|
||||||
|
data: {
|
||||||
|
stationId: stationId,
|
||||||
|
user: getPublicUser(user, { ignorePrivacy: true }),
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
return { connectedAircrafts, mission };
|
return { connectedAircrafts, mission };
|
||||||
|
|||||||
@@ -1,5 +1,7 @@
|
|||||||
import {
|
import {
|
||||||
|
getPublicUser,
|
||||||
HpgValidationState,
|
HpgValidationState,
|
||||||
|
MissionAlertLog,
|
||||||
MissionSdsLog,
|
MissionSdsLog,
|
||||||
MissionStationLog,
|
MissionStationLog,
|
||||||
NotificationPayload,
|
NotificationPayload,
|
||||||
@@ -119,6 +121,11 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
vehicleName?: "ambulance" | "police" | "firebrigade";
|
vehicleName?: "ambulance" | "police" | "firebrigade";
|
||||||
};
|
};
|
||||||
|
|
||||||
|
if (!req.user) {
|
||||||
|
res.status(401).json({ error: "Unauthorized" });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
if (vehicleName) {
|
if (vehicleName) {
|
||||||
const hpgAircrafts = await prisma.connectedAircraft.findMany({
|
const hpgAircrafts = await prisma.connectedAircraft.findMany({
|
||||||
@@ -137,6 +144,17 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
hpgAmbulanceState: vehicleName === "ambulance" ? "DISPATCHED" : undefined,
|
hpgAmbulanceState: vehicleName === "ambulance" ? "DISPATCHED" : undefined,
|
||||||
hpgFireEngineState: vehicleName === "firebrigade" ? "DISPATCHED" : undefined,
|
hpgFireEngineState: vehicleName === "firebrigade" ? "DISPATCHED" : undefined,
|
||||||
hpgPoliceState: vehicleName === "police" ? "DISPATCHED" : undefined,
|
hpgPoliceState: vehicleName === "police" ? "DISPATCHED" : undefined,
|
||||||
|
missionLog: {
|
||||||
|
push: {
|
||||||
|
type: "alert-log",
|
||||||
|
auto: false,
|
||||||
|
timeStamp: new Date().toISOString(),
|
||||||
|
data: {
|
||||||
|
vehicle: vehicleName,
|
||||||
|
user: getPublicUser(req.user as User),
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
hpgAircrafts.forEach((aircraft) => {
|
hpgAircrafts.forEach((aircraft) => {
|
||||||
@@ -156,9 +174,13 @@ router.post("/:id/send-alert", async (req, res) => {
|
|||||||
io.to("dispatchers").emit("update-mission", newMission);
|
io.to("dispatchers").emit("update-mission", newMission);
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
const { connectedAircrafts, mission } = await sendAlert(Number(id), {
|
const { connectedAircrafts, mission } = await sendAlert(
|
||||||
stationId,
|
Number(id),
|
||||||
});
|
{
|
||||||
|
stationId,
|
||||||
|
},
|
||||||
|
req.user,
|
||||||
|
);
|
||||||
|
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
message: `Einsatz gesendet (${connectedAircrafts.length} Nutzer) `,
|
message: `Einsatz gesendet (${connectedAircrafts.length} Nutzer) `,
|
||||||
@@ -231,7 +253,7 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
res.json({
|
res.json({
|
||||||
message: "HPG validation started",
|
message: "HPG validierung gestartet",
|
||||||
});
|
});
|
||||||
|
|
||||||
io.to(`desktop:${activeAircraftinMission}`).emit(
|
io.to(`desktop:${activeAircraftinMission}`).emit(
|
||||||
@@ -265,7 +287,8 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
|||||||
message: `HPG Validierung erfolgreich`,
|
message: `HPG Validierung erfolgreich`,
|
||||||
} as NotificationPayload);
|
} as NotificationPayload);
|
||||||
if (config?.alertWhenValid) {
|
if (config?.alertWhenValid) {
|
||||||
sendAlert(Number(id), {});
|
if (!req.user) return;
|
||||||
|
sendAlert(Number(id), {}, req.user);
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
io.to(`user:${req.user?.id}`).emit("notification", {
|
io.to(`user:${req.user?.id}`).emit("notification", {
|
||||||
@@ -276,25 +299,6 @@ router.post("/:id/validate-hpg", async (req, res) => {
|
|||||||
}
|
}
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
// TODO: remove this after testing
|
|
||||||
setTimeout(() => {
|
|
||||||
io.to(`user:${req.user?.id}`).emit("notification", {
|
|
||||||
type: "hpg-validation",
|
|
||||||
status: "success",
|
|
||||||
message: "HPG_BUSY",
|
|
||||||
data: {
|
|
||||||
mission,
|
|
||||||
},
|
|
||||||
} as NotificationPayload);
|
|
||||||
io.to(`user:${req.user?.id}`).emit("notification", {
|
|
||||||
type: "hpg-validation",
|
|
||||||
status: "failed",
|
|
||||||
message: `HPG Validation fehlgeschlagen`,
|
|
||||||
data: {
|
|
||||||
mission,
|
|
||||||
},
|
|
||||||
} as NotificationPayload);
|
|
||||||
}, 5000);
|
|
||||||
} catch (error) {
|
} catch (error) {
|
||||||
console.error(error);
|
console.error(error);
|
||||||
res.json({ error: (error as Error).message || "Failed to validate HPG" });
|
res.json({ error: (error as Error).message || "Failed to validate HPG" });
|
||||||
|
|||||||
@@ -24,6 +24,8 @@ import {
|
|||||||
HpgState,
|
HpgState,
|
||||||
HpgValidationState,
|
HpgValidationState,
|
||||||
Mission,
|
Mission,
|
||||||
|
MissionAlertLog,
|
||||||
|
MissionCompletedLog,
|
||||||
MissionLog,
|
MissionLog,
|
||||||
MissionMessageLog,
|
MissionMessageLog,
|
||||||
Prisma,
|
Prisma,
|
||||||
@@ -47,6 +49,7 @@ const Einsatzdetails = ({
|
|||||||
mission: Mission;
|
mission: Mission;
|
||||||
hpgNeedsAttention?: boolean;
|
hpgNeedsAttention?: boolean;
|
||||||
}) => {
|
}) => {
|
||||||
|
const session = useSession();
|
||||||
const queryClient = useQueryClient();
|
const queryClient = useQueryClient();
|
||||||
const deleteMissionMutation = useMutation({
|
const deleteMissionMutation = useMutation({
|
||||||
mutationKey: ["missions"],
|
mutationKey: ["missions"],
|
||||||
@@ -127,10 +130,21 @@ const Einsatzdetails = ({
|
|||||||
<button
|
<button
|
||||||
className="btn btn-xs btn-warning flex items-center gap-2"
|
className="btn btn-xs btn-warning flex items-center gap-2"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
|
if (!session.data) return;
|
||||||
editMissionMutation.mutate({
|
editMissionMutation.mutate({
|
||||||
id: mission.id,
|
id: mission.id,
|
||||||
mission: {
|
mission: {
|
||||||
state: "finished",
|
state: "finished",
|
||||||
|
missionLog: {
|
||||||
|
push: {
|
||||||
|
type: "completed-log",
|
||||||
|
auto: false,
|
||||||
|
timeStamp: new Date().toISOString(),
|
||||||
|
data: {
|
||||||
|
user: getPublicUser(session.data?.user, { ignorePrivacy: true }),
|
||||||
|
},
|
||||||
|
} as any,
|
||||||
|
},
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}}
|
}}
|
||||||
@@ -593,7 +607,7 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
|||||||
timeStamp: new Date().toISOString(),
|
timeStamp: new Date().toISOString(),
|
||||||
data: {
|
data: {
|
||||||
message: note,
|
message: note,
|
||||||
user: getPublicUser(session.data?.user),
|
user: getPublicUser(session.data?.user, { ignorePrivacy: true }),
|
||||||
},
|
},
|
||||||
} as MissionMessageLog,
|
} as MissionMessageLog,
|
||||||
];
|
];
|
||||||
@@ -694,7 +708,49 @@ const FMSStatusHistory = ({ mission }: { mission: Mission }) => {
|
|||||||
<span className="text-base-content">{entry.data.message}</span>
|
<span className="text-base-content">{entry.data.message}</span>
|
||||||
</li>
|
</li>
|
||||||
);
|
);
|
||||||
|
if (entry.type === "alert-log") {
|
||||||
|
const alertReceiver = entry.data.station?.bosCallsignShort || entry.data.vehicle;
|
||||||
|
return (
|
||||||
|
<li key={index} className="flex items-center gap-2">
|
||||||
|
<span className="text-base-content">
|
||||||
|
{new Date(entry.timeStamp).toLocaleTimeString([], {
|
||||||
|
hour: "2-digit",
|
||||||
|
minute: "2-digit",
|
||||||
|
})}
|
||||||
|
</span>
|
||||||
|
<span
|
||||||
|
className="font-bold text-base flex items-center gap-0.5"
|
||||||
|
style={{
|
||||||
|
color: FMS_STATUS_TEXT_COLORS[6],
|
||||||
|
}}
|
||||||
|
>
|
||||||
|
{entry.data.user.firstname?.[0]?.toUpperCase() ?? "?"}
|
||||||
|
{entry.data.user.lastname?.[0]?.toUpperCase() ?? "?"}
|
||||||
|
{alertReceiver && (
|
||||||
|
<>
|
||||||
|
<svg
|
||||||
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
|
fill="none"
|
||||||
|
viewBox="0 0 24 24"
|
||||||
|
strokeWidth="2"
|
||||||
|
stroke="currentColor"
|
||||||
|
className="size-4"
|
||||||
|
>
|
||||||
|
<path
|
||||||
|
strokeLinecap="round"
|
||||||
|
strokeLinejoin="round"
|
||||||
|
d="M13.5 4.5 21 12m0 0-7.5 7.5M21 12H3"
|
||||||
|
/>
|
||||||
|
</svg>
|
||||||
|
|
||||||
|
{alertReceiver}
|
||||||
|
</>
|
||||||
|
)}
|
||||||
|
</span>
|
||||||
|
<span className="text-base-content">Einsatz alarmiert</span>
|
||||||
|
</li>
|
||||||
|
);
|
||||||
|
}
|
||||||
return null;
|
return null;
|
||||||
})}
|
})}
|
||||||
</ul>
|
</ul>
|
||||||
|
|||||||
@@ -154,6 +154,8 @@ export const MissionForm = () => {
|
|||||||
if (validationRequired) {
|
if (validationRequired) {
|
||||||
await startHpgValidation(newMission.id, {
|
await startHpgValidation(newMission.id, {
|
||||||
alertWhenValid,
|
alertWhenValid,
|
||||||
|
}).catch((error) => {
|
||||||
|
toast.error(`Fehler beim Starten der HPG-Validierung: ${error.message}`);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return newMission;
|
return newMission;
|
||||||
@@ -171,7 +173,9 @@ export const MissionForm = () => {
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
if (validationRequired) {
|
if (validationRequired) {
|
||||||
await startHpgValidation(newMission.id, {});
|
await startHpgValidation(newMission.id, {}).catch((error) => {
|
||||||
|
toast.error(`Fehler beim Starten der HPG-Validierung: ${error.message}`);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
return newMission;
|
return newMission;
|
||||||
};
|
};
|
||||||
|
|||||||
80
apps/hub/app/(app)/_components/RecentFlights.tsx
Normal file
80
apps/hub/app/(app)/_components/RecentFlights.tsx
Normal file
@@ -0,0 +1,80 @@
|
|||||||
|
"use client";
|
||||||
|
import { Mission, MissionAlertLog, MissionLog, Station } from "@repo/db";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { PaginatedTable } from "_components/PaginatedTable";
|
||||||
|
import { ArrowRight, NotebookText } from "lucide-react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const RecentFlights = () => {
|
||||||
|
const session = useSession();
|
||||||
|
return (
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title justify-between">
|
||||||
|
<span className="card-title">
|
||||||
|
<NotebookText className="w-4 h-4" /> Logbook
|
||||||
|
</span>
|
||||||
|
<Link className="badge badge-sm badge-info badge-outline" href="/logbook">
|
||||||
|
Zum vollständigen Logbook <ArrowRight className="w-4 h-4" />
|
||||||
|
</Link>
|
||||||
|
</h2>
|
||||||
|
<PaginatedTable
|
||||||
|
prismaModel={"missionOnStationUsers"}
|
||||||
|
filter={{
|
||||||
|
userId: session.data?.user?.id ?? "",
|
||||||
|
Mission: {
|
||||||
|
state: "finished",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
include={{
|
||||||
|
Station: true,
|
||||||
|
User: true,
|
||||||
|
Mission: true,
|
||||||
|
}}
|
||||||
|
columns={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
header: "Station",
|
||||||
|
accessorKey: "Station.name",
|
||||||
|
cell: ({ row }) => row.original.Station.bosCallsign,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
header: "Datum",
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
cell: ({ row }) => new Date(row.original.Mission.createdAt).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Einsatzlänge",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const missionStartLogs = row.original.Mission.missionLog.filter(
|
||||||
|
(log) => (log as unknown as MissionLog).type === "alert-log",
|
||||||
|
) as unknown as MissionAlertLog[] | undefined;
|
||||||
|
// Find the first log with a station ID that matches the current row's station ID or use the first log if none match
|
||||||
|
const missionStartLog =
|
||||||
|
missionStartLogs?.find(
|
||||||
|
(log) => log.data.station?.id === row.original.Station.id,
|
||||||
|
) || missionStartLogs?.[0];
|
||||||
|
|
||||||
|
const missionEndLog = row.original.Mission.missionLog.find(
|
||||||
|
(log) => (log as unknown as MissionLog).type === "completed-log",
|
||||||
|
) as unknown as MissionAlertLog | undefined;
|
||||||
|
|
||||||
|
if (!missionStartLog) return "Unbekannt";
|
||||||
|
if (!missionEndLog) return "Unbekannt";
|
||||||
|
|
||||||
|
const start = new Date(missionStartLog.timeStamp);
|
||||||
|
const end = new Date(missionEndLog?.timeStamp);
|
||||||
|
const duration = Math.round((end.getTime() - start.getTime()) / 1000 / 60); // in minutes
|
||||||
|
return `${duration} Minuten`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as ColumnDef<{
|
||||||
|
Station: Station;
|
||||||
|
Mission: Mission;
|
||||||
|
}>[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -274,10 +274,13 @@ export const DispoStats = async () => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const Stats = ({ stats }: { stats: "pilot" | "dispo" }) => {
|
export const Stats = async ({ stats }: { stats: "pilot" | "dispo" }) => {
|
||||||
|
const session = await getServerSession();
|
||||||
|
if (!session) return null;
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<StatsToggle />
|
{session.user.permissions.includes("DISPO") && <StatsToggle />}
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
{stats === "dispo" && <DispoStats />}
|
{stats === "dispo" && <DispoStats />}
|
||||||
{stats === "pilot" && <PilotStats />}
|
{stats === "pilot" && <PilotStats />}
|
||||||
|
|||||||
@@ -30,30 +30,19 @@ export const StatsToggle = () => {
|
|||||||
return (
|
return (
|
||||||
<header className="flex justify-between items-center p-4">
|
<header className="flex justify-between items-center p-4">
|
||||||
<h1 className="text-2xl font-bold">
|
<h1 className="text-2xl font-bold">
|
||||||
Hallo,{" "}
|
Hallo, {session.status === "authenticated" ? session.data?.user.firstname : "<username>"}
|
||||||
{session.status === "authenticated"
|
|
||||||
? session.data?.user.firstname
|
|
||||||
: "<username>"}
|
|
||||||
{"!"}
|
{"!"}
|
||||||
</h1>
|
</h1>
|
||||||
<div>
|
<div>
|
||||||
<div className="tooltip" data-tip="Disponent / Pilot">
|
<div className="tooltip tooltip-left" data-tip="Disponent / Pilot">
|
||||||
<label className="toggle text-base-content">
|
<label className="toggle text-base-content">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={checked}
|
checked={checked}
|
||||||
onChange={(e) => setChecked(e.target.checked)}
|
onChange={(e) => setChecked(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<Workflow
|
<Workflow className="w-4 h-4" viewBox="0 0 24 24" aria-label="enabled" />
|
||||||
className="w-4 h-4"
|
<PlaneIcon className="w-4 h-4" viewBox="0 0 24 24" aria-label="disabled" />
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-label="enabled"
|
|
||||||
/>
|
|
||||||
<PlaneIcon
|
|
||||||
className="w-4 h-4"
|
|
||||||
viewBox="0 0 24 24"
|
|
||||||
aria-label="disabled"
|
|
||||||
/>
|
|
||||||
</label>
|
</label>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ import {
|
|||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { Button } from "../../../../../_components/ui/Button";
|
import { Button } from "../../../../../_components/ui/Button";
|
||||||
import { Select } from "../../../../../_components/ui/Select";
|
import { Select } from "../../../../../_components/ui/Select";
|
||||||
import { UserOptionalDefaults, UserOptionalDefaultsSchema, UserSchema } from "@repo/db/zod";
|
import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
|
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
|
||||||
import { cn } from "../../../../../../helper/cn";
|
import { cn } from "../../../../../../helper/cn";
|
||||||
@@ -41,10 +41,7 @@ interface ProfileFormProps {
|
|||||||
export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormProps) => {
|
export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const form = useForm<UserOptionalDefaults>({
|
const form = useForm<UserOptionalDefaults>({
|
||||||
defaultValues: {
|
defaultValues: user,
|
||||||
...user,
|
|
||||||
emailVerified: user.emailVerified ?? undefined,
|
|
||||||
},
|
|
||||||
resolver: zodResolver(UserOptionalDefaultsSchema),
|
resolver: zodResolver(UserOptionalDefaultsSchema),
|
||||||
});
|
});
|
||||||
if (!user) return <Error title="User not found" statusCode={404} />;
|
if (!user) return <Error title="User not found" statusCode={404} />;
|
||||||
|
|||||||
@@ -1,37 +1,41 @@
|
|||||||
import { User2 } from 'lucide-react';
|
import { User2 } from "lucide-react";
|
||||||
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||||
|
|
||||||
export default async () => {
|
const AdminUserPage = async () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
showEditButton
|
showEditButton
|
||||||
prismaModel="user"
|
prismaModel="user"
|
||||||
searchFields={['publicId', 'firstname', 'lastname', 'email']}
|
searchFields={["publicId", "firstname", "lastname", "email"]}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'ID',
|
header: "ID",
|
||||||
accessorKey: 'publicId',
|
accessorKey: "publicId",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Vorname',
|
header: "Vorname",
|
||||||
accessorKey: 'firstname',
|
accessorKey: "firstname",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Nachname',
|
header: "Nachname",
|
||||||
accessorKey: 'lastname',
|
accessorKey: "lastname",
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Email',
|
header: "Email",
|
||||||
accessorKey: 'email',
|
accessorKey: "email",
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
leftOfSearch={
|
leftOfSearch={
|
||||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||||
<User2 className="w-5 h-5" /> Benutzer
|
<User2 className="w-5 h-5" /> Benutzer
|
||||||
</p>
|
</p>
|
||||||
}
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
AdminUserPage.displayName = "AdminUserPage";
|
||||||
|
|
||||||
|
export default AdminUserPage;
|
||||||
|
|||||||
@@ -42,7 +42,7 @@ export default async function RootLayout({
|
|||||||
<VerticalNav />
|
<VerticalNav />
|
||||||
|
|
||||||
{/* Scrollbarer Content-Bereich */}
|
{/* Scrollbarer Content-Bereich */}
|
||||||
<div className="flex-grow bg-base-100 p-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
|
<div className="flex-grow bg-base-100 px-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,6 +1,16 @@
|
|||||||
|
"use client";
|
||||||
|
import { Mission, MissionAlertLog, MissionLog, Station } from "@repo/db";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
import { PaginatedTable } from "_components/PaginatedTable";
|
||||||
import { NotebookText } from "lucide-react";
|
import { NotebookText } from "lucide-react";
|
||||||
|
import { useSession } from "next-auth/react";
|
||||||
|
|
||||||
|
const Page = () => {
|
||||||
|
const session = useSession();
|
||||||
|
|
||||||
|
if (!session) return <Error title="Nicht eingelogt" statusCode={401} />;
|
||||||
|
|
||||||
const page = () => {
|
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="col-span-full">
|
<div className="col-span-full">
|
||||||
@@ -9,10 +19,72 @@ const page = () => {
|
|||||||
</p>
|
</p>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6">
|
||||||
<h2 className="text-2xl text-gray-600 ">W.I.P.</h2>
|
<PaginatedTable
|
||||||
|
prismaModel={"missionOnStationUsers"}
|
||||||
|
filter={{
|
||||||
|
userId: session.data?.user?.id ?? "",
|
||||||
|
Mission: {
|
||||||
|
state: "finished",
|
||||||
|
},
|
||||||
|
}}
|
||||||
|
include={{
|
||||||
|
Station: true,
|
||||||
|
User: true,
|
||||||
|
Mission: true,
|
||||||
|
}}
|
||||||
|
columns={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
header: "Station",
|
||||||
|
accessorKey: "Station.name",
|
||||||
|
cell: ({ row }) => row.original.Station.bosCallsign,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Stichwort",
|
||||||
|
accessorKey: "Mission.name",
|
||||||
|
cell: ({ row }) =>
|
||||||
|
`${row.original.Mission.missionKeywordCategory} / ${row.original.Mission.missionKeywordName}`,
|
||||||
|
},
|
||||||
|
|
||||||
|
{
|
||||||
|
header: "Datum",
|
||||||
|
accessorKey: "createdAt",
|
||||||
|
cell: ({ row }) => new Date(row.original.Mission.createdAt).toLocaleDateString(),
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Einsatzlänge",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
const missionStartLogs = row.original.Mission.missionLog.filter(
|
||||||
|
(log) => (log as unknown as MissionLog).type === "alert-log",
|
||||||
|
) as unknown as MissionAlertLog[] | undefined;
|
||||||
|
// Find the first log with a station ID that matches the current row's station ID or use the first log if none match
|
||||||
|
const missionStartLog =
|
||||||
|
missionStartLogs?.find(
|
||||||
|
(log) => log.data.station?.id === row.original.Station.id,
|
||||||
|
) || missionStartLogs?.[0];
|
||||||
|
|
||||||
|
const missionEndLog = row.original.Mission.missionLog.find(
|
||||||
|
(log) => (log as unknown as MissionLog).type === "completed-log",
|
||||||
|
) as unknown as MissionAlertLog | undefined;
|
||||||
|
|
||||||
|
if (!missionStartLog) return "Unbekannt";
|
||||||
|
if (!missionEndLog) return "Unbekannt";
|
||||||
|
|
||||||
|
const start = new Date(missionStartLog.timeStamp);
|
||||||
|
const end = new Date(missionEndLog?.timeStamp);
|
||||||
|
const duration = Math.round((end.getTime() - start.getTime()) / 1000 / 60); // in minutes
|
||||||
|
return `${duration} Minuten`;
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as ColumnDef<{
|
||||||
|
Station: Station;
|
||||||
|
Mission: Mission;
|
||||||
|
}>[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export default page;
|
export default Page;
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
import { ArrowRight, NotebookText } from "lucide-react";
|
|
||||||
import Link from "next/link";
|
|
||||||
import Events from "./_components/Events";
|
import Events from "./_components/Events";
|
||||||
import { Stats } from "./_components/Stats";
|
import { Stats } from "./_components/Stats";
|
||||||
import { Badges } from "./_components/Badges";
|
import { Badges } from "./_components/Badges";
|
||||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||||
import { EmailVerification } from "_components/EmailVerification";
|
import { EmailVerification } from "_components/EmailVerification";
|
||||||
|
import { RecentFlights } from "(app)/_components/RecentFlights";
|
||||||
|
|
||||||
export default async function Home({
|
export default async function Home({
|
||||||
searchParams,
|
searchParams,
|
||||||
@@ -20,16 +19,7 @@ export default async function Home({
|
|||||||
<Stats stats={view} />
|
<Stats stats={view} />
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
<div className="card-body">
|
<RecentFlights />
|
||||||
<h2 className="card-title justify-between">
|
|
||||||
<span className="card-title">
|
|
||||||
<NotebookText className="w-4 h-4" /> Logbook
|
|
||||||
</span>
|
|
||||||
<Link className="badge badge-sm badge-info badge-outline" href="/logbook">
|
|
||||||
Zum vollständigen Logbook <ArrowRight className="w-4 h-4" />
|
|
||||||
</Link>
|
|
||||||
</h2>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
<Badges />
|
<Badges />
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ import {
|
|||||||
LockOpen1Icon,
|
LockOpen1Icon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { UserOptionalDefaults, UserOptionalDefaultsSchema, UserSchema } from "@repo/db/zod";
|
import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
import { Bell, Plane } from "lucide-react";
|
import { Bell, Plane } from "lucide-react";
|
||||||
|
|
||||||
export const ProfileForm = ({ user }: { user: User }) => {
|
export const ProfileForm = ({ user }: { user: User }) => {
|
||||||
@@ -30,6 +30,7 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
email: z.string().email({
|
email: z.string().email({
|
||||||
message: "Bitte gebe eine gültige E-Mail Adresse ein",
|
message: "Bitte gebe eine gültige E-Mail Adresse ein",
|
||||||
}),
|
}),
|
||||||
|
settingsHideLastname: z.boolean().default(false),
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
type IFormInput = z.infer<typeof schema>;
|
type IFormInput = z.infer<typeof schema>;
|
||||||
@@ -39,6 +40,7 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
firstname: user.firstname,
|
firstname: user.firstname,
|
||||||
lastname: user.lastname,
|
lastname: user.lastname,
|
||||||
email: user.email,
|
email: user.email,
|
||||||
|
settingsHideLastname: user.settingsHideLastname,
|
||||||
},
|
},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
@@ -92,7 +94,11 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
{form.formState.errors.lastname && (
|
{form.formState.errors.lastname && (
|
||||||
<p className="text-error">{form.formState.errors.lastname?.message}</p>
|
<p className="text-error">{form.formState.errors.lastname?.message}</p>
|
||||||
)}
|
)}
|
||||||
<label className="floating-label w-full">
|
<label className="label">
|
||||||
|
<input type="checkbox" {...form.register("settingsHideLastname")} className="checkbox" />
|
||||||
|
Initialien des Nachnamens verstecken
|
||||||
|
</label>
|
||||||
|
<label className="floating-label w-full mt-4">
|
||||||
<span className="text-lg flex items-center gap-2">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<EnvelopeClosedIcon /> E-Mail
|
<EnvelopeClosedIcon /> E-Mail
|
||||||
</span>
|
</span>
|
||||||
|
|||||||
@@ -11,7 +11,7 @@
|
|||||||
"generate": "npx prisma generate && npx prisma generate zod",
|
"generate": "npx prisma generate && npx prisma generate zod",
|
||||||
"migrate": "npx prisma migrate dev",
|
"migrate": "npx prisma migrate dev",
|
||||||
"deploy": "npx prisma migrate deploy",
|
"deploy": "npx prisma migrate deploy",
|
||||||
"dev": "npx prisma studio"
|
"dev": "npx prisma studio --browser none"
|
||||||
},
|
},
|
||||||
"exports": {
|
"exports": {
|
||||||
".": "./index.ts",
|
".": "./index.ts",
|
||||||
|
|||||||
@@ -37,4 +37,29 @@ export interface MissionMessageLog {
|
|||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
export type MissionLog = MissionStationLog | MissionMessageLog | MissionSdsLog;
|
export interface MissionAlertLog {
|
||||||
|
type: "alert-log";
|
||||||
|
auto: false;
|
||||||
|
timeStamp: string;
|
||||||
|
data: {
|
||||||
|
station?: Station;
|
||||||
|
vehicle?: string;
|
||||||
|
user: PublicUser;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface MissionCompletedLog {
|
||||||
|
type: "completed-log";
|
||||||
|
auto: boolean;
|
||||||
|
timeStamp: string;
|
||||||
|
data: {
|
||||||
|
user?: PublicUser;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export type MissionLog =
|
||||||
|
| MissionStationLog
|
||||||
|
| MissionMessageLog
|
||||||
|
| MissionSdsLog
|
||||||
|
| MissionAlertLog
|
||||||
|
| MissionCompletedLog;
|
||||||
|
|||||||
@@ -8,13 +8,21 @@ export interface PublicUser {
|
|||||||
fullName: string;
|
fullName: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getPublicUser = (user: User): PublicUser => {
|
export const getPublicUser = (
|
||||||
|
user: User,
|
||||||
|
options = {
|
||||||
|
ignorePrivacy: false,
|
||||||
|
},
|
||||||
|
): PublicUser => {
|
||||||
return {
|
return {
|
||||||
firstname: user.firstname,
|
firstname: user.firstname,
|
||||||
lastname: user.lastname
|
lastname:
|
||||||
.split(" ")
|
user.settingsHideLastname && !options.ignorePrivacy
|
||||||
.map((part) => `${part[0]}.`)
|
? ""
|
||||||
.join(" "), // Only take the first part of the name
|
: user.lastname
|
||||||
|
.split(" ")
|
||||||
|
.map((part) => `${part[0]}.`)
|
||||||
|
.join(" "), // Only take the first part of the name
|
||||||
fullName: `${user.firstname} ${user.lastname
|
fullName: `${user.firstname} ${user.lastname
|
||||||
.split(" ")
|
.split(" ")
|
||||||
.map((part) => `${part[0]}.`)
|
.map((part) => `${part[0]}.`)
|
||||||
|
|||||||
@@ -30,14 +30,15 @@ model User {
|
|||||||
moodleId Int? @map(name: "moodle_id")
|
moodleId Int? @map(name: "moodle_id")
|
||||||
|
|
||||||
// Settings:
|
// Settings:
|
||||||
settingsNtfyRoom String? @map(name: "settings_ntfy_room")
|
settingsNtfyRoom String? @map(name: "settings_ntfy_room")
|
||||||
settingsMicDevice String? @map(name: "settings_mic_device")
|
settingsMicDevice String? @map(name: "settings_mic_device")
|
||||||
settingsMicVolume Int? @map(name: "settings_mic_volume")
|
settingsMicVolume Int? @map(name: "settings_mic_volume")
|
||||||
|
settingsHideLastname Boolean @default(false) @map(name: "settings_hide_lastname")
|
||||||
|
|
||||||
// email Verification:
|
// email Verification:
|
||||||
emailVerificationToken String? @map(name: "email_verification_token")
|
emailVerificationToken String? @map(name: "email_verification_token")
|
||||||
emailVerificationExpiresAt DateTime? @map(name: "email_verification_expires_at")
|
emailVerificationExpiresAt DateTime? @map(name: "email_verification_expires_at")
|
||||||
emailVerified Boolean? @default(false)
|
emailVerified Boolean @default(false)
|
||||||
|
|
||||||
image String?
|
image String?
|
||||||
badges BADGES[] @default([])
|
badges BADGES[] @default([])
|
||||||
|
|||||||
Reference in New Issue
Block a user