saving connection log in DB, added Dispo stats
This commit is contained in:
@@ -1,4 +1,4 @@
|
|||||||
import { getPublicUser, prisma } from "@repo/db";
|
import { getPublicUser, prisma, User } from "@repo/db";
|
||||||
import { pubClient } from "modules/redis";
|
import { pubClient } from "modules/redis";
|
||||||
import { Server, Socket } from "socket.io";
|
import { Server, Socket } from "socket.io";
|
||||||
|
|
||||||
@@ -12,15 +12,40 @@ export const handleConnectDispatch =
|
|||||||
selectedZone: string;
|
selectedZone: string;
|
||||||
}) => {
|
}) => {
|
||||||
try {
|
try {
|
||||||
const userId = socket.data.user.id; // User ID aus dem JWT-Token
|
const user: User = socket.data.user; // User ID aus dem JWT-Token
|
||||||
console.log("User connected to dispatch server");
|
console.log("User connected to dispatch server");
|
||||||
const user = await prisma.user.findUnique({
|
|
||||||
|
if (!user) return Error("User not found");
|
||||||
|
|
||||||
|
if (!user.permissions?.includes("DISPO")) {
|
||||||
|
socket.emit(
|
||||||
|
"error",
|
||||||
|
"You do not have permission to connect to the dispatch server.",
|
||||||
|
);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const existingConnection = await prisma.connectedDispatcher.findFirst({
|
||||||
where: {
|
where: {
|
||||||
id: userId,
|
userId: user.id,
|
||||||
|
logoutTime: null,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return Error("User not found");
|
if (existingConnection) {
|
||||||
|
await io
|
||||||
|
.to(`user:${user.id}`)
|
||||||
|
.emit("force-disconnect", "double-connection");
|
||||||
|
await prisma.connectedDispatcher.updateMany({
|
||||||
|
where: {
|
||||||
|
userId: user.id,
|
||||||
|
logoutTime: null,
|
||||||
|
},
|
||||||
|
data: {
|
||||||
|
logoutTime: new Date().toISOString(),
|
||||||
|
},
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
let parsedLogoffDate = null;
|
let parsedLogoffDate = null;
|
||||||
if (logoffTime.length > 0) {
|
if (logoffTime.length > 0) {
|
||||||
@@ -48,20 +73,14 @@ export const handleConnectDispatch =
|
|||||||
publicUser: getPublicUser(user) as any,
|
publicUser: getPublicUser(user) as any,
|
||||||
esimatedLogoutTime: parsedLogoffDate?.toISOString() || null,
|
esimatedLogoutTime: parsedLogoffDate?.toISOString() || null,
|
||||||
lastHeartbeat: new Date().toISOString(),
|
lastHeartbeat: new Date().toISOString(),
|
||||||
userId: userId,
|
userId: user.id,
|
||||||
loginTime: new Date().toISOString(),
|
loginTime: new Date().toISOString(),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten
|
socket.join("dispatchers"); // Dem Dispatcher-Raum beitreten
|
||||||
socket.join(`user:${userId}`); // Dem User-Raum beitreten
|
socket.join(`user:${user.id}`); // Dem User-Raum beitreten
|
||||||
|
|
||||||
/* const keys = await pubClient.keys("Dispatcher:*");
|
|
||||||
await Promise.all(
|
|
||||||
keys.map(async (key) => {
|
|
||||||
return await pubClient.json.get(key);
|
|
||||||
}),
|
|
||||||
); */
|
|
||||||
io.to("dispatchers").emit("dispatcher-update");
|
io.to("dispatchers").emit("dispatcher-update");
|
||||||
io.to("pilots").emit("dispatcher-update");
|
io.to("pilots").emit("dispatcher-update");
|
||||||
|
|
||||||
|
|||||||
@@ -2,7 +2,8 @@ import { create } from "zustand";
|
|||||||
import { socket } from "../dispatch/socket";
|
import { socket } from "../dispatch/socket";
|
||||||
|
|
||||||
interface ConnectionStore {
|
interface ConnectionStore {
|
||||||
isConnected: boolean;
|
status: "connected" | "disconnected" | "connecting" | "error";
|
||||||
|
message: string;
|
||||||
selectedZone: string;
|
selectedZone: string;
|
||||||
connect: (
|
connect: (
|
||||||
uid: string,
|
uid: string,
|
||||||
@@ -13,10 +14,12 @@ interface ConnectionStore {
|
|||||||
}
|
}
|
||||||
|
|
||||||
export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
||||||
isConnected: false,
|
status: "disconnected",
|
||||||
|
message: "",
|
||||||
selectedZone: "LST_01",
|
selectedZone: "LST_01",
|
||||||
connect: async (uid, selectedZone, logoffTime) =>
|
connect: async (uid, selectedZone, logoffTime) =>
|
||||||
new Promise((resolve) => {
|
new Promise((resolve) => {
|
||||||
|
set({ status: "connecting", message: "" });
|
||||||
socket.auth = { uid };
|
socket.auth = { uid };
|
||||||
set({ selectedZone });
|
set({ selectedZone });
|
||||||
socket.connect();
|
socket.connect();
|
||||||
@@ -34,8 +37,24 @@ export const useDispatchConnectionStore = create<ConnectionStore>((set) => ({
|
|||||||
}));
|
}));
|
||||||
|
|
||||||
socket.on("connect", () => {
|
socket.on("connect", () => {
|
||||||
useDispatchConnectionStore.setState({ isConnected: true });
|
useDispatchConnectionStore.setState({ status: "connected", message: "" });
|
||||||
});
|
});
|
||||||
|
|
||||||
|
socket.on("connect_error", (err) => {
|
||||||
|
useDispatchConnectionStore.setState({
|
||||||
|
status: "error",
|
||||||
|
message: err.message,
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
socket.on("disconnect", () => {
|
socket.on("disconnect", () => {
|
||||||
useDispatchConnectionStore.setState({ isConnected: false });
|
useDispatchConnectionStore.setState({ status: "disconnected", message: "" });
|
||||||
|
});
|
||||||
|
|
||||||
|
socket.on("force-disconnect", (reason: string) => {
|
||||||
|
console.log("force-disconnect", reason);
|
||||||
|
useDispatchConnectionStore.setState({
|
||||||
|
status: "disconnected",
|
||||||
|
message: reason,
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
@@ -39,10 +39,10 @@ export const Chat = () => {
|
|||||||
const data = await getConenctedUsers();
|
const data = await getConenctedUsers();
|
||||||
if (data) {
|
if (data) {
|
||||||
const filteredConnectedUser = data.filter((user) => {
|
const filteredConnectedUser = data.filter((user) => {
|
||||||
/* return (
|
return (
|
||||||
user.userId !== session.data?.user.id &&
|
user.userId !== session.data?.user.id &&
|
||||||
!Object.keys(chats).includes(user.userId)
|
!Object.keys(chats).includes(user.userId)
|
||||||
); */
|
);
|
||||||
|
|
||||||
return true;
|
return true;
|
||||||
});
|
});
|
||||||
@@ -65,8 +65,6 @@ export const Chat = () => {
|
|||||||
};
|
};
|
||||||
}, [addTabValue, chats, session.data?.user.id]);
|
}, [addTabValue, chats, session.data?.user.id]);
|
||||||
|
|
||||||
console.log("connectedUser", connectedUser);
|
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={cn("dropdown dropdown-right", chatOpen && "dropdown-open")}>
|
<div className={cn("dropdown dropdown-right", chatOpen && "dropdown-open")}>
|
||||||
<div className="indicator">
|
<div className="indicator">
|
||||||
|
|||||||
@@ -231,8 +231,6 @@ export const MarkerCluster = () => {
|
|||||||
const avgLng =
|
const avgLng =
|
||||||
allPos.reduce((sum, pos) => sum + pos[1]!, 0) / allPos.length;
|
allPos.reduce((sum, pos) => sum + pos[1]!, 0) / allPos.length;
|
||||||
|
|
||||||
console.log(allPos, { avgLat, avgLng });
|
|
||||||
|
|
||||||
return {
|
return {
|
||||||
...c,
|
...c,
|
||||||
lat: avgLat,
|
lat: avgLat,
|
||||||
|
|||||||
@@ -36,7 +36,7 @@ export const Audio = () => {
|
|||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
const joinRoom = async () => {
|
const joinRoom = async () => {
|
||||||
if (!connection.isConnected) return;
|
if (connection.status != "connected") return;
|
||||||
if (state === "connected") return;
|
if (state === "connected") return;
|
||||||
connect(selectedRoom);
|
connect(selectedRoom);
|
||||||
};
|
};
|
||||||
@@ -46,7 +46,8 @@ export const Audio = () => {
|
|||||||
return () => {
|
return () => {
|
||||||
disconnect();
|
disconnect();
|
||||||
};
|
};
|
||||||
}, [connection.isConnected]);
|
// eslint-disable-next-line react-hooks/exhaustive-deps
|
||||||
|
}, [connection.status]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
|||||||
@@ -14,15 +14,21 @@ export const ConnectionBtn = () => {
|
|||||||
const uid = session.data?.user?.id;
|
const uid = session.data?.user?.id;
|
||||||
if (!uid) return null;
|
if (!uid) return null;
|
||||||
return (
|
return (
|
||||||
<>
|
<div className="rounded-box bg-base-200 flex justify-center items-center gap-2 p-1">
|
||||||
{!connection.isConnected ? (
|
{connection.message.length > 0 && (
|
||||||
|
<span className="mx-2 text-error">{connection.message}</span>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{connection.status === "disconnected" && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-soft btn-info"
|
className="btn btn-sm btn-soft btn-info "
|
||||||
onClick={() => modalRef.current?.showModal()}
|
onClick={() => modalRef.current?.showModal()}
|
||||||
>
|
>
|
||||||
Verbinden
|
Verbinden
|
||||||
</button>
|
</button>
|
||||||
) : (
|
)}
|
||||||
|
|
||||||
|
{connection.status == "connected" && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-soft btn-success"
|
className="btn btn-soft btn-success"
|
||||||
onClick={() => modalRef.current?.showModal()}
|
onClick={() => modalRef.current?.showModal()}
|
||||||
@@ -33,7 +39,7 @@ export const ConnectionBtn = () => {
|
|||||||
|
|
||||||
<dialog ref={modalRef} className="modal">
|
<dialog ref={modalRef} className="modal">
|
||||||
<div className="modal-box flex flex-col items-center justify-center">
|
<div className="modal-box flex flex-col items-center justify-center">
|
||||||
{connection.isConnected ? (
|
{connection.status == "connected" ? (
|
||||||
<h3 className="text-lg font-bold mb-5">
|
<h3 className="text-lg font-bold mb-5">
|
||||||
Verbunden als{" "}
|
Verbunden als{" "}
|
||||||
<span className="text-info">
|
<span className="text-info">
|
||||||
@@ -58,7 +64,7 @@ export const ConnectionBtn = () => {
|
|||||||
className="input w-full"
|
className="input w-full"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{!connection.isConnected && (
|
{connection.status == "disconnected" && (
|
||||||
<p className="fieldset-label">
|
<p className="fieldset-label">
|
||||||
Du kannst diese Zeit später noch anpassen.
|
Du kannst diese Zeit später noch anpassen.
|
||||||
</p>
|
</p>
|
||||||
@@ -67,7 +73,7 @@ export const ConnectionBtn = () => {
|
|||||||
<div className="modal-action flex justify-between w-full">
|
<div className="modal-action flex justify-between w-full">
|
||||||
<form method="dialog" className="w-full flex justify-between">
|
<form method="dialog" className="w-full flex justify-between">
|
||||||
<button className="btn btn-soft">Abbrechen</button>
|
<button className="btn btn-soft">Abbrechen</button>
|
||||||
{connection.isConnected ? (
|
{connection.status == "connected" ? (
|
||||||
<button
|
<button
|
||||||
className="btn btn-soft btn-error"
|
className="btn btn-soft btn-error"
|
||||||
type="submit"
|
type="submit"
|
||||||
@@ -94,7 +100,7 @@ export const ConnectionBtn = () => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
</>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -97,6 +97,7 @@ export const MissionForm = () => {
|
|||||||
});
|
});
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
|
console.log(form.formState.errors);
|
||||||
return (
|
return (
|
||||||
<form className="space-y-4">
|
<form className="space-y-4">
|
||||||
{/* Koorinaten Section */}
|
{/* Koorinaten Section */}
|
||||||
@@ -159,14 +160,14 @@ export const MissionForm = () => {
|
|||||||
<div className="form-control">
|
<div className="form-control">
|
||||||
<h2 className="text-lg font-bold mb-2">Rettungsmittel</h2>
|
<h2 className="text-lg font-bold mb-2">Rettungsmittel</h2>
|
||||||
<Select
|
<Select
|
||||||
name="Rettungsmittel"
|
name="missionStationIds"
|
||||||
label={""}
|
label={""}
|
||||||
placeholder="Wähle ein oder mehrere Rettungsmittel aus"
|
placeholder="Wähle ein oder mehrere Rettungsmittel aus"
|
||||||
isMulti
|
isMulti
|
||||||
form={form}
|
form={form}
|
||||||
options={stations.map((s) => ({
|
options={stations.map((s) => ({
|
||||||
label: s.bosCallsign,
|
label: s.bosCallsign,
|
||||||
value: s.id,
|
value: s.id.toString(),
|
||||||
}))}
|
}))}
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
187
apps/hub/app/(app)/_components/PilotDispoStats.tsx
Normal file
187
apps/hub/app/(app)/_components/PilotDispoStats.tsx
Normal file
@@ -0,0 +1,187 @@
|
|||||||
|
"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,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
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>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -1,25 +0,0 @@
|
|||||||
"use client";
|
|
||||||
|
|
||||||
import { useState } from "react";
|
|
||||||
import Header from "./Header";
|
|
||||||
import Stats from "./stats";
|
|
||||||
|
|
||||||
export default () => {
|
|
||||||
const [isChecked, setIsChecked] = useState(true);
|
|
||||||
|
|
||||||
const handleCheckboxChange = (event: React.ChangeEvent<HTMLInputElement>) => {
|
|
||||||
setIsChecked(event.target.checked);
|
|
||||||
};
|
|
||||||
|
|
||||||
return (
|
|
||||||
<>
|
|
||||||
<Header
|
|
||||||
isChecked={isChecked}
|
|
||||||
handleCheckboxChange={handleCheckboxChange}
|
|
||||||
/>
|
|
||||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
|
||||||
<Stats isChecked={isChecked} />
|
|
||||||
</div>
|
|
||||||
</>
|
|
||||||
);
|
|
||||||
};
|
|
||||||
@@ -1,13 +1,32 @@
|
|||||||
|
"use client";
|
||||||
import { PlaneIcon, Workflow } from "lucide-react";
|
import { PlaneIcon, Workflow } from "lucide-react";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
|
import { useRouter, useSearchParams } from "next/navigation";
|
||||||
|
import { useEffect, useState } from "react";
|
||||||
|
|
||||||
interface HeaderProps {
|
export const StatsToggle = () => {
|
||||||
isChecked: boolean;
|
const [checked, setChecked] = useState(false);
|
||||||
handleCheckboxChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({ isChecked, handleCheckboxChange }: HeaderProps) => {
|
|
||||||
const session = useSession();
|
const session = useSession();
|
||||||
|
const router = useRouter();
|
||||||
|
const searchParams = useSearchParams();
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
const statsPage = searchParams.get("stats") || "pilot";
|
||||||
|
if (statsPage === "dispo") {
|
||||||
|
setChecked(false);
|
||||||
|
} else {
|
||||||
|
setChecked(true);
|
||||||
|
}
|
||||||
|
}, [searchParams]);
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
if (checked) {
|
||||||
|
router.push("/?stats=pilot");
|
||||||
|
} else {
|
||||||
|
router.push("/?stats=dispo");
|
||||||
|
}
|
||||||
|
}, [checked, router]);
|
||||||
|
|
||||||
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">
|
||||||
@@ -22,8 +41,8 @@ export default ({ isChecked, handleCheckboxChange }: HeaderProps) => {
|
|||||||
<label className="toggle text-base-content">
|
<label className="toggle text-base-content">
|
||||||
<input
|
<input
|
||||||
type="checkbox"
|
type="checkbox"
|
||||||
checked={isChecked}
|
checked={checked}
|
||||||
onChange={handleCheckboxChange}
|
onChange={(e) => setChecked(e.target.checked)}
|
||||||
/>
|
/>
|
||||||
<Workflow
|
<Workflow
|
||||||
className="w-4 h-4"
|
className="w-4 h-4"
|
||||||
@@ -1,129 +1,16 @@
|
|||||||
import { PlaneIcon } from "lucide-react";
|
/* import { useState } from "react";
|
||||||
|
import { StatsToggle } from "./StatsToggle"; */
|
||||||
|
import { StatsToggle } from "(app)/_components/StatsToggle";
|
||||||
|
import { DispoStats, PilotStats } from "./PilotDispoStats";
|
||||||
|
|
||||||
interface StatsProps {
|
export const Stats = ({ stats }: { stats: "pilot" | "dispo" }) => {
|
||||||
isChecked: boolean;
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ({ isChecked }: StatsProps) => {
|
|
||||||
return isChecked ? <PilotStats /> : <DispoStats />;
|
|
||||||
};
|
|
||||||
|
|
||||||
export const PilotStats = (): JSX.Element => {
|
|
||||||
return (
|
return (
|
||||||
<div className="stats shadow">
|
<>
|
||||||
<div className="stat">
|
<StatsToggle />
|
||||||
<div className="stat-figure text-primary">
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
|
||||||
<svg
|
{stats === "dispo" && <DispoStats />}
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
{stats === "pilot" && <PilotStats />}
|
||||||
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 = (): JSX.Element => {
|
|
||||||
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">578</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">53h 12m</div>
|
|
||||||
<div className="stat-desc">Mehr als 69% 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 Berlin</div>
|
|
||||||
<div className="stat-title">Wurde von dir am meisten Disponiert</div>
|
|
||||||
<div className="stat-desc text-secondary">
|
|
||||||
43 Stationen warten auf deine Einsätze!
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
</div>
|
</div>
|
||||||
|
</>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
import Logbook from "./_components/Logbook";
|
import Logbook from "./_components/Logbook";
|
||||||
import { ArrowRight, NotebookText, Award, RocketIcon } from "lucide-react";
|
import { ArrowRight, NotebookText } from "lucide-react";
|
||||||
import Link from "next/link";
|
import Link from "next/link";
|
||||||
import Events from "./_components/Events";
|
import Events from "./_components/Events";
|
||||||
import StatsClientWrapper from "./_components/StatsClientWrapper";
|
import { Stats } from "./_components/Stats";
|
||||||
import { Badges } from "./_components/Badges";
|
import { Badges } from "./_components/Badges";
|
||||||
|
|
||||||
/*
|
/*
|
||||||
@@ -15,10 +15,16 @@ Badges
|
|||||||
Aktive Events / Mandatory Events
|
Aktive Events / Mandatory Events
|
||||||
*/
|
*/
|
||||||
|
|
||||||
export default function Home() {
|
export default async function Home({
|
||||||
|
searchParams,
|
||||||
|
}: {
|
||||||
|
searchParams: { stats?: "pilot" | "dispo" };
|
||||||
|
}) {
|
||||||
|
const { stats } = await searchParams;
|
||||||
|
const view = stats || "pilot";
|
||||||
return (
|
return (
|
||||||
<div>
|
<div>
|
||||||
<StatsClientWrapper />
|
<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">
|
||||||
|
|||||||
@@ -19,16 +19,6 @@ services:
|
|||||||
- postgres
|
- postgres
|
||||||
volumes:
|
volumes:
|
||||||
- ./grafana:/var/lib/grafana
|
- ./grafana:/var/lib/grafana
|
||||||
pgadmin:
|
|
||||||
image: dpage/pgadmin4:latest
|
|
||||||
container_name: pgadmin
|
|
||||||
environment:
|
|
||||||
PGADMIN_DEFAULT_EMAIL: dev@var.de
|
|
||||||
PGADMIN_DEFAULT_PASSWORD: dev
|
|
||||||
ports:
|
|
||||||
- "8080:80"
|
|
||||||
depends_on:
|
|
||||||
- postgres
|
|
||||||
redis:
|
redis:
|
||||||
container_name: redis
|
container_name: redis
|
||||||
image: redis/redis-stack:latest
|
image: redis/redis-stack:latest
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user