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 }}
|
username: ${{ secrets.SSH_USERNAME }}
|
||||||
password: ${{ secrets.SSH_PASSWORD }}
|
password: ${{ secrets.SSH_PASSWORD }}
|
||||||
port: 22
|
port: 22
|
||||||
command_timeout: 30m
|
command_timeout: 60m
|
||||||
script: |
|
script: |
|
||||||
export NVM_DIR="$HOME/.nvm"
|
export NVM_DIR="$HOME/.nvm"
|
||||||
source "$NVM_DIR/nvm.sh"
|
source "$NVM_DIR/nvm.sh"
|
||||||
|
|||||||
@@ -25,7 +25,7 @@ export const StatusToast = ({ event, t }: { event: StationStatus; t: Toast }) =>
|
|||||||
const { data: livekitRooms } = useQuery({
|
const { data: livekitRooms } = useQuery({
|
||||||
queryKey: ["livekit-rooms"],
|
queryKey: ["livekit-rooms"],
|
||||||
queryFn: () => getLivekitRooms(),
|
queryFn: () => getLivekitRooms(),
|
||||||
refetchInterval: 10000,
|
refetchInterval: 5000,
|
||||||
});
|
});
|
||||||
const audioRoom = useAudioStore((s) => s.room?.name);
|
const audioRoom = useAudioStore((s) => s.room?.name);
|
||||||
|
|
||||||
|
|||||||
@@ -234,7 +234,7 @@ const StationTab = ({ aircraft }: { aircraft: ConnectedAircraft & { Station: Sta
|
|||||||
const { data: livekitRooms } = useQuery({
|
const { data: livekitRooms } = useQuery({
|
||||||
queryKey: ["livekit-rooms"],
|
queryKey: ["livekit-rooms"],
|
||||||
queryFn: () => getLivekitRooms(),
|
queryFn: () => getLivekitRooms(),
|
||||||
refetchInterval: 10000,
|
refetchInterval: 5000,
|
||||||
});
|
});
|
||||||
|
|
||||||
const participants =
|
const participants =
|
||||||
|
|||||||
@@ -35,7 +35,7 @@ export default function AdminPanel() {
|
|||||||
const { data: livekitRooms } = useQuery({
|
const { data: livekitRooms } = useQuery({
|
||||||
queryKey: ["livekit-rooms"],
|
queryKey: ["livekit-rooms"],
|
||||||
queryFn: () => getLivekitRooms(),
|
queryFn: () => getLivekitRooms(),
|
||||||
refetchInterval: 10000,
|
refetchInterval: 5000,
|
||||||
});
|
});
|
||||||
const kickLivekitParticipantMutation = useMutation({
|
const kickLivekitParticipantMutation = useMutation({
|
||||||
mutationFn: kickLivekitParticipant,
|
mutationFn: kickLivekitParticipant,
|
||||||
|
|||||||
@@ -17,10 +17,7 @@ export const deleteEvent = async (id: Event["id"]) => {
|
|||||||
};
|
};
|
||||||
|
|
||||||
export const upsertAppointment = async (
|
export const upsertAppointment = async (
|
||||||
eventAppointment: Prisma.XOR<
|
eventAppointment: Prisma.EventAppointmentUncheckedCreateInput,
|
||||||
Prisma.EventAppointmentCreateInput,
|
|
||||||
Prisma.EventAppointmentUncheckedCreateInput
|
|
||||||
>,
|
|
||||||
) => {
|
) => {
|
||||||
const newEventAppointment = eventAppointment.id
|
const newEventAppointment = eventAppointment.id
|
||||||
? await prisma.eventAppointment.update({
|
? await prisma.eventAppointment.update({
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import { useForm } from "react-hook-form";
|
|||||||
import { KEYWORD_CATEGORY, Keyword } from "@repo/db";
|
import { KEYWORD_CATEGORY, Keyword } from "@repo/db";
|
||||||
import { FileText } from "lucide-react";
|
import { FileText } from "lucide-react";
|
||||||
import { Input } from "../../../../_components/ui/Input";
|
import { Input } from "../../../../_components/ui/Input";
|
||||||
import { useState } from "react";
|
|
||||||
import { deleteKeyword, upsertKeyword } from "../action";
|
import { deleteKeyword, upsertKeyword } from "../action";
|
||||||
import { Button } from "../../../../_components/ui/Button";
|
import { Button } from "../../../../_components/ui/Button";
|
||||||
import { redirect } from "next/navigation";
|
import { redirect } from "next/navigation";
|
||||||
@@ -17,7 +16,6 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
|||||||
resolver: zodResolver(KeywordOptionalDefaultsSchema),
|
resolver: zodResolver(KeywordOptionalDefaultsSchema),
|
||||||
defaultValues: keyword,
|
defaultValues: keyword,
|
||||||
});
|
});
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<form
|
<form
|
||||||
@@ -28,13 +26,13 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
|||||||
})}
|
})}
|
||||||
className="grid grid-cols-6 gap-3"
|
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">
|
<div className="card-body">
|
||||||
<h2 className="card-title">
|
<h2 className="card-title">
|
||||||
<FileText className="w-5 h-5" /> Allgemeines
|
<FileText className="h-5 w-5" /> Allgemeines
|
||||||
</h2>
|
</h2>
|
||||||
<label className="form-control w-full ">
|
<label className="form-control w-full">
|
||||||
<span className="label-text text-lg flex items-center gap-2">Kategorie</span>
|
<span className="label-text flex items-center gap-2 text-lg">Kategorie</span>
|
||||||
<select
|
<select
|
||||||
className="input-sm select select-bordered select-sm w-full"
|
className="input-sm select select-bordered select-sm w-full"
|
||||||
{...form.register("category")}
|
{...form.register("category")}
|
||||||
@@ -70,8 +68,8 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|
||||||
<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 ">
|
<div className="card-body">
|
||||||
<div className="flex w-full gap-4">
|
<div className="flex w-full gap-4">
|
||||||
<Button
|
<Button
|
||||||
isLoading={form.formState.isSubmitting}
|
isLoading={form.formState.isSubmitting}
|
||||||
@@ -83,7 +81,6 @@ export const KeywordForm = ({ keyword }: { keyword?: Keyword }) => {
|
|||||||
{keyword && (
|
{keyword && (
|
||||||
<Button
|
<Button
|
||||||
onClick={async () => {
|
onClick={async () => {
|
||||||
setDeleteLoading(true);
|
|
||||||
await deleteKeyword(keyword.id);
|
await deleteKeyword(keyword.id);
|
||||||
redirect("/admin/keyword");
|
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 (
|
return (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">
|
<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{" "}
|
{Reported?.firstname} {Reported?.lastname} ({Reported?.publicId}) als{" "}
|
||||||
</Link>
|
</Link>
|
||||||
<span className="text-primary">{report.reportedUserRole}</span>
|
<span className="text-primary">{report.reportedUserRole}</span>
|
||||||
</h2>
|
</h2>
|
||||||
<div className="textarea w-full text-left">{report.text}</div>
|
<div className="textarea w-full text-left">{report.text}</div>
|
||||||
<Link
|
<Link
|
||||||
href={`/admin/user/${Reported?.id}`}
|
href={`/admin/user/${Sender?.id}`}
|
||||||
className="text-sm text-gray-600 text-right link link-hover"
|
className="link link-hover text-right text-sm text-gray-600"
|
||||||
>
|
>
|
||||||
gemeldet von Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}) am{" "}
|
gemeldet von Nutzer {Sender?.firstname} {Sender?.lastname} ({Sender?.publicId}) am{" "}
|
||||||
{new Date(report.timestamp).toLocaleString()}
|
{new Date(report.timestamp).toLocaleString()}
|
||||||
@@ -82,7 +82,7 @@ export const ReportAdmin = ({
|
|||||||
>
|
>
|
||||||
<h2 className="card-title">Staff Kommentar</h2>
|
<h2 className="card-title">Staff Kommentar</h2>
|
||||||
<textarea {...form.register("reviewerComment")} className="textarea w-full" placeholder="" />
|
<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 &&
|
{report.Reviewer &&
|
||||||
`Kommentar von ${Reviewer?.firstname} ${Reviewer?.lastname} (${Reviewer?.publicId})`}
|
`Kommentar von ${Reviewer?.firstname} ${Reviewer?.lastname} (${Reviewer?.publicId})`}
|
||||||
</p>
|
</p>
|
||||||
@@ -141,7 +141,7 @@ export const ReportPenalties = ({
|
|||||||
return (
|
return (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">
|
<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>
|
</h2>
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
prismaModel="penalty"
|
prismaModel="penalty"
|
||||||
|
|||||||
@@ -1,10 +1,7 @@
|
|||||||
"use server";
|
"use server";
|
||||||
import { Prisma, prisma } from "@repo/db";
|
import { Prisma, prisma } from "@repo/db";
|
||||||
|
|
||||||
export const editReport = async (
|
export const editReport = async (id: number, data: Prisma.ReportUncheckedUpdateInput) => {
|
||||||
id: number,
|
|
||||||
data: Prisma.ReportUncheckedUpdateInput,
|
|
||||||
) => {
|
|
||||||
return await prisma.report.update({
|
return await prisma.report.update({
|
||||||
where: {
|
where: {
|
||||||
id: id,
|
id: id,
|
||||||
@@ -12,3 +9,11 @@ export const editReport = async (
|
|||||||
data,
|
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";
|
"use client";
|
||||||
import { PaginatedTable } from "_components/PaginatedTable";
|
import { PaginatedTable } from "_components/PaginatedTable";
|
||||||
import { reportColumns } from "(app)/admin/report/columns";
|
import { reportColumns } from "(app)/admin/report/columns";
|
||||||
|
import { TriangleAlert } from "lucide-react";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export default function ReportPage() {
|
export default function ReportPage() {
|
||||||
return (
|
return (
|
||||||
@@ -12,6 +14,18 @@ export default function ReportPage() {
|
|||||||
Sender: true,
|
Sender: true,
|
||||||
Reported: 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}
|
columns={reportColumns}
|
||||||
/>
|
/>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ import {
|
|||||||
Eye,
|
Eye,
|
||||||
LockKeyhole,
|
LockKeyhole,
|
||||||
PlaneIcon,
|
PlaneIcon,
|
||||||
|
Plus,
|
||||||
ShieldUser,
|
ShieldUser,
|
||||||
Timer,
|
Timer,
|
||||||
Trash2,
|
Trash2,
|
||||||
@@ -484,9 +485,16 @@ export const UserPenalties = ({ user }: { user: User }) => {
|
|||||||
export const UserReports = ({ user }: { user: User }) => {
|
export const UserReports = ({ user }: { user: User }) => {
|
||||||
return (
|
return (
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">
|
<div className="card-title flex justify-between">
|
||||||
<ExclamationTriangleIcon className="h-5 w-5" /> Nutzer Reports
|
<h2 className="flex items-center gap-2">
|
||||||
</h2>
|
<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
|
<PaginatedTable
|
||||||
prismaModel="report"
|
prismaModel="report"
|
||||||
filter={{
|
filter={{
|
||||||
|
|||||||
@@ -3,6 +3,12 @@ import { prisma, Prisma } from "@repo/db";
|
|||||||
import bcrypt from "bcryptjs";
|
import bcrypt from "bcryptjs";
|
||||||
import { sendMailByTemplate } from "../../../../helper/mail";
|
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) => {
|
export const editUser = async (id: string, data: Prisma.UserUpdateInput) => {
|
||||||
return await prisma.user.update({
|
return await prisma.user.update({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user