Files
var-monorepo/apps/hub/app/(app)/_components/Stats.tsx
2025-06-30 16:38:57 +02:00

293 lines
7.7 KiB
TypeScript

import { StatsToggle } from "(app)/_components/StatsToggle";
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 = session.user;
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));
const totalFlownMissionsPercent = ((ownRankMissionsFlown * 100) / totalUserCount).toFixed(0);
return (
<div className="stats shadow flex flex-row items-stretch">
<div className="stat flex-1 min-w-0">
<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{" "}
{!isNaN(Number(totalFlownMissionsPercent)) ? totalFlownMissionsPercent : 0}%!
</div>
</div>
<div className="stat flex-1 min-w-0">
<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 w-full">
<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 = session.user;
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 flex flex-row items-stretch">
<div className="stat flex-1 min-w-0">
<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>
<div className="stat flex-1 min-w-0">
<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}min
</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 = async ({ stats }: { stats: "pilot" | "dispo" }) => {
const session = await getServerSession();
if (!session) return null;
return (
<>
{session.user.permissions.includes("DISPO") && <StatsToggle />}
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
{stats === "dispo" && <DispoStats />}
{stats === "pilot" && <PilotStats />}
</div>
</>
);
};