Added Pilot Stats in Admin view
This commit is contained in:
@@ -79,6 +79,10 @@ export const DispoStats = async () => {
|
|||||||
not: null,
|
not: null,
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
select: {
|
||||||
|
loginTime: true,
|
||||||
|
logoutTime: true,
|
||||||
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
const mostDispatchedStationIds = await prisma.mission.groupBy({
|
const mostDispatchedStationIds = await prisma.mission.groupBy({
|
||||||
|
|||||||
@@ -1,10 +1,9 @@
|
|||||||
"use client";
|
"use client";
|
||||||
import { zodResolver } from "@hookform/resolvers/zod";
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { BADGES, PERMISSION, User } from "@repo/db";
|
import { BADGES, PERMISSION, User } from "@repo/db";
|
||||||
import { useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from "zod";
|
import { deleteDispoHistory, editUser, resetPassword } from "../../action";
|
||||||
import { editUser, resetPassword } from "../../action";
|
|
||||||
import { toast } from "react-hot-toast";
|
import { toast } from "react-hot-toast";
|
||||||
import {
|
import {
|
||||||
PersonIcon,
|
PersonIcon,
|
||||||
@@ -14,18 +13,23 @@ import {
|
|||||||
LightningBoltIcon,
|
LightningBoltIcon,
|
||||||
LockOpen1Icon,
|
LockOpen1Icon,
|
||||||
HobbyKnifeIcon,
|
HobbyKnifeIcon,
|
||||||
HeartIcon,
|
|
||||||
} 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 { UserSchema } from "@repo/db/zod";
|
import { UserSchema } from "@repo/db/zod";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
|
||||||
|
import { min } from "date-fns";
|
||||||
|
import { cn } from "../../../../../../helper/cn";
|
||||||
|
import { ChartBarBigIcon, PlaneIcon } from "lucide-react";
|
||||||
|
|
||||||
interface ProfileFormProps {
|
interface ProfileFormProps {
|
||||||
user: User;
|
user: User;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
export const ProfileForm: React.FC<ProfileFormProps> = ({
|
||||||
|
user,
|
||||||
|
}: ProfileFormProps) => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
const form = useForm<User>({
|
const form = useForm<User>({
|
||||||
defaultValues: user,
|
defaultValues: user,
|
||||||
@@ -135,7 +139,163 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }) => {
|
|||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
|
export const ConnectionHistory: React.FC<{ user: User }> = ({
|
||||||
|
user,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
}) => {
|
||||||
|
const dispoTableRef = useRef<PaginatedTableRef>(null);
|
||||||
|
return (
|
||||||
|
<div className="card-body flex-row flex-wrap">
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<MixerHorizontalIcon className="w-5 h-5" /> Dispo-Verbindungs Historie
|
||||||
|
</h2>
|
||||||
|
<PaginatedTable
|
||||||
|
ref={dispoTableRef}
|
||||||
|
filter={{
|
||||||
|
userId: user.id,
|
||||||
|
}}
|
||||||
|
prismaModel={"connectedDispatcher"}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="flex-1">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<PlaneIcon className="w-5 h-5" /> Pilot-Verbindungs Historie
|
||||||
|
</h2>
|
||||||
|
<PaginatedTable
|
||||||
|
ref={dispoTableRef}
|
||||||
|
filter={{
|
||||||
|
userId: user.id,
|
||||||
|
}}
|
||||||
|
prismaModel={"connectedAircraft"}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
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
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
interface AdminFormProps {
|
||||||
|
user: User;
|
||||||
|
dispoTime: {
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
lastLogin?: Date;
|
||||||
|
};
|
||||||
|
pilotTime: {
|
||||||
|
hours: number;
|
||||||
|
minutes: number;
|
||||||
|
lastLogin?: Date;
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AdminForm = ({ user, dispoTime, pilotTime }: AdminFormProps) => {
|
||||||
const router = useRouter();
|
const router = useRouter();
|
||||||
|
|
||||||
return (
|
return (
|
||||||
@@ -201,6 +361,37 @@ export const AdminForm: React.FC<ProfileFormProps> = ({ user }) => {
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
<h2 className="card-title">
|
||||||
|
<ChartBarBigIcon className="w-5 h-5" /> Aktivität
|
||||||
|
</h2>
|
||||||
|
<div className="stats shadow">
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-primary">
|
||||||
|
<LightningBoltIcon className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="stat-value text-primary">
|
||||||
|
{dispoTime.hours}h {dispoTime.minutes}min
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">Dispo Zeit</div>
|
||||||
|
<div className="stat-desc text-secondary">
|
||||||
|
{dispoTime.lastLogin &&
|
||||||
|
new Date(dispoTime.lastLogin).toLocaleString("de-DE")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="stat">
|
||||||
|
<div className="stat-figure text-primary">
|
||||||
|
<PlaneIcon className="w-8 h-8" />
|
||||||
|
</div>
|
||||||
|
<div className="stat-value text-primary">
|
||||||
|
{pilotTime.hours}h {pilotTime.minutes}min
|
||||||
|
</div>
|
||||||
|
<div className="stat-title">Pilot Zeit</div>
|
||||||
|
<div className="stat-desc text-secondary">
|
||||||
|
{pilotTime.lastLogin &&
|
||||||
|
new Date(pilotTime.lastLogin).toLocaleString("de-DE")}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
import { PersonIcon } from "@radix-ui/react-icons";
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
import { PrismaClient, User } from "@repo/db";
|
import { PrismaClient, User } from "@repo/db";
|
||||||
import { AdminForm, ProfileForm } from "./_components/forms";
|
import { AdminForm, ConnectionHistory, ProfileForm } from "./_components/forms";
|
||||||
import { Error } from "../../../../_components/Error";
|
import { Error } from "../../../../_components/Error";
|
||||||
|
|
||||||
export default async ({ params }: { params: { id: string } }) => {
|
const Page = async ({ params }: { params: { id: string } }) => {
|
||||||
const prisma = new PrismaClient();
|
const prisma = new PrismaClient();
|
||||||
const { id } = await params;
|
const { id } = await params;
|
||||||
|
|
||||||
@@ -12,6 +12,56 @@ export default async ({ params }: { params: { id: string } }) => {
|
|||||||
id: id,
|
id: id,
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
|
||||||
|
const dispoSessions = await prisma.connectedDispatcher.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user?.id,
|
||||||
|
logoutTime: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
loginTime: true,
|
||||||
|
logoutTime: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
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 dispoTime = {
|
||||||
|
hours: Math.floor(totalDispoTime / (1000 * 60 * 60)),
|
||||||
|
minutes: Math.floor((totalDispoTime % (1000 * 60 * 60)) / (1000 * 60)),
|
||||||
|
lastLogin: dispoSessions[dispoSessions.length - 1]?.loginTime,
|
||||||
|
};
|
||||||
|
|
||||||
|
const pilotSessions = await prisma.connectedAircraft.findMany({
|
||||||
|
where: {
|
||||||
|
userId: user?.id,
|
||||||
|
logoutTime: {
|
||||||
|
not: null,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
select: {
|
||||||
|
loginTime: true,
|
||||||
|
logoutTime: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const totalPilotTime = pilotSessions.reduce((acc, session) => {
|
||||||
|
const logoffTime = new Date(session.logoutTime!).getTime();
|
||||||
|
const logonTime = new Date(session.loginTime).getTime();
|
||||||
|
return acc + (logoffTime - logonTime);
|
||||||
|
}, 0);
|
||||||
|
|
||||||
|
const pilotTime = {
|
||||||
|
hours: Math.floor(totalPilotTime / (1000 * 60 * 60)),
|
||||||
|
minutes: Math.floor((totalPilotTime % (1000 * 60 * 60)) / (1000 * 60)),
|
||||||
|
lastLogin: pilotSessions[pilotSessions.length - 1]?.loginTime,
|
||||||
|
};
|
||||||
|
|
||||||
if (!user) return <Error statusCode={404} title="User not found" />;
|
if (!user) return <Error statusCode={404} title="User not found" />;
|
||||||
return (
|
return (
|
||||||
<div className="grid grid-cols-6 gap-4">
|
<div className="grid grid-cols-6 gap-4">
|
||||||
@@ -25,8 +75,13 @@ export default async ({ params }: { params: { id: string } }) => {
|
|||||||
<ProfileForm user={user} />
|
<ProfileForm user={user} />
|
||||||
</div>
|
</div>
|
||||||
<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">
|
||||||
<AdminForm user={user} />
|
<AdminForm user={user} dispoTime={dispoTime} pilotTime={pilotTime} />
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-6">
|
||||||
|
<ConnectionHistory user={user} />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export default Page;
|
||||||
|
|||||||
@@ -36,3 +36,11 @@ export const resetPassword = async (id: string) => {
|
|||||||
|
|
||||||
return { password };
|
return { password };
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const deleteDispoHistory = async (id: string) => {
|
||||||
|
return await prisma.connectedDispatcher.deleteMany({
|
||||||
|
where: {
|
||||||
|
userId: id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|||||||
Binary file not shown.
Reference in New Issue
Block a user