saving connection log in DB, added Dispo stats
This commit is contained in:
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 { useSession } from "next-auth/react";
|
||||
import { useRouter, useSearchParams } from "next/navigation";
|
||||
import { useEffect, useState } from "react";
|
||||
|
||||
interface HeaderProps {
|
||||
isChecked: boolean;
|
||||
handleCheckboxChange: (event: React.ChangeEvent<HTMLInputElement>) => void;
|
||||
}
|
||||
|
||||
export default ({ isChecked, handleCheckboxChange }: HeaderProps) => {
|
||||
export const StatsToggle = () => {
|
||||
const [checked, setChecked] = useState(false);
|
||||
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 (
|
||||
<header className="flex justify-between items-center p-4">
|
||||
<h1 className="text-2xl font-bold">
|
||||
@@ -22,8 +41,8 @@ export default ({ isChecked, handleCheckboxChange }: HeaderProps) => {
|
||||
<label className="toggle text-base-content">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={isChecked}
|
||||
onChange={handleCheckboxChange}
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked(e.target.checked)}
|
||||
/>
|
||||
<Workflow
|
||||
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 {
|
||||
isChecked: boolean;
|
||||
}
|
||||
|
||||
export default ({ isChecked }: StatsProps) => {
|
||||
return isChecked ? <PilotStats /> : <DispoStats />;
|
||||
};
|
||||
|
||||
export const PilotStats = (): JSX.Element => {
|
||||
export const Stats = ({ stats }: { stats: "pilot" | "dispo" }) => {
|
||||
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>
|
||||
<>
|
||||
<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>
|
||||
|
||||
<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>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
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 Events from "./_components/Events";
|
||||
import StatsClientWrapper from "./_components/StatsClientWrapper";
|
||||
import { Stats } from "./_components/Stats";
|
||||
import { Badges } from "./_components/Badges";
|
||||
|
||||
/*
|
||||
@@ -15,10 +15,16 @@ Badges
|
||||
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 (
|
||||
<div>
|
||||
<StatsClientWrapper />
|
||||
<Stats stats={view} />
|
||||
|
||||
<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">
|
||||
|
||||
Reference in New Issue
Block a user