added logbook
This commit is contained in:
80
apps/hub/app/(app)/_components/RecentFlights.tsx
Normal file
80
apps/hub/app/(app)/_components/RecentFlights.tsx
Normal file
@@ -0,0 +1,80 @@
|
||||
"use client";
|
||||
import { Mission, MissionAlertLog, MissionLog, Station } from "@repo/db";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { PaginatedTable } from "_components/PaginatedTable";
|
||||
import { ArrowRight, NotebookText } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
import Link from "next/link";
|
||||
|
||||
export const RecentFlights = () => {
|
||||
const session = useSession();
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title justify-between">
|
||||
<span className="card-title">
|
||||
<NotebookText className="w-4 h-4" /> Logbook
|
||||
</span>
|
||||
<Link className="badge badge-sm badge-info badge-outline" href="/logbook">
|
||||
Zum vollständigen Logbook <ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</h2>
|
||||
<PaginatedTable
|
||||
prismaModel={"missionOnStationUsers"}
|
||||
filter={{
|
||||
userId: session.data?.user?.id ?? "",
|
||||
Mission: {
|
||||
state: "finished",
|
||||
},
|
||||
}}
|
||||
include={{
|
||||
Station: true,
|
||||
User: true,
|
||||
Mission: true,
|
||||
}}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
header: "Station",
|
||||
accessorKey: "Station.name",
|
||||
cell: ({ row }) => row.original.Station.bosCallsign,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Datum",
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => new Date(row.original.Mission.createdAt).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
header: "Einsatzlänge",
|
||||
cell: ({ row }) => {
|
||||
const missionStartLogs = row.original.Mission.missionLog.filter(
|
||||
(log) => (log as unknown as MissionLog).type === "alert-log",
|
||||
) as unknown as MissionAlertLog[] | undefined;
|
||||
// Find the first log with a station ID that matches the current row's station ID or use the first log if none match
|
||||
const missionStartLog =
|
||||
missionStartLogs?.find(
|
||||
(log) => log.data.station?.id === row.original.Station.id,
|
||||
) || missionStartLogs?.[0];
|
||||
|
||||
const missionEndLog = row.original.Mission.missionLog.find(
|
||||
(log) => (log as unknown as MissionLog).type === "completed-log",
|
||||
) as unknown as MissionAlertLog | undefined;
|
||||
|
||||
if (!missionStartLog) return "Unbekannt";
|
||||
if (!missionEndLog) return "Unbekannt";
|
||||
|
||||
const start = new Date(missionStartLog.timeStamp);
|
||||
const end = new Date(missionEndLog?.timeStamp);
|
||||
const duration = Math.round((end.getTime() - start.getTime()) / 1000 / 60); // in minutes
|
||||
return `${duration} Minuten`;
|
||||
},
|
||||
},
|
||||
] as ColumnDef<{
|
||||
Station: Station;
|
||||
Mission: Mission;
|
||||
}>[]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
@@ -274,10 +274,13 @@ export const DispoStats = async () => {
|
||||
);
|
||||
};
|
||||
|
||||
export const Stats = ({ stats }: { stats: "pilot" | "dispo" }) => {
|
||||
export const Stats = async ({ stats }: { stats: "pilot" | "dispo" }) => {
|
||||
const session = await getServerSession();
|
||||
if (!session) return null;
|
||||
|
||||
return (
|
||||
<>
|
||||
<StatsToggle />
|
||||
{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 />}
|
||||
|
||||
@@ -30,30 +30,19 @@ export const StatsToggle = () => {
|
||||
return (
|
||||
<header className="flex justify-between items-center p-4">
|
||||
<h1 className="text-2xl font-bold">
|
||||
Hallo,{" "}
|
||||
{session.status === "authenticated"
|
||||
? session.data?.user.firstname
|
||||
: "<username>"}
|
||||
Hallo, {session.status === "authenticated" ? session.data?.user.firstname : "<username>"}
|
||||
{"!"}
|
||||
</h1>
|
||||
<div>
|
||||
<div className="tooltip" data-tip="Disponent / Pilot">
|
||||
<div className="tooltip tooltip-left" data-tip="Disponent / Pilot">
|
||||
<label className="toggle text-base-content">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={checked}
|
||||
onChange={(e) => setChecked(e.target.checked)}
|
||||
/>
|
||||
<Workflow
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="enabled"
|
||||
/>
|
||||
<PlaneIcon
|
||||
className="w-4 h-4"
|
||||
viewBox="0 0 24 24"
|
||||
aria-label="disabled"
|
||||
/>
|
||||
<Workflow className="w-4 h-4" viewBox="0 0 24 24" aria-label="enabled" />
|
||||
<PlaneIcon className="w-4 h-4" viewBox="0 0 24 24" aria-label="disabled" />
|
||||
</label>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -25,7 +25,7 @@ import {
|
||||
} from "@radix-ui/react-icons";
|
||||
import { Button } from "../../../../../_components/ui/Button";
|
||||
import { Select } from "../../../../../_components/ui/Select";
|
||||
import { UserOptionalDefaults, UserOptionalDefaultsSchema, UserSchema } from "@repo/db/zod";
|
||||
import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { PaginatedTable, PaginatedTableRef } from "_components/PaginatedTable";
|
||||
import { cn } from "../../../../../../helper/cn";
|
||||
@@ -41,10 +41,7 @@ interface ProfileFormProps {
|
||||
export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormProps) => {
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const form = useForm<UserOptionalDefaults>({
|
||||
defaultValues: {
|
||||
...user,
|
||||
emailVerified: user.emailVerified ?? undefined,
|
||||
},
|
||||
defaultValues: user,
|
||||
resolver: zodResolver(UserOptionalDefaultsSchema),
|
||||
});
|
||||
if (!user) return <Error title="User not found" statusCode={404} />;
|
||||
|
||||
@@ -1,37 +1,41 @@
|
||||
import { User2 } from 'lucide-react';
|
||||
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||
import { User2 } from "lucide-react";
|
||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
||||
|
||||
export default async () => {
|
||||
return (
|
||||
<>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="user"
|
||||
searchFields={['publicId', 'firstname', 'lastname', 'email']}
|
||||
columns={[
|
||||
{
|
||||
header: 'ID',
|
||||
accessorKey: 'publicId',
|
||||
},
|
||||
{
|
||||
header: 'Vorname',
|
||||
accessorKey: 'firstname',
|
||||
},
|
||||
{
|
||||
header: 'Nachname',
|
||||
accessorKey: 'lastname',
|
||||
},
|
||||
{
|
||||
header: 'Email',
|
||||
accessorKey: 'email',
|
||||
},
|
||||
]}
|
||||
leftOfSearch={
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<User2 className="w-5 h-5" /> Benutzer
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
const AdminUserPage = async () => {
|
||||
return (
|
||||
<>
|
||||
<PaginatedTable
|
||||
showEditButton
|
||||
prismaModel="user"
|
||||
searchFields={["publicId", "firstname", "lastname", "email"]}
|
||||
columns={[
|
||||
{
|
||||
header: "ID",
|
||||
accessorKey: "publicId",
|
||||
},
|
||||
{
|
||||
header: "Vorname",
|
||||
accessorKey: "firstname",
|
||||
},
|
||||
{
|
||||
header: "Nachname",
|
||||
accessorKey: "lastname",
|
||||
},
|
||||
{
|
||||
header: "Email",
|
||||
accessorKey: "email",
|
||||
},
|
||||
]}
|
||||
leftOfSearch={
|
||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||
<User2 className="w-5 h-5" /> Benutzer
|
||||
</p>
|
||||
}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
AdminUserPage.displayName = "AdminUserPage";
|
||||
|
||||
export default AdminUserPage;
|
||||
|
||||
@@ -42,7 +42,7 @@ export default async function RootLayout({
|
||||
<VerticalNav />
|
||||
|
||||
{/* Scrollbarer Content-Bereich */}
|
||||
<div className="flex-grow bg-base-100 p-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
|
||||
<div className="flex-grow bg-base-100 px-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -1,6 +1,16 @@
|
||||
"use client";
|
||||
import { Mission, MissionAlertLog, MissionLog, Station } from "@repo/db";
|
||||
import { ColumnDef } from "@tanstack/react-table";
|
||||
import { Error } from "_components/Error";
|
||||
import { PaginatedTable } from "_components/PaginatedTable";
|
||||
import { NotebookText } from "lucide-react";
|
||||
import { useSession } from "next-auth/react";
|
||||
|
||||
const Page = () => {
|
||||
const session = useSession();
|
||||
|
||||
if (!session) return <Error title="Nicht eingelogt" statusCode={401} />;
|
||||
|
||||
const page = () => {
|
||||
return (
|
||||
<div className="grid grid-cols-6 gap-4">
|
||||
<div className="col-span-full">
|
||||
@@ -9,10 +19,72 @@ const page = () => {
|
||||
</p>
|
||||
</div>
|
||||
<div className="card bg-base-200 shadow-xl mb-4 col-span-6">
|
||||
<h2 className="text-2xl text-gray-600 ">W.I.P.</h2>
|
||||
<PaginatedTable
|
||||
prismaModel={"missionOnStationUsers"}
|
||||
filter={{
|
||||
userId: session.data?.user?.id ?? "",
|
||||
Mission: {
|
||||
state: "finished",
|
||||
},
|
||||
}}
|
||||
include={{
|
||||
Station: true,
|
||||
User: true,
|
||||
Mission: true,
|
||||
}}
|
||||
columns={
|
||||
[
|
||||
{
|
||||
header: "Station",
|
||||
accessorKey: "Station.name",
|
||||
cell: ({ row }) => row.original.Station.bosCallsign,
|
||||
},
|
||||
{
|
||||
header: "Stichwort",
|
||||
accessorKey: "Mission.name",
|
||||
cell: ({ row }) =>
|
||||
`${row.original.Mission.missionKeywordCategory} / ${row.original.Mission.missionKeywordName}`,
|
||||
},
|
||||
|
||||
{
|
||||
header: "Datum",
|
||||
accessorKey: "createdAt",
|
||||
cell: ({ row }) => new Date(row.original.Mission.createdAt).toLocaleDateString(),
|
||||
},
|
||||
{
|
||||
header: "Einsatzlänge",
|
||||
cell: ({ row }) => {
|
||||
const missionStartLogs = row.original.Mission.missionLog.filter(
|
||||
(log) => (log as unknown as MissionLog).type === "alert-log",
|
||||
) as unknown as MissionAlertLog[] | undefined;
|
||||
// Find the first log with a station ID that matches the current row's station ID or use the first log if none match
|
||||
const missionStartLog =
|
||||
missionStartLogs?.find(
|
||||
(log) => log.data.station?.id === row.original.Station.id,
|
||||
) || missionStartLogs?.[0];
|
||||
|
||||
const missionEndLog = row.original.Mission.missionLog.find(
|
||||
(log) => (log as unknown as MissionLog).type === "completed-log",
|
||||
) as unknown as MissionAlertLog | undefined;
|
||||
|
||||
if (!missionStartLog) return "Unbekannt";
|
||||
if (!missionEndLog) return "Unbekannt";
|
||||
|
||||
const start = new Date(missionStartLog.timeStamp);
|
||||
const end = new Date(missionEndLog?.timeStamp);
|
||||
const duration = Math.round((end.getTime() - start.getTime()) / 1000 / 60); // in minutes
|
||||
return `${duration} Minuten`;
|
||||
},
|
||||
},
|
||||
] as ColumnDef<{
|
||||
Station: Station;
|
||||
Mission: Mission;
|
||||
}>[]
|
||||
}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default page;
|
||||
export default Page;
|
||||
|
||||
@@ -1,10 +1,9 @@
|
||||
import { ArrowRight, NotebookText } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
import Events from "./_components/Events";
|
||||
import { Stats } from "./_components/Stats";
|
||||
import { Badges } from "./_components/Badges";
|
||||
import { getServerSession } from "api/auth/[...nextauth]/auth";
|
||||
import { EmailVerification } from "_components/EmailVerification";
|
||||
import { RecentFlights } from "(app)/_components/RecentFlights";
|
||||
|
||||
export default async function Home({
|
||||
searchParams,
|
||||
@@ -20,16 +19,7 @@ export default async function Home({
|
||||
<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">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title justify-between">
|
||||
<span className="card-title">
|
||||
<NotebookText className="w-4 h-4" /> Logbook
|
||||
</span>
|
||||
<Link className="badge badge-sm badge-info badge-outline" href="/logbook">
|
||||
Zum vollständigen Logbook <ArrowRight className="w-4 h-4" />
|
||||
</Link>
|
||||
</h2>
|
||||
</div>
|
||||
<RecentFlights />
|
||||
</div>
|
||||
<Badges />
|
||||
</div>
|
||||
|
||||
@@ -20,7 +20,7 @@ import {
|
||||
LockOpen1Icon,
|
||||
} from "@radix-ui/react-icons";
|
||||
import toast from "react-hot-toast";
|
||||
import { UserOptionalDefaults, UserOptionalDefaultsSchema, UserSchema } from "@repo/db/zod";
|
||||
import { UserOptionalDefaults, UserOptionalDefaultsSchema } from "@repo/db/zod";
|
||||
import { Bell, Plane } from "lucide-react";
|
||||
|
||||
export const ProfileForm = ({ user }: { user: User }) => {
|
||||
@@ -30,6 +30,7 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
||||
email: z.string().email({
|
||||
message: "Bitte gebe eine gültige E-Mail Adresse ein",
|
||||
}),
|
||||
settingsHideLastname: z.boolean().default(false),
|
||||
});
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
type IFormInput = z.infer<typeof schema>;
|
||||
@@ -39,6 +40,7 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
||||
firstname: user.firstname,
|
||||
lastname: user.lastname,
|
||||
email: user.email,
|
||||
settingsHideLastname: user.settingsHideLastname,
|
||||
},
|
||||
resolver: zodResolver(schema),
|
||||
});
|
||||
@@ -92,7 +94,11 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
||||
{form.formState.errors.lastname && (
|
||||
<p className="text-error">{form.formState.errors.lastname?.message}</p>
|
||||
)}
|
||||
<label className="floating-label w-full">
|
||||
<label className="label">
|
||||
<input type="checkbox" {...form.register("settingsHideLastname")} className="checkbox" />
|
||||
Initialien des Nachnamens verstecken
|
||||
</label>
|
||||
<label className="floating-label w-full mt-4">
|
||||
<span className="text-lg flex items-center gap-2">
|
||||
<EnvelopeClosedIcon /> E-Mail
|
||||
</span>
|
||||
|
||||
Reference in New Issue
Block a user