added reports page

This commit is contained in:
PxlLoewe
2025-05-16 22:49:44 -07:00
parent da5ec8470d
commit 40ca6b1bd9
25 changed files with 1355 additions and 1363 deletions

View File

@@ -1,81 +1,44 @@
"use client";
import { useEffect, useState } from "react";
import { Eye } from "lucide-react";
import { fetchReportDetails, handleMarkAsResolved } from "../actions";
import { ExclamationTriangleIcon } from "@radix-ui/react-icons";
import { prisma } from "@repo/db";
import { Error } from "_components/Error";
import {
ReportAdmin,
ReportSenderInfo,
} from "(app)/admin/report/_components/form";
export default function ReportDetailsPage({
params: paramsPromise,
export default async function ReportDetailsPage({
params,
}: {
params: Promise<{ id: number }>;
params: { id: string };
}) {
const [params, setParams] = useState<{ id: number } | null>(null);
const [report, setReport] = useState<any>(null);
const [loading, setLoading] = useState(true);
const { id } = await params;
useEffect(() => {
async function unwrapParams() {
const resolvedParams = await paramsPromise;
setParams(resolvedParams);
}
unwrapParams();
}, [paramsPromise]);
const report = await prisma.report.findUnique({
where: {
id: Number(id),
},
include: {
Reported: true,
Reviewer: true,
Sender: true,
},
});
useEffect(() => {
if (!params) return;
async function loadReport() {
if (!params) return;
const fetchedReport = await fetchReportDetails(parseInt(params.id));
setReport(fetchedReport);
setLoading(false);
}
loadReport();
}, [params]);
if (!params || loading) return <div>Loading...</div>;
if (!report) return <div>Report not found</div>;
if (!report) return <Error statusCode={404} title="User not found" />;
return (
<div className="p-4">
<h1 className="text-2xl font-bold mb-4 flex items-center gap-2">
<Eye className="w-6 h-6" /> Report Details
</h1>
<div className="grid grid-cols-2 gap-4">
<div>
<p>
<b>Sender:</b> {report.sender.firstname} {report.sender.lastname} (
{report.sender.publicId})
</p>
<p>
<b>Reported:</b> {report.reported.firstname}{" "}
{report.reported.lastname} ({report.reported.publicId})
</p>
<p>
<b>Timestamp:</b> {new Date(report.timestamp).toLocaleString()}
</p>
</div>
<div>
<p>
<b>Message:</b>
</p>
<p>{report.text}</p>
</div>
<div className="grid grid-cols-6 gap-4">
<div className="col-span-full">
<p className="text-2xl font-semibold text-left flex items-center gap-2">
<ExclamationTriangleIcon className="w-5 h-5" />
Report #{report.id}
</p>
</div>
<div className="mt-4 flex gap-2">
<button
className="btn btn-success btn-outline"
onClick={async () => {
await handleMarkAsResolved(report.id);
}}
>
Erledigen
</button>
<button
className="btn btn-primary btn-outline"
onClick={() => window.history.back()}
>
Zurück
</button>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ReportSenderInfo report={report} />
</div>
<div className="card bg-base-200 shadow-xl mb-4 col-span-6 xl:col-span-3">
<ReportAdmin report={report} />
</div>
</div>
);

View File

@@ -0,0 +1,138 @@
"use client";
import { editReport } from "(app)/admin/report/actions";
import { zodResolver } from "@hookform/resolvers/zod";
import { Report as IReport, User } from "@repo/db";
import { ReportSchema, Report as IReportZod } from "@repo/db/zod";
import { Button } from "_components/ui/Button";
import { Switch } from "_components/ui/Switch";
import { Trash } from "lucide-react";
import { useSession } from "next-auth/react";
import Link from "next/link";
import { useRouter } from "next/navigation";
import { useState } from "react";
import { useForm } from "react-hook-form";
import toast from "react-hot-toast";
export const ReportSenderInfo = ({
report,
}: {
report: IReport & {
Reported?: User;
Sender?: User;
};
}) => {
const { Reported, Sender } = report;
return (
<div className="card-body">
<Link
href={`/admin/user/${Reported?.id}`}
className="card-title link link-hover"
>
{Reported?.firstname} {Reported?.lastname} ({Reported?.publicId})
</Link>
<div className="textarea w-full text-left">{report.text}</div>
<Link
href={`/admin/user/${Reported?.id}`}
className="text-sm text-gray-600 text-right link link-hover"
>
Meldet Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}
) am {new Date(report.timestamp).toLocaleString()}
</Link>
</div>
);
};
export const ReportAdmin = ({
report,
}: {
report: IReport & {
Reported?: User;
Sender?: User;
Reviewer?: User | null;
};
}) => {
const { Reviewer } = report;
const [isEditLoading, setIsEditLoading] = useState(false);
const session = useSession();
const router = useRouter();
const form = useForm<IReportZod>({
resolver: zodResolver(ReportSchema),
defaultValues: report,
});
return (
<form
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
setIsEditLoading(true);
const newReport = await editReport(values.id, {
reviewerUserId: session.data?.user.id,
reviewerComment: values.reviewerComment,
reviewed: values.reviewed,
});
form.reset(newReport);
setIsEditLoading(false);
router.refresh();
toast.success("Deine Änderungen wurden gespeichert!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
})}
>
<h2 className="card-title">Staff Kommentar</h2>
<textarea
{...form.register("reviewerComment")}
className="textarea w-full"
placeholder=""
/>
<p className="text-sm text-gray-600 text-right">
{report.Reviewer &&
`Kommentar von ${Reviewer?.firstname} ${Reviewer?.lastname} (${Reviewer?.publicId})`}
</p>
<Switch
form={form}
name="reviewed"
label="Report als geklärt markieren"
/>
<div className="card-actions flex justify-between">
<Button
role="submit"
className="btn-sm btn-wide btn-outline btn-primary"
disabled={!form.formState.isDirty}
isLoading={isEditLoading}
>
Speichern
</Button>
<Button
role="button"
className="btn btn-warning"
onSubmit={() => false}
onClick={async () => {
await editReport(report.id, {
reviewerUserId: null,
reviewerComment: null,
reviewed: false,
});
form.reset({
...report,
reviewerComment: null,
reviewed: false,
});
router.refresh();
toast.success("Kommentar gelöscht!", {
style: {
background: "var(--color-base-100)",
color: "var(--color-base-content)",
},
});
}}
>
<Trash /> Kommentar löschen
</Button>
</div>
</form>
);
};

