changes pilot socket to reperate pilto socket, added pilot stats

This commit is contained in:
PxlLoewe
2025-05-17 23:51:04 -07:00
parent 16e05a08a6
commit 6b58f564b2
16 changed files with 583 additions and 352 deletions

View File

@@ -32,7 +32,6 @@ const router = Router();
router.get("/token", async (req, res) => { router.get("/token", async (req, res) => {
const roomName = req.query.roomName as string; const roomName = req.query.roomName as string;
console.log("roomName", roomName);
res.send({ res.send({
token: await createToken(roomName), token: await createToken(roomName),
}); });

View File

@@ -80,4 +80,86 @@ router.delete("/:id", async (req, res) => {
} }
}); });
// Send mission
router.post("/:id/send-alert", async (req, res) => {
const { id } = req.params;
try {
const mission = await prisma.mission.findUnique({
where: { id: Number(id) },
});
const Stations = await prisma.station.findMany({
where: {
id: {
in: mission?.missionStationIds,
},
},
});
if (!mission) {
res.status(404).json({ error: "Mission not found" });
return;
}
const connectedAircrafts = await prisma.connectedAircraft.findMany({
where: {
stationId: {
in: mission.missionStationIds,
},
logoutTime: null,
},
});
for (const aircraft of connectedAircrafts) {
console.log(`Sending mission to: station:${aircraft.stationId}`);
io.to(`station:${aircraft.stationId}`).emit("mission-alert", {
...mission,
Stations,
});
const existingMissionOnStationUser =
await prisma.missionOnStationUsers.findFirst({
where: {
missionId: mission.id,
userId: aircraft.userId,
stationId: aircraft.stationId,
},
});
if (!existingMissionOnStationUser)
await prisma.missionOnStationUsers.create({
data: {
missionId: mission.id,
userId: aircraft.userId,
stationId: aircraft.stationId,
},
});
}
// for statistics only
await prisma.missionsOnStations
.createMany({
data: mission.missionStationIds.map((stationId) => ({
missionId: mission.id,
stationId,
})),
})
.catch(() => {
// Ignore if the entry already exists
});
await prisma.mission.update({
where: { id: Number(id) },
data: {
state: "running",
},
});
res.status(200).json({
message: `Einsatz gesendet (${connectedAircrafts.length} Nutzer) `,
});
io.to("dispatchers").emit("update-mission", mission);
} catch (error) {
console.error(error);
res.status(500).json({ error: "Failed to send mission" });
}
});
export default router; export default router;

View File

@@ -14,6 +14,8 @@ export const handleConnectPilot =
const user = socket.data.user; // User ID aus dem JWT-Token const user = socket.data.user; // User ID aus dem JWT-Token
const userId = socket.data.user.id; // User ID aus dem JWT-Token const userId = socket.data.user.id; // User ID aus dem JWT-Token
console.log("Pilot connected:", userId);
if (!user) return Error("User not found"); if (!user) return Error("User not found");
const existingConnection = await prisma.connectedAircraft.findFirst({ const existingConnection = await prisma.connectedAircraft.findFirst({
@@ -62,8 +64,6 @@ export const handleConnectPilot =
userId: userId, userId: userId,
loginTime: new Date().toISOString(), loginTime: new Date().toISOString(),
stationId: parseInt(stationId), stationId: parseInt(stationId),
/* user: { connect: { id: userId } }, // Ensure the user relationship is set
station: { connect: { id: stationId } }, // Ensure the station relationship is set */
}, },
}); });
@@ -71,6 +71,8 @@ export const handleConnectPilot =
socket.join(`user:${userId}`); // Join the user-specific room socket.join(`user:${userId}`); // Join the user-specific room
socket.join(`station:${stationId}`); // Join the station-specific room socket.join(`station:${stationId}`); // Join the station-specific room
console.log(`Pilot in: station:${stationId}`);
io.to("dispatchers").emit("pilots-update"); io.to("dispatchers").emit("pilots-update");
io.to("pilots").emit("pilots-update"); io.to("pilots").emit("pilots-update");
@@ -83,14 +85,16 @@ export const handleConnectPilot =
socket.on("disconnect", async () => { socket.on("disconnect", async () => {
console.log("Disconnected from dispatch server"); console.log("Disconnected from dispatch server");
await prisma.connectedAircraft.update({ await prisma.connectedAircraft
where: { .update({
id: connectedAircraftEntry.id, where: {
}, id: connectedAircraftEntry.id,
data: { },
logoutTime: new Date().toISOString(), data: {
}, logoutTime: new Date().toISOString(),
}); },
})
.catch(console.error);
io.to("dispatchers").emit("pilots-update"); io.to("dispatchers").emit("pilots-update");
io.to("pilots").emit("pilots-update"); io.to("pilots").emit("pilots-update");
}); });

