Merge pull request #127 from VAR-Virtual-Air-Rescue/staging
Bugfixes + Manuelle reports
This commit was merged in pull request #127.
This commit is contained in:
2
.github/workflows/deploy-staging.yml
vendored
2
.github/workflows/deploy-staging.yml
vendored
@@ -40,7 +40,7 @@ jobs:
|
||||
username: ${{ secrets.SSH_USERNAME }}
|
||||
password: ${{ secrets.SSH_PASSWORD }}
|
||||
port: 22
|
||||
command_timeout: 30m
|
||||
command_timeout: 60m
|
||||
script: |
|
||||
export NVM_DIR="$HOME/.nvm"
|
||||
source "$NVM_DIR/nvm.sh"
|
||||
|
||||
@@ -25,7 +25,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["livekit-rooms"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
refetchInterval: 10000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const audioRoom = useAudioStore((s) => s.room?.name);
|
||||
|
||||
|
||||
@@ -234,7 +234,7 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["livekit-rooms"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
refetchInterval: 10000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
|
||||
const participants =
|
||||
|
||||
@@ -35,7 +35,7 @@ export default function AdminPanel() {
|
||||
const { data: livekitRooms } = useQuery({
|
||||
queryKey: ["livekit-rooms"],
|
||||
queryFn: () => getLivekitRooms(),
|
||||
refetchInterval: 10000,
|
||||
refetchInterval: 5000,
|
||||
});
|
||||
const kickLivekitParticipantMutation = useMutation({
|
||||
mutationFn: kickLivekitParticipant,
|
||||
|
||||
@@ -17,10 +17,7 @@ export const deleteEvent = async (id: Event["id"]) => {
|
||||
};
|
||||
|
||||
export const upsertAppointment = async (
|
||||
eventAppointment: Prisma.XOR<
|
||||
Prisma.EventAppointmentCreateInput,
|
||||
Prisma.EventAppointmentUncheckedCreateInput
|
||||
>,
|
||||
eventAppointment: Prisma.EventAppointmentUncheckedCreateInput,
|
||||
) => {
|
||||
const newEventAppointment = eventAppointment.id
|
||||
? await prisma.eventAppointment.update({
|
||||
|
||||
@@ -5,7 +5,6 @@ import { useForm } from "react-hook-form";
|
||||
import { KEYWORD_CATEGORY, Keyword } from "@repo/db";
|
||||
import { FileText } from "lucide-react";
|
||||
import { Input } from "../../../../_components/ui/Input";
|
||||
import { useState } from "react";
|
||||
import { deleteKeyword, upsertKeyword } from "../action";
|
||||
import { Button } from "../../../../_components/ui/Button";
|
||||
import { redirect } from "next/navigation";
|
||||
@@ -17,7 +16,6 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
||||
resolver: zodResolver(KeywordOptionalDefaultsSchema),
|
||||
defaultValues: keyword,
|
||||
});
|
||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||
return (
|
||||
<>
|
||||
<form
|
||||
@@ -28,13 +26,13 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
||||
})}
|
||||
className="grid grid-cols-6 gap-3"
|
||||
>
|
||||
<div className="card bg-base-200 shadow-xl col-span-6 ">
|
||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<FileText className="w-5 h-5" /> Allgemeines
|
||||
<FileText className="h-5 w-5" /> Allgemeines
|
||||
</h2>
|
||||
<label className="form-control w-full ">
|
||||
<span className="label-text text-lg flex items-center gap-2">Kategorie</span>
|
||||
<label className="form-control w-full">
|
||||
<span className="label-text flex items-center gap-2 text-lg">Kategorie</span>
|
||||
<select
|
||||
className="input-sm select select-bordered select-sm w-full"
|
||||
{...form.register("category")}
|
||||
@@ -70,8 +68,8 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="card bg-base-200 shadow-xl col-span-6">
|
||||
<div className="card-body ">
|
||||
<div className="card bg-base-200 col-span-6 shadow-xl">
|
||||
<div className="card-body">
|
||||
<div className="flex w-full gap-4">
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
@@ -83,7 +81,6 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
||||
{keyword && (
|
||||
<Button
|
||||
onClick={async () => {
|
||||
setDeleteLoading(true);
|
||||
await deleteKeyword(keyword.id);
|
||||
redirect("/admin/keyword");
|
||||
}}
|
||||
|
||||
111
apps/hub/app/(app)/admin/report/_components/NewReport.tsx
Normal file
111
apps/hub/app/(app)/admin/report/_components/NewReport.tsx
Normal file
@@ -0,0 +1,111 @@
|
||||
"use client";
|
||||
import { createReport } from "(app)/admin/report/actions";
|
||||
import { getUser } from "(app)/admin/user/action";
|
||||
import { useQuery } from "@tanstack/react-query";
|
||||
import { Select } from "_components/ui/Select";
|
||||
import { useSession } from "next-auth/react";
|
||||
import { useRouter } from "next/navigation";
|
||||
import { useState } from "react";
|
||||
import { useForm } from "react-hook-form";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import toast from "react-hot-toast";
|
||||
import { Button } from "@repo/shared-components";
|
||||
import { ReportOptionalDefaults, ReportOptionalDefaultsSchema } from "@repo/db/zod";
|
||||
import { zodResolver } from "@hookform/resolvers/zod";
|
||||
|
||||
export const NewReportForm = ({
|
||||
defaultValues,
|
||||
}: {
|
||||
defaultValues?: Partial<ReportOptionalDefaults>;
|
||||
}) => {
|
||||
const session = useSession();
|
||||
const [search, setSearch] = useState("");
|
||||
const { data: user } = useQuery({
|
||||
queryKey: ["newReport"],
|
||||
queryFn: async () =>
|
||||
getUser({
|
||||
OR: [
|
||||
{ firstname: { contains: search, mode: "insensitive" } },
|
||||
{ lastname: { contains: search, mode: "insensitive" } },
|
||||
{ publicId: { contains: search, mode: "insensitive" } },
|
||||
{ id: defaultValues?.reportedUserId || "" },
|
||||
],
|
||||
}),
|
||||
enabled: search.length > 0,
|
||||
refetchOnWindowFocus: false,
|
||||
});
|
||||
const router = useRouter();
|
||||
const form = useForm({
|
||||
resolver: zodResolver(ReportOptionalDefaultsSchema),
|
||||
defaultValues: {
|
||||
reportedUserId: defaultValues?.reportedUserId || "",
|
||||
senderUserId: session.data?.user.id || "",
|
||||
},
|
||||
});
|
||||
|
||||
return (
|
||||
<form
|
||||
className="flex flex-wrap gap-3"
|
||||
onSubmit={form.handleSubmit(async (values) => {
|
||||
console.log("Form submitted with values:", values);
|
||||
const newReport = await createReport(values);
|
||||
toast.success("Report erfolgreich erstellt!");
|
||||
router.push(`/admin/report/${newReport.id}`);
|
||||
})}
|
||||
>
|
||||
<div className="card bg-base-200 flex-1 basis-[800px] shadow-xl">
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<TriangleAlert /> Neuen Report erstellen
|
||||
</h2>
|
||||
<Select
|
||||
form={form}
|
||||
options={
|
||||
user?.map((u) => ({
|
||||
label: `${u.firstname} ${u.lastname} (${u.publicId})`,
|
||||
value: u.id,
|
||||
})) || [
|
||||
{
|
||||
label: "Kein Nutzer gefunden",
|
||||
value: "",
|
||||
disabled: true,
|
||||
},
|
||||
]
|
||||
}
|
||||
onInputChange={(v) => setSearch(v)}
|
||||
name="reportedUserId"
|
||||
label="Nutzer"
|
||||
/>
|
||||
<Select
|
||||
form={form}
|
||||
options={[
|
||||
{ label: "Nutzer", value: "Nutzer" },
|
||||
{ label: "Pilot", value: "Pilot" },
|
||||
{ label: "Disponent", value: "Disponent" },
|
||||
]}
|
||||
name="reportedUserRole"
|
||||
label="Rolle des Nutzers"
|
||||
/>
|
||||
<textarea
|
||||
{...form.register("text")}
|
||||
className="textarea w-full"
|
||||
placeholder="Beschreibe den Vorfall"
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
<div className="card bg-base-200 flex-1 basis-[800px] shadow-xl">
|
||||
<div className="card-body">
|
||||
<div className="flex w-full gap-4">
|
||||
<Button
|
||||
isLoading={form.formState.isSubmitting}
|
||||
type="submit"
|
||||
className="btn btn-primary flex-1"
|
||||
>
|
||||
Speichern
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</form>
|
||||
);
|
||||
};
|
||||
@@ -26,15 +26,15 @@ export const ReportSenderInfo = ({
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<Link href={`/admin/user/${Reported?.id}`} className=" link link-hover">
|
||||
<Link href={`/admin/user/${Reported?.id}`} className="link link-hover">
|
||||
{Reported?.firstname} {Reported?.lastname} ({Reported?.publicId}) als{" "}
|
||||
</Link>
|
||||
<span className="text-primary">{report.reportedUserRole}</span>
|
||||
</h2>
|
||||
<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"
|
||||
href={`/admin/user/${Sender?.id}`}
|
||||
className="link link-hover text-right text-sm text-gray-600"
|
||||
>
|
||||
gemeldet von Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}) am{" "}
|
||||
{new Date(report.timestamp).toLocaleString()}
|
||||
@@ -82,7 +82,7 @@ export const ReportAdmin = ({
|
||||
>
|
||||
<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">
|
||||
<p className="text-right text-sm text-gray-600">
|
||||
{report.Reviewer &&
|
||||
`Kommentar von ${Reviewer?.firstname} ${Reviewer?.lastname} (${Reviewer?.publicId})`}
|
||||
</p>
|
||||
@@ -141,7 +141,7 @@ export const ReportPenalties = ({
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<Shield className="w-5 h-5" /> Strafen zu diesem Report
|
||||
<Shield className="h-5 w-5" /> Strafen zu diesem Report
|
||||
</h2>
|
||||
<PaginatedTable
|
||||
prismaModel="penalty"
|
||||
|
||||
@@ -1,10 +1,7 @@
|
||||
"use server";
|
||||
import { Prisma, prisma } from "@repo/db";
|
||||
|
||||
export const editReport = async (
|
||||
id: number,
|
||||
data: Prisma.ReportUncheckedUpdateInput,
|
||||
) => {
|
||||
export const editReport = async (id: number, data: Prisma.ReportUncheckedUpdateInput) => {
|
||||
return await prisma.report.update({
|
||||
where: {
|
||||
id: id,
|
||||
@@ -12,3 +9,11 @@ export const editReport = async (
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
export const createReport = async (
|
||||
data: Prisma.XOR<Prisma.ReportCreateInput, Prisma.ReportUncheckedCreateInput>,
|
||||
) => {
|
||||
return await prisma.report.create({
|
||||
data,
|
||||
});
|
||||
};
|
||||
|
||||
21
apps/hub/app/(app)/admin/report/new/page.tsx
Normal file
21
apps/hub/app/(app)/admin/report/new/page.tsx
Normal file
@@ -0,0 +1,21 @@
|
||||
import { NewReportForm } from "(app)/admin/report/_components/NewReport";
|
||||
|
||||
const Page = async ({
|
||||
searchParams,
|
||||
}: {
|
||||
searchParams?: {
|
||||
[key: string]: string | undefined;
|
||||
};
|
||||
}) => {
|
||||
const params = await searchParams;
|
||||
console.log("searchParams", params);
|
||||
return (
|
||||
<NewReportForm
|
||||
defaultValues={{
|
||||
reportedUserId: params?.reportedUserId || "",
|
||||
}}
|
||||
/>
|
||||
);
|
||||
};
|
||||
|
||||
export default Page;
|
||||
@@ -1,6 +1,8 @@
|
||||
"use client";
|
||||
import { PaginatedTable } from "_components/PaginatedTable";
|
||||
import { reportColumns } from "(app)/admin/report/columns";
|
||||
import { TriangleAlert } from "lucide-react";
|
||||
import Link from "next/link";
|
||||
|
||||
export default function ReportPage() {
|
||||
return (
|
||||
@@ -12,6 +14,18 @@ export default function ReportPage() {
|
||||
Sender: true,
|
||||
Reported: true,
|
||||
}}
|
||||
leftOfSearch={
|
||||
<p className="flex items-center gap-2 text-left text-2xl font-semibold">
|
||||
<TriangleAlert className="h-5 w-5" /> Reports
|
||||
</p>
|
||||
}
|
||||
rightOfSearch={
|
||||
<p className="flex items-center justify-between gap-2 text-left text-2xl font-semibold">
|
||||
<Link href={"/admin/report/new"}>
|
||||
<button className="btn btn-sm btn-outline btn-primary">Erstellen</button>
|
||||
</Link>
|
||||
</p>
|
||||
}
|
||||
columns={reportColumns}
|
||||
/>
|
||||
);
|
||||
|
||||
@@ -42,6 +42,7 @@ import {
|
||||
Eye,
|
||||
LockKeyhole,
|
||||
PlaneIcon,
|
||||
Plus,
|
||||
ShieldUser,
|
||||
Timer,
|
||||
Trash2,
|
||||
@@ -484,9 +485,16 @@ export const UserPenalties = ({ user }: { user: User }) => {
|
||||
export const UserReports = ({ user }: { user: User }) => {
|
||||
return (
|
||||
<div className="card-body">
|
||||
<h2 className="card-title">
|
||||
<ExclamationTriangleIcon className="h-5 w-5" /> Nutzer Reports
|
||||
</h2>
|
||||
<div className="card-title flex justify-between">
|
||||
<h2 className="flex items-center gap-2">
|
||||
<ExclamationTriangleIcon className="h-5 w-5" /> Nutzer Reports
|
||||
</h2>
|
||||
<Link href={`/admin/report/new?reportedUserId=${user.id}`}>
|
||||
<button className={"btn btn-xs btn-square btn-soft cursor-pointer"}>
|
||||
<Plus />
|
||||
</button>
|
||||
</Link>
|
||||
</div>
|
||||
<PaginatedTable
|
||||
prismaModel="report"
|
||||
filter={{
|
||||
|
||||
@@ -3,6 +3,12 @@ import { prisma, Prisma } from "@repo/db";
|
||||
import bcrypt from "bcryptjs";
|
||||
import { sendMailByTemplate } from "../../../../helper/mail";
|
||||
|
||||
export const getUser = async (where: Prisma.UserWhereInput) => {
|
||||
return await prisma.user.findMany({
|
||||
where,
|
||||
});
|
||||
};
|
||||
|
||||
export const editUser = async (id: string, data: Prisma.UserUpdateInput) => {
|
||||
return await prisma.user.update({
|
||||
where: {
|
||||
|
||||
Reference in New Issue
Block a user