View File

@@ -1,48 +1,14 @@
"use server";
import { prisma, User } from "@repo/db";
import { Prisma, prisma } from "@repo/db";
export const markAsResolved = async (id: number) => {
await prisma.report.update({
where: { id: id },
data: { erledigt: true },
});
};
// New function to handle marking a report as resolved
export const handleMarkAsResolved = async (id: number) => {
try {
await markAsResolved(id);
return { success: true };
} catch (error) {
console.error("Error marking report as resolved:", error);
return { success: false, error: error };
}
};
export const getReports = async () => {
return prisma.report.findMany({
include: {
sender: true,
reported: true,
},
});
};
export const getUserReports = async (user: User) => {
return prisma.report.findMany({
export const editReport = async (
id: number,
data: Prisma.ReportUncheckedUpdateInput,
) => {
return await prisma.report.update({
where: {
reportedUserId: user.id,
},
include: {
sender: true,
reported: true,
id: id,
},
});
};
export const fetchReportDetails = async (id: number) => {
return prisma.report.findUnique({
where: { id },
include: { sender: true, reported: true },
data,
});
};

View File

@@ -1,122 +1,71 @@
"use client";
import { useEffect, useState } from "react";
import { Check, Eye, X } from "lucide-react";
import Link from "next/link";
import { getReports, handleMarkAsResolved } from "./actions";
import { PaginatedTable } from "_components/PaginatedTable";
import { Report, User } from "@repo/db";
import { ColumnDef } from "@tanstack/react-table";
export default function ReportPage() {
const [reports, setReports] = useState<
{
id: number;
timestamp: string;
erledigt: boolean;
sender: {
id: string;
firstname: string;
lastname: string;
publicId: string;
};
reported: {
id: string;
firstname: string;
lastname: string;
publicId: string;
};
}[]
>([]);
useEffect(() => {
const fetchReports = async () => {
const reps = await getReports();
const transformedReports = reps.map((report) => ({
id: report.id,
timestamp: report.timestamp.toISOString(),
erledigt: report.erledigt,
sender: {
id: report.sender.id,
firstname: report.sender.firstname,
lastname: report.sender.lastname,
publicId: report.sender.publicId,
},
reported: {
id: report.reported.id,
firstname: report.reported.firstname,
lastname: report.reported.lastname,
publicId: report.reported.publicId,
},
}));
setReports(transformedReports);
};
fetchReports();
}, []);
return (
<>
<div className="flex items-center gap-2 mb-4">
<Eye className="w-5 h-5" />{" "}
<span className="text-lg font-bold">Reports</span>
</div>
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Erledigt</th>
<th>Sender</th>
<th>Reported</th>
<th>Time</th>
<th>ID</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr key={report.id}>
<td className="text-center">
{report.erledigt ? (
<PaginatedTable
prismaModel="report"
include={{
Sender: true,
Reported: true,
}}
columns={
[
{
accessorKey: "reviewed",
header: "Erledigt",
cell: ({ row }) => {
return (
<div className="text-center">
{row.getValue("reviewed") ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</td>
<td>{`${report.sender.firstname} ${report.sender.lastname} (${report.sender.publicId})`}</td>
<td>{`${report.reported.firstname} ${report.reported.lastname} (${report.reported.publicId})`}</td>
<td>{new Date(report.timestamp).toLocaleString()}</td>
<td>{report.id}</td>
<td>
<div className="flex gap-2">
<Link href={`/admin/report/${report.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
{!report.erledigt && (
<button
className="btn btn-sm btn-outline btn-success flex items-center gap-2"
onClick={async () => {
const result = await handleMarkAsResolved(report.id);
if (result.success) {
setReports((prevReports) =>
prevReports.map((r) =>
r.id === report.id
? { ...r, erledigt: true }
: r,
),
);
} else {
alert("Error: " + result.error);
}
}}
>
<Check className="w-4 h-4" /> Erledigen
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
</>
</div>
);
},
},
{
accessorKey: "Sender",
header: "Sender",
cell: ({ row }) => {
const user = row.getValue("Sender") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "Reported",
header: "Reported",
cell: ({ row }) => {
const user = row.getValue("Reported") as User;
return `${user.firstname} ${user.lastname} (${user.publicId})`;
},
},
{
accessorKey: "timestamp",
header: "Time",
cell: ({ row }) =>
new Date(row.getValue("timestamp")).toLocaleString(),
},
{
accessorKey: "actions",
header: "Actions",
cell: ({ row }) => (
<Link href={`/admin/report/${row.original.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
),
},
] as ColumnDef<Report>[]
}
/>
);
}

View File

@@ -1,47 +1,49 @@
import { DatabaseBackupIcon } from 'lucide-react';
import { PaginatedTable } from '../../../_components/PaginatedTable';
import Link from 'next/link';
import { DatabaseBackupIcon } from "lucide-react";
import { PaginatedTable } from "../../../_components/PaginatedTable";
import Link from "next/link";
export default () => {
return (
<>
<PaginatedTable
showEditButton
prismaModel="station"
searchFields={['bosCallsign', 'bosUse', 'country', 'operator']}
columns={[
{
header: 'BOS Name',
accessorKey: 'bosCallsign',
},
{
header: 'Bos Use',
accessorKey: 'bosUse',
},
{
header: 'Country',
accessorKey: 'country',
},
{
header: 'operator',
accessorKey: 'operator',
},
]}
leftOfSearch={
<span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
</span>
}
rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
<Link href={'/admin/station/new'}>
<button className="btn btn-sm btn-outline btn-primary">
Erstellen
</button>
</Link>
</p>
}
/>
</>
);
const page = () => {
return (
<>
<PaginatedTable
showEditButton
prismaModel="station"
searchFields={["bosCallsign", "bosUse", "country", "operator"]}
columns={[
{
header: "BOS Name",
accessorKey: "bosCallsign",
},
{
header: "Bos Use",
accessorKey: "bosUse",
},
{
header: "Country",
accessorKey: "country",
},
{
header: "operator",
accessorKey: "operator",
},
]}
leftOfSearch={
<span className="flex items-center gap-2">
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
</span>
}
rightOfSearch={
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
<Link href={"/admin/station/new"}>
<button className="btn btn-sm btn-outline btn-primary">
Erstellen
</button>
</Link>
</p>
}
/>
</>
);
};
export default page;

View File

@@ -1,98 +0,0 @@
import { User } from "@repo/db";
import { Check, Eye, X } from "lucide-react";
import Link from "next/link";
import { useEffect, useState } from "react";
import { getUserReports, handleMarkAsResolved } from "../../../report/actions";
export default function UserReports({ user }: { user: User }) {
const [reports, setReports] = useState<
{
id: number;
timestamp: string;
erledigt: boolean;
sender: {
id: string;
firstname: string;
lastname: string;
publicId: string;
};
}[]
>([]);
useEffect(() => {
const fetchReports = async (user: User) => {
const reps = await getUserReports(user);
const transformedReports = reps.map((report) => ({
id: report.id,
timestamp: report.timestamp.toISOString(),
erledigt: report.erledigt,
sender: {
id: report.sender.id,
firstname: report.sender.firstname,
lastname: report.sender.lastname,
publicId: report.sender.publicId,
},
}));
setReports(transformedReports);
};
fetchReports(user);
}, [user]);
return (
<div className="overflow-x-auto">
<table className="table table-zebra w-full">
<thead>
<tr>
<th>Erledigt</th>
<th>Sender</th>
<th>Time</th>
<th>Actions</th>
</tr>
</thead>
<tbody>
{reports.map((report) => (
<tr key={report.id}>
<td className="text-center">
{report.erledigt ? (
<Check className="text-green-500 w-5 h-5" />
) : (
<X className="text-red-500 w-5 h-5" />
)}
</td>
<td>{`${report.sender.firstname} ${report.sender.lastname} (${report.sender.publicId})`}</td>
<td>{new Date(report.timestamp).toLocaleString()}</td>
<td>
<div className="flex gap-2">
<Link href={`/admin/report/${report.id}`}>
<button className="btn btn-sm btn-outline btn-info flex items-center gap-2">
<Eye className="w-4 h-4" /> Anzeigen
</button>
</Link>
{!report.erledigt && (
<button
className="btn btn-sm btn-outline btn-success flex items-center gap-2"
onClick={async () => {
const result = await handleMarkAsResolved(report.id);
if (result.success) {
setReports((prevReports) =>
prevReports.map((r) =>
r.id === report.id ? { ...r, erledigt: true } : r,
),
);
} else {
alert("Error: " + result.error);
}
}}
>
<Check className="w-4 h-4" /> Erledigen
</button>
)}
</div>
</td>
</tr>
))}
</tbody>
</table>
</div>
);
}

View File

@@ -24,7 +24,6 @@ import { min } from "date-fns";
import { cn } from "../../../../../../helper/cn";
import { ChartBarBigIcon, PlaneIcon } from "lucide-react";
import Link from "next/link";
import UserReports from "./UserReports";
interface ProfileFormProps {
user: User;
@@ -420,7 +419,7 @@ export const AdminForm = ({ user, dispoTime, pilotTime }: AdminFormProps) => {
<h2 className="card-title">
<ExclamationTriangleIcon className="w-5 h-5" /> Reports
</h2>
<UserReports user={user} />
{/* TODO: Report summary Here */}
</div>
);
};

View File

@@ -1,10 +1,9 @@
import { PersonIcon } from "@radix-ui/react-icons";
import { PrismaClient, User } from "@repo/db";
import { prisma, User } from "@repo/db";
import { AdminForm, ConnectionHistory, ProfileForm } from "./_components/forms";
import { Error } from "../../../../_components/Error";
const Page = async ({ params }: { params: { id: string } }) => {
const prisma = new PrismaClient();
const { id } = await params;
const user: User | null = await prisma.user.findUnique({