View File

@@ -1,12 +1,17 @@
import { create } from "zustand"; import { create } from "zustand";
import { dispatchSocket } from "../../dispatch/socket"; import { dispatchSocket } from "../../dispatch/socket";
import { Station } from "@repo/db"; import { Mission, Station } from "@repo/db";
import { pilotSocket } from "pilot/socket"; import { pilotSocket } from "pilot/socket";
interface ConnectionStore { interface ConnectionStore {
status: "connected" | "disconnected" | "connecting" | "error"; status: "connected" | "disconnected" | "connecting" | "error";
message: string; message: string;
selectedStation: Station | null; selectedStation: Station | null;
activeMission:
| (Mission & {
Stations: Station[];
})
| null;
connect: ( connect: (
uid: string, uid: string,
stationId: string, stationId: string,
@@ -20,13 +25,14 @@ export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
status: "disconnected", status: "disconnected",
message: "", message: "",
selectedStation: null, selectedStation: null,
activeMission: null,
connect: async (uid, stationId, logoffTime, station) => connect: async (uid, stationId, logoffTime, station) =>
new Promise((resolve) => { new Promise((resolve) => {
set({ status: "connecting", message: "", selectedStation: station }); set({ status: "connecting", message: "", selectedStation: station });
dispatchSocket.auth = { uid }; pilotSocket.auth = { uid };
dispatchSocket.connect(); pilotSocket.connect();
dispatchSocket.once("connect", () => { pilotSocket.once("connect", () => {
dispatchSocket.emit("connect-pilot", { pilotSocket.emit("connect-pilot", {
logoffTime, logoffTime,
stationId, stationId,
}); });
@@ -34,30 +40,36 @@ export const usePilotConnectionStore = create<ConnectionStore>((set) => ({
}); });
}), }),
disconnect: () => { disconnect: () => {
dispatchSocket.disconnect(); pilotSocket.disconnect();
}, },
})); }));
dispatchSocket.on("connect", () => { pilotSocket.on("connect", () => {
pilotSocket.disconnect(); dispatchSocket.disconnect();
usePilotConnectionStore.setState({ status: "connected", message: "" }); usePilotConnectionStore.setState({ status: "connected", message: "" });
}); });
dispatchSocket.on("connect_error", (err) => { pilotSocket.on("connect_error", (err) => {
usePilotConnectionStore.setState({ usePilotConnectionStore.setState({
status: "error", status: "error",
message: err.message, message: err.message,
}); });
}); });
dispatchSocket.on("disconnect", () => { pilotSocket.on("disconnect", () => {
usePilotConnectionStore.setState({ status: "disconnected", message: "" }); usePilotConnectionStore.setState({ status: "disconnected", message: "" });
}); });
dispatchSocket.on("force-disconnect", (reason: string) => { pilotSocket.on("force-disconnect", (reason: string) => {
console.log("force-disconnect", reason); console.log("force-disconnect", reason);
usePilotConnectionStore.setState({ usePilotConnectionStore.setState({
status: "disconnected", status: "disconnected",
message: reason, message: reason,
}); });
}); });
pilotSocket.on("mission-alert", (data) => {
usePilotConnectionStore.setState({
activeMission: data,
});
});

View File

@@ -1,9 +1,7 @@
"use client"; "use client";
import React, { useState } from "react"; import React, { useState } from "react";
import { FMS_STATUS_TEXT_COLORS } from "../AircraftMarker"; import { FMS_STATUS_TEXT_COLORS } from "../AircraftMarker";
/* import { Select } from "_components/Select"; import { toast } from "react-hot-toast";
import { Station } from "@repo/db";
import { getStations } from "dispatch/_components/pannel/action"; */
import { import {
Ban, Ban,
BellRing, BellRing,
@@ -29,7 +27,11 @@ import {
import { usePannelStore } from "_store/pannelStore"; import { usePannelStore } from "_store/pannelStore";
import { useSession } from "next-auth/react"; import { useSession } from "next-auth/react";
import { useMutation, useQueryClient } from "@tanstack/react-query"; import { useMutation, useQueryClient } from "@tanstack/react-query";
import { deleteMissionAPI, editMissionAPI } from "querys/missions"; import {
deleteMissionAPI,
editMissionAPI,
sendMissionAPI,
} from "querys/missions";
const Einsatzdetails = ({ mission }: { mission: Mission }) => { const Einsatzdetails = ({ mission }: { mission: Mission }) => {
const queryClient = useQueryClient(); const queryClient = useQueryClient();
@@ -42,6 +44,20 @@ const Einsatzdetails = ({ mission }: { mission: Mission }) => {
}); });
}, },
}); });
const sendAlertMutation = useMutation({
mutationKey: ["missions"],
mutationFn: sendMissionAPI,
onError: (error) => {
console.error(error);
toast.error("Fehler beim Alarmieren");
},
onSuccess: (data) => {
toast.success(data.message);
queryClient.invalidateQueries({
queryKey: ["missions"],
});
},
});
const { setMissionFormValues, setOpen } = usePannelStore((state) => state); const { setMissionFormValues, setOpen } = usePannelStore((state) => state);
return ( return (
<div className="p-4 text-base-content"> <div className="p-4 text-base-content">
@@ -76,7 +92,10 @@ const Einsatzdetails = ({ mission }: { mission: Mission }) => {
</div> </div>
<div className="divider mt-0 mb-0" /> <div className="divider mt-0 mb-0" />
<div className="flex items-center gap-2 w-full"> <div className="flex items-center gap-2 w-full">
<button className="btn btn-sm btn-info btn-outline flex-3"> <button
className="btn btn-sm btn-info btn-outline flex-3"
onClick={() => sendAlertMutation.mutate(mission.id)}
>
<span className="flex items-center gap-2"> <span className="flex items-center gap-2">
<BellRing size={16} /> Alarmieren <BellRing size={16} /> Alarmieren
</span> </span>

View File

@@ -176,7 +176,7 @@ export const MissionForm = () => {
form={form} form={form}
options={stations?.map((s) => ({ options={stations?.map((s) => ({
label: s.bosCallsign, label: s.bosCallsign,
value: s.id.toString(), value: s.id,
}))} }))}
/> />
</div> </div>

View File

@@ -1,4 +1,4 @@
import { pilotSocket } from "pilot/socket"; "use client";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
export const dispatchSocket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, { export const dispatchSocket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {

View File

@@ -1,14 +1,12 @@
"use client"; "use client";
import { Pannel } from "dispatch/_components/pannel/Pannel";
import { usePannelStore } from "_store/pannelStore";
import { cn } from "helpers/cn";
import dynamic from "next/dynamic";
import { Chat } from "../_components/left/Chat"; import { Chat } from "../_components/left/Chat";
import { Report } from "../_components/left/Report"; import { Report } from "../_components/left/Report";
import { usePilotConnectionStore } from "_store/pilot/connectionStore";
const DispatchPage = () => { const DispatchPage = () => {
const { isOpen } = usePannelStore(); const { activeMission } = usePilotConnectionStore();
return ( return (
<div className="relative flex-1 flex transition-all duration-500 ease w-full"> <div className="relative flex-1 flex transition-all duration-500 ease w-full">
{/* <MapToastCard2 /> */} {/* <MapToastCard2 /> */}
@@ -20,14 +18,7 @@ const DispatchPage = () => {
</div> </div>
</div> </div>
</div> </div>
<div <div>{JSON.stringify(activeMission)}</div>
className={cn(
"absolute right-0 w-[500px] z-999 transition-transform",
isOpen ? "translate-x-0" : "translate-x-full",
)}
>
<Pannel />
</div>
</div> </div>
); );
}; };

View File

@@ -1,5 +1,6 @@
"use client";
import { io } from "socket.io-client"; import { io } from "socket.io-client";
import { dispatchSocket } from "dispatch/socket";
export const pilotSocket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, { export const pilotSocket = io(process.env.NEXT_PUBLIC_DISPATCH_SERVER_URL, {
autoConnect: false, autoConnect: false,

View File

@@ -27,6 +27,13 @@ export const editMissionAPI = async (
return respone.data; return respone.data;
}; };
export const sendMissionAPI = async (id: number) => {
const respone = await serverApi.post<{
message: string;
}>(`/mission/${id}/send-alert`);
return respone.data;
};
export const deleteMissionAPI = async (id: number) => { export const deleteMissionAPI = async (id: number) => {
await serverApi.delete(`/mission/${id}`); await serverApi.delete(`/mission/${id}`);
}; };

View File

@@ -1,191 +0,0 @@
"use server";
import { prisma } from "@repo/db";
import { getServerSession } from "api/auth/[...nextauth]/auth";
import { PlaneIcon } from "lucide-react";
export const PilotStats = async () => {
return (
<div className="stats shadow">
<div className="stat">
<div className="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<div className="stat-title">Einsätze geflogen</div>
<div className="stat-value text-primary">127</div>
<div className="stat-desc">Du bist damit unter den top 5%!</div>
</div>
<div className="stat">
<div className="stat-figure text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div className="stat-title">Pilot Login Zeit</div>
<div className="stat-value text-secondary">35h 12m</div>
<div className="stat-desc">Mehr als 58% aller anderen User!</div>
</div>
<div className="stat">
<div className="stat-figure text-info">
<PlaneIcon className="w-8 h-8" />
</div>
<div className="stat-value text-info">Christoph 31</div>
<div className="stat-title">
War bisher dein Rettungsmittel der Wahl
</div>
<div className="stat-desc text-secondary">
87 Stationen warten noch auf dich!
</div>
</div>
</div>
);
};
export const DispoStats = async () => {
const session = await getServerSession();
if (!session) return null;
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});
const dispoSessions = await prisma.connectedDispatcher.findMany({
where: {
userId: user?.id,
logoutTime: {
not: null,
},
},
select: {
loginTime: true,
logoutTime: true,
},
});
const mostDispatchedStationIds = await prisma.mission.groupBy({
where: {
createdUserId: user?.id,
},
by: ["missionStationIds"],
orderBy: {
_count: {
missionStationIds: "desc",
},
},
take: 1,
_count: {
missionStationIds: true,
},
});
let mostDispatchedStation = null;
if (mostDispatchedStationIds[0]?.missionStationIds[0]) {
mostDispatchedStation = await prisma.station.findUnique({
where: {
id: parseInt(mostDispatchedStationIds[0]?.missionStationIds[0]),
},
});
}
const totalDispatchedMissions = await prisma.mission.count({
where: {
createdUserId: user?.id,
},
});
const totalDispoTime = dispoSessions.reduce((acc, session) => {
const logoffTime = new Date(session.logoutTime!).getTime();
const logonTime = new Date(session.loginTime).getTime();
return acc + (logoffTime - logonTime);
}, 0);
const hours = Math.floor(totalDispoTime / (1000 * 60 * 60));
const minutes = Math.floor((totalDispoTime % (1000 * 60 * 60)) / (1000 * 60));
return (
<div className="stats shadow">
<div className="stat">
<div className="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<div className="stat-title">Einsätze disponiert</div>
<div className="stat-value text-primary">{totalDispatchedMissions}</div>
<div className="stat-desc">Du bist damit unter den top 9%!</div>
</div>
<div className="stat">
<div className="stat-figure text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div className="stat-title">Disponent Login Zeit</div>
<div className="stat-value text-secondary">
{hours}h {minutes}m
</div>
<div className="stat-desc">Mehr als 69% aller anderen User!</div>
</div>
{mostDispatchedStation && (
<div className="stat">
<div className="stat-figure text-info">
<PlaneIcon className="w-8 h-8" />
</div>
<div className="stat-value text-info">
{mostDispatchedStation?.bosCallsign}
</div>
<div className="stat-title">Wurde von dir am meisten Disponiert</div>
<div className="stat-desc text-secondary">
{mostDispatchedStationIds[0]?._count.missionStationIds} Einsätze
</div>
</div>
)}
</div>
);
};

View File

@@ -1,7 +1,292 @@
/* import { useState } from "react";
import { StatsToggle } from "./StatsToggle"; */
import { StatsToggle } from "(app)/_components/StatsToggle"; import { StatsToggle } from "(app)/_components/StatsToggle";
import { DispoStats, PilotStats } from "./PilotDispoStats";
import { prisma } from "@repo/db";
import { getServerSession } from "api/auth/[...nextauth]/auth";
import { PlaneIcon } from "lucide-react";
export const PilotStats = async () => {
const session = await getServerSession();
if (!session) return null;
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});
const mostFlownStationsIds = await prisma.missionOnStationUsers.groupBy({
where: {
userId: user?.id,
},
by: ["stationId"],
orderBy: {
_count: {
stationId: "desc",
},
},
take: 1,
_count: {
stationId: true,
},
});
let mostFlownStation = null;
if (mostFlownStationsIds[0]?.stationId) {
mostFlownStation = await prisma.station.findUnique({
where: {
id: mostFlownStationsIds[0]?.stationId,
},
});
}
const dispoSessions = await prisma.connectedAircraft.findMany({
where: {
userId: user?.id,
logoutTime: {
not: null,
},
},
select: {
loginTime: true,
logoutTime: true,
},
});
const totalFlownMissions = await prisma.missionOnStationUsers.count({
where: {
userId: user?.id,
},
});
// Get the user's rank by missions flown
const missionsFlownRanks = await prisma.missionOnStationUsers.groupBy({
by: ["userId"],
_count: { userId: true },
orderBy: { _count: { userId: "desc" } },
});
const ownRankMissionsFlown =
missionsFlownRanks.findIndex((rank) => rank.userId === user?.id) + 1;
const totalUserCount = await prisma.user.count({
where: {
NOT: {
id: user?.id,
},
},
});
const totalStationsCount = await prisma.station.count();
const unflownStationsCount = totalStationsCount - mostFlownStationsIds.length;
const totalPilotTime = dispoSessions.reduce((acc, session) => {
const logoffTime = new Date(session.logoutTime!).getTime();
const logonTime = new Date(session.loginTime).getTime();
return acc + (logoffTime - logonTime);
}, 0);
const hours = Math.floor(totalPilotTime / (1000 * 60 * 60));
const minutes = Math.floor((totalPilotTime % (1000 * 60 * 60)) / (1000 * 60));
return (
<div className="stats shadow">
<div className="stat">
<div className="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<div className="stat-title">Einsätze geflogen</div>
<div className="stat-value text-primary">{totalFlownMissions}</div>
<div className="stat-desc">
Du bist damit unter den top{" "}
{((ownRankMissionsFlown * 100) / totalUserCount).toFixed(0)}%!
</div>
</div>
<div className="stat">
<div className="stat-figure text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div className="stat-title">Pilot Login Zeit</div>
<div className="stat-value text-secondary">
{hours}h {minutes}min
</div>
</div>
{mostFlownStation && (
<div className="stat">
<div className="stat-figure text-info">
<PlaneIcon className="w-8 h-8" />
</div>
<div className="stat-value text-info">
{mostFlownStation?.bosCallsign}
</div>
<div className="stat-title">
War bisher dein Rettungsmittel der Wahl
</div>
{unflownStationsCount > 0 && (
<div className="stat-desc text-secondary">
{unflownStationsCount}{" "}
{unflownStationsCount > 1 ? "Stationen" : "Station"} warten noch
auf dich!
</div>
)}
{unflownStationsCount === 0 && (
<div className="stat-desc text-secondary">
Du hast alle Stationen geflogen! Krass...
</div>
)}
</div>
)}
</div>
);
};
export const DispoStats = async () => {
const session = await getServerSession();
if (!session) return null;
const user = await prisma.user.findUnique({
where: { id: session.user.id },
});
const dispoSessions = await prisma.connectedDispatcher.findMany({
where: {
userId: user?.id,
logoutTime: {
not: null,
},
},
select: {
loginTime: true,
logoutTime: true,
},
});
const mostDispatchedStationIds = await prisma.mission.groupBy({
where: {
createdUserId: user?.id,
},
by: ["missionStationIds"],
orderBy: {
_count: {
missionStationIds: "desc",
},
},
take: 1,
_count: {
missionStationIds: true,
},
});
let mostDispatchedStation = null;
if (mostDispatchedStationIds[0]?.missionStationIds[0]) {
mostDispatchedStation = await prisma.station.findUnique({
where: {
id: mostDispatchedStationIds[0]?.missionStationIds[0],
},
});
}
const totalDispatchedMissions = await prisma.mission.count({
where: {
createdUserId: user?.id,
},
});
const totalDispoTime = dispoSessions.reduce((acc, session) => {
const logoffTime = new Date(session.logoutTime!).getTime();
const logonTime = new Date(session.loginTime).getTime();
return acc + (logoffTime - logonTime);
}, 0);
const hours = Math.floor(totalDispoTime / (1000 * 60 * 60));
const minutes = Math.floor((totalDispoTime % (1000 * 60 * 60)) / (1000 * 60));
return (
<div className="stats shadow">
<div className="stat">
<div className="stat-figure text-primary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M4.318 6.318a4.5 4.5 0 000 6.364L12 20.364l7.682-7.682a4.5 4.5 0 00-6.364-6.364L12 7.636l-1.318-1.318a4.5 4.5 0 00-6.364 0z"
></path>
</svg>
</div>
<div className="stat-title">Einsätze disponiert</div>
<div className="stat-value text-primary">{totalDispatchedMissions}</div>
<div className="stat-desc">Du bist damit unter den top 9%!</div>
</div>
<div className="stat">
<div className="stat-figure text-secondary">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
className="inline-block h-8 w-8 stroke-current"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
strokeWidth="2"
d="M13 10V3L4 14h7v7l9-11h-7z"
></path>
</svg>
</div>
<div className="stat-title">Disponent Login Zeit</div>
<div className="stat-value text-secondary">
{hours}h {minutes}m
</div>
</div>
{mostDispatchedStation && (
<div className="stat">
<div className="stat-figure text-info">
<PlaneIcon className="w-8 h-8" />
</div>
<div className="stat-value text-info">
{mostDispatchedStation?.bosCallsign}
</div>
<div className="stat-title">Wurde von dir am meisten Disponiert</div>
<div className="stat-desc text-secondary">
{mostDispatchedStationIds[0]?._count.missionStationIds} Einsätze
</div>
</div>
)}
</div>
);
};
export const Stats = ({ stats }: { stats: "pilot" | "dispo" }) => { export const Stats = ({ stats }: { stats: "pilot" | "dispo" }) => {
return ( return (

View File

@@ -1,9 +1,22 @@
"use client"; "use client";
import { zodResolver } from "@hookform/resolvers/zod"; import { zodResolver } from "@hookform/resolvers/zod";
import { BADGES, PERMISSION, Report, User } from "@repo/db"; import {
BADGES,
ConnectedAircraft,
ConnectedDispatcher,
PERMISSION,
Report,
Station,
User,
} from "@repo/db";
import { useRef, useState } from "react"; import { useRef, useState } from "react";
import { useForm } from "react-hook-form"; import { useForm } from "react-hook-form";
import { deleteDispoHistory, editUser, resetPassword } from "../../action"; import {
deleteDispoHistory,
deletePilotHistory,
editUser,
resetPassword,
} from "../../action";
import { toast } from "react-hot-toast"; import { toast } from "react-hot-toast";
import { import {
PersonIcon, PersonIcon,
@@ -166,59 +179,60 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
userId: user.id, userId: user.id,
}} }}
prismaModel={"connectedDispatcher"} prismaModel={"connectedDispatcher"}
columns={[ columns={
{ [
accessorKey: "loginTime", {
header: "Login", accessorKey: "loginTime",
cell: ({ row }) => { header: "Login",
return new Date(row.getValue("loginTime")).toLocaleString( cell: ({ row }) => {
"de-DE", return new Date(row.getValue("loginTime")).toLocaleString(
); "de-DE",
);
},
}, },
}, {
{ header: "Time Online",
header: "Time Online", cell: ({ row }) => {
cell: ({ row }) => { if (row.original.logoutTime == null) {
console.log(row.original); return <span className="text-success">Online</span>;
const loginTime = new Date(row.original.loginTime).getTime(); }
const logoutTime = new Date( const loginTime = new Date(row.original.loginTime).getTime();
(row.original as any).logoutTime, const logoutTime = new Date(
).getTime(); row.original.logoutTime,
const timeOnline = logoutTime - loginTime; ).getTime();
const hours = Math.floor(timeOnline / 1000 / 60 / 60); const timeOnline = logoutTime - loginTime;
const minutes = Math.floor((timeOnline / 1000 / 60) % 60);
if ((row.original as any).logoutTime == null) { const hours = Math.floor(timeOnline / 1000 / 60 / 60);
return <span className="text-success">Online</span>; const minutes = Math.floor((timeOnline / 1000 / 60) % 60);
}
return ( return (
<span className={cn(hours > 2 && "text-error")}> <span className={cn(hours > 2 && "text-error")}>
{hours}h {minutes}min {hours}h {minutes}min
</span> </span>
); );
},
}, },
}, {
{ header: "Aktionen",
header: "Aktionen", cell: ({ row }) => {
cell: ({ row }) => { return (
return ( <div>
<div> <button
<button className="btn btn-sm btn-error"
className="btn btn-sm btn-error" onClick={async () => {
onClick={async () => { await deleteDispoHistory(row.original.id);
await deleteDispoHistory((row.original as any).id); dispoTableRef.current?.refresh();
dispoTableRef.current?.refresh(); }}
}} >
> löschen
löschen </button>
</button> </div>
</div> );
); },
}, },
}, ] as ColumnDef<ConnectedDispatcher>[]
]} }
/> />
</div> </div>
<div className="flex-1"> <div className="flex-1">
@@ -232,73 +246,73 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({
}} }}
prismaModel={"connectedAircraft"} prismaModel={"connectedAircraft"}
include={{ Station: true }} include={{ Station: true }}
columns={[ columns={
{ [
accessorKey: "Station.bosCallsign", {
header: "Station", accessorKey: "Station.bosCallsign",
cell: ({ row }) => { header: "Station",
return ( cell: ({ row }) => {
<Link return (
className="link link-hover" <Link
href={`/admin/station/${(row.original as any).id}`} className="link link-hover"
> href={`/admin/station/${row.original.id}`}
{(row.original as any).Station.bosCallsign}
</Link>
);
},
},
{
accessorKey: "loginTime",
header: "Login",
cell: ({ row }) => {
return new Date(row.getValue("loginTime")).toLocaleString(
"de-DE",
);
},
},
{
header: "Time Online",
cell: ({ row }) => {
console.log(row.original);
const loginTime = new Date(row.original.loginTime).getTime();
const logoutTime = new Date(
(row.original as any).logoutTime,
).getTime();
const timeOnline = logoutTime - loginTime;
const hours = Math.floor(timeOnline / 1000 / 60 / 60);
const minutes = Math.floor((timeOnline / 1000 / 60) % 60);
if ((row.original as any).logoutTime == null) {
return <span className="text-success">Online</span>;
}
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 deleteDispoHistory((row.original as any).id);
dispoTableRef.current?.refresh();
}}
> >
löschen {row.original.Station.bosCallsign}
</button> </Link>
</div> );
); },
}, },
}, {
]} accessorKey: "loginTime",
header: "Login",
cell: ({ row }) => {
return new Date(row.getValue("loginTime")).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 }>[]
}
/> />
</div> </div>
</div> </div>

View File

@@ -44,3 +44,11 @@ export const deleteDispoHistory = async (id: number) => {
}, },
}); });
}; };
export const deletePilotHistory = async (id: number) => {
return await prisma.connectedAircraft.delete({
where: {
id: id,
},
});
};

Binary file not shown.

View File

@@ -13,7 +13,7 @@ model Mission {
missionKeywordAbbreviation String? missionKeywordAbbreviation String?
missionPatientInfo String missionPatientInfo String
missionAdditionalInfo String missionAdditionalInfo String
missionStationIds String[] @default([]) missionStationIds Int[] @default([])
missionStationUserIds String[] @default([]) missionStationUserIds String[] @default([])
missionLog Json[] @default([]) missionLog Json[] @default([])
hpgMissionString String? hpgMissionString String?