Added Account Dublicate fucntion, improved default sorts

This commit is contained in:
PxlLoewe
2025-12-26 01:23:32 +01:00
parent 51ef9cd90c
commit 17208eded9
18 changed files with 486 additions and 139 deletions

View File

@@ -76,16 +76,17 @@ export const ProfileForm: React.FC<ProfileFormProps> = ({ user }: ProfileFormPro
className="card-body"
onSubmit={form.handleSubmit(async (values) => {
if (!values.id) return;
if (values.id === session.data?.user.id && values.permissions !== user.permissions){
if (values.id === session.data?.user.id && values.permissions !== user.permissions) {
toast.error("Du kannst deine eigenen Berechtigungen nicht ändern.");
return;
}
if ( values.permissions?.some((perm) => !session.data?.user.permissions.includes(perm)) ){
if (values.permissions?.some((perm) => !session.data?.user.permissions.includes(perm))) {
toast.error("Du kannst Berechtigungen nicht hinzufügen, die du selbst nicht besitzt.");
return;
}
const removedPermissions = user.permissions?.filter((perm) => !values.permissions?.includes(perm)) || [];
if ( removedPermissions.some((perm) => !session.data?.user.permissions.includes(perm)) ){
const removedPermissions =
user.permissions?.filter((perm) => !values.permissions?.includes(perm)) || [];
if (removedPermissions.some((perm) => !session.data?.user.permissions.includes(perm))) {
toast.error("Du kannst Berechtigungen nicht entfernen, die du selbst nicht besitzt.");
return;
}
@@ -284,6 +285,12 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
userId: user.id,
}}
prismaModel={"connectedDispatcher"}
initialOrderBy={[
{
id: "loginTime",
desc: true,
},
]}
columns={
[
{
@@ -347,6 +354,12 @@ export const ConnectionHistory: React.FC<{ user: User }> = ({ user }: { user: Us
}}
prismaModel={"connectedAircraft"}
include={{ Station: true }}
initialOrderBy={[
{
id: "loginTime",
desc: true,
},
]}
columns={
[
{
@@ -519,6 +532,12 @@ export const UserReports = ({ user }: { user: User }) => {
filter={{
reportedUserId: user.id,
}}
initialOrderBy={[
{
id: "timestamp",
desc: true,
},
]}
include={{
Sender: true,
Reported: true,
@@ -531,7 +550,7 @@ export const UserReports = ({ user }: { user: User }) => {
interface AdminFormProps {
discordAccount?: DiscordAccount;
user: User;
user: User & { CanonicalUser?: User | null; Duplicates?: User[] | null };
dispoTime: {
hours: number;
minutes: number;
@@ -653,7 +672,59 @@ export const AdminForm = ({
</div>
)}
</div>
{session?.user.permissions.includes("ADMIN_USER_ADVANCED") && (
<div className="mt-2 space-y-1">
<Link href={`/admin/user/${user.id}/duplicate`}>
<Button className="btn-sm btn-outline w-full">
<LockKeyhole className="h-4 w-4" /> Duplikat markieren & sperren
</Button>
</Link>
</div>
)}
</div>
{(user.CanonicalUser || (user.Duplicates && user.Duplicates.length > 0)) && (
<div role="alert" className="alert alert-error alert-outline flex flex-col">
<div className="flex items-center gap-2">
<TriangleAlert />
<div>
{user.CanonicalUser && (
<div>
<h3 className="text-lg font-semibold">Als Duplikat markiert</h3>
<p>
Dieser Account wurde als Duplikat von{" "}
<Link
href={`/admin/user/${user.CanonicalUser.id}`}
className="link link-hover font-semibold"
>
{user.CanonicalUser.firstname} {user.CanonicalUser.lastname} (
{user.CanonicalUser.publicId})
</Link>{" "}
markiert.
</p>
</div>
)}
{user.Duplicates && user.Duplicates.length > 0 && (
<div className="mt-2">
<h3 className="text-lg font-semibold">Duplikate erkannt</h3>
<p>Folgende Accounts wurden als Duplikate dieses Accounts markiert:</p>
<ul className="ml-4 mt-1 list-inside list-disc">
{user.Duplicates.map((duplicate) => (
<li key={duplicate.id}>
<Link href={`/admin/user/${duplicate.id}`} className="link link-hover">
{duplicate.firstname} {duplicate.lastname} ({duplicate.publicId})
</Link>
</li>
))}
</ul>
</div>
)}
</div>
</div>
<p className="text-sm text-gray-400">
Achtung! Dieser Account ist als Duplikat markiert oder hat Duplikate!
</p>
</div>
)}
{(!!openBans.length || !!openTimebans.length) && (
<div role="alert" className="alert alert-warning alert-outline flex flex-col">
<div className="flex items-center gap-2">
@@ -681,6 +752,7 @@ export const AdminForm = ({
</p>
</div>
)}
<h2 className="card-title">
<ChartBarBigIcon className="h-5 w-5" /> Aktivität
</h2>

View File

@@ -0,0 +1,109 @@
"use client";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { useQuery } from "@tanstack/react-query";
import { getUser, markDuplicate } from "(app)/admin/user/action";
import { Button } from "@repo/shared-components";
import { Select } from "_components/ui/Select";
import toast from "react-hot-toast";
import { TriangleAlert } from "lucide-react";
import { useState } from "react";
const DuplicateSchema = z.object({
canonicalUserId: z.string().min(1, "Bitte Nutzer auswählen"),
reason: z.string().max(500).optional(),
});
export const DuplicateForm = ({ duplicateUserId }: { duplicateUserId: string }) => {
const form = useForm<z.infer<typeof DuplicateSchema>>({
resolver: zodResolver(DuplicateSchema),
defaultValues: { canonicalUserId: "", reason: "" },
});
const [search, setSearch] = useState("");
const { data: users } = useQuery({
queryKey: ["duplicate-search"],
queryFn: async () =>
getUser({
OR: [
{ firstname: { contains: search, mode: "insensitive" } },
{ lastname: { contains: search, mode: "insensitive" } },
{ publicId: { contains: search, mode: "insensitive" } },
{ email: { contains: search, mode: "insensitive" } },
],
}),
enabled: search.length > 0,
refetchOnWindowFocus: false,
});
return (
<form
className="flex flex-wrap gap-3"
onSubmit={form.handleSubmit(async (values) => {
try {
// find selected canonical user by id to obtain publicId
const canonical = (users || []).find((u) => u.id === values.canonicalUserId);
if (!canonical) {
toast.error("Bitte wähle einen Original-Account aus.");
return;
}
await markDuplicate({
duplicateUserId,
canonicalPublicId: canonical.publicId,
reason: values.reason,
});
toast.success("Duplikat verknüpft und Nutzer gesperrt.");
} catch (e: unknown) {
const message =
typeof e === "object" && e && "message" in e
? (e as { message?: string }).message || "Fehler beim Verknüpfen"
: "Fehler beim Verknüpfen";
toast.error(message);
}
})}
>
<div className="card bg-base-200 flex-1 basis-[800px] shadow-xl">
<div className="card-body">
<h2 className="card-title">
<TriangleAlert /> Duplikat markieren & sperren
</h2>
<Select
form={form}
name="canonicalUserId"
label="Original-Nutzer suchen & auswählen"
onInputChange={(v) => setSearch(String(v))}
options={
users?.map((u) => ({
label: `${u.firstname} ${u.lastname} (${u.publicId})`,
value: u.id,
})) || [{ label: "Kein Nutzer gefunden", value: "", disabled: true }]
}
/>
<label className="floating-label w-full">
<span className="flex items-center gap-2 text-lg">Grund (optional)</span>
<input
{...form.register("reason")}
type="text"
className="input input-bordered w-full"
placeholder="Begründung/Audit-Hinweis"
/>
</label>
</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"
>
Als Duplikat verknüpfen & sperren
</Button>
</div>
</div>
</div>
</form>
);
};

View File

@@ -0,0 +1,37 @@
import { prisma } from "@repo/db";
import { DuplicateForm } from "./_components/DuplicateForm";
import { PersonIcon } from "@radix-ui/react-icons";
import Link from "next/link";
import { ArrowLeft } from "lucide-react";
export default async function Page({ params }: { params: Promise<{ id: string }> }) {
const { id } = await params;
const user = await prisma.user.findUnique({
where: { id },
select: { id: true, firstname: true, lastname: true, publicId: true },
});
if (!user) {
return (
<div className="card bg-base-200 shadow-xl">
<div className="card-body">Nutzer nicht gefunden</div>
</div>
);
}
return (
<>
<div className="my-3">
<div className="text-left">
<Link href={`/admin/user/${user.id}`} className="link-hover l-0 text-gray-500">
<ArrowLeft className="mb-1 mr-1 inline h-4 w-4" />
Zurück zum Nutzer
</Link>
</div>
<p className="text-left text-2xl font-semibold">
<PersonIcon className="mr-2 inline h-5 w-5" /> Duplikat für {user.firstname}{" "}
{user.lastname} #{user.publicId}
</p>
</div>
<DuplicateForm duplicateUserId={user.id} />
</>
);
}

View File

@@ -18,6 +18,8 @@ export default async function Page({ params }: { params: Promise<{ id: string }>
},
include: {
discordAccounts: true,
CanonicalUser: true,
Duplicates: true,
},
});
if (!user) return <Error statusCode={404} title="User not found" />;