Event admin redesign #153
@@ -0,0 +1,34 @@
|
|||||||
|
import { AppointmentForm } from "(app)/admin/event/_components/AppointmentForm";
|
||||||
|
import { prisma } from "@repo/db";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string; appointmentId: string }>;
|
||||||
|
}) {
|
||||||
|
const { id: eventId, appointmentId } = await params;
|
||||||
|
|
||||||
|
const event = await prisma.event.findUnique({
|
||||||
|
where: { id: parseInt(eventId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!event) return <div>Event nicht gefunden</div>;
|
||||||
|
|
||||||
|
let appointment = null;
|
||||||
|
if (appointmentId !== "new") {
|
||||||
|
appointment = await prisma.eventAppointment.findUnique({
|
||||||
|
where: { id: parseInt(appointmentId) },
|
||||||
|
include: {
|
||||||
|
Presenter: true,
|
||||||
|
Participants: {
|
||||||
|
include: { User: true },
|
||||||
|
},
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!appointment) return <div>Termin nicht gefunden</div>;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<AppointmentForm event={event} initialAppointment={appointment} appointmentId={appointmentId} />
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -0,0 +1,50 @@
|
|||||||
|
import { prisma } from "@repo/db";
|
||||||
|
import { ParticipantForm } from "../../../_components/ParticipantForm";
|
||||||
|
import { Error } from "_components/Error";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { PersonIcon } from "@radix-ui/react-icons";
|
||||||
|
import { ArrowLeft } from "lucide-react";
|
||||||
|
|
||||||
|
export default async function Page({
|
||||||
|
params,
|
||||||
|
}: {
|
||||||
|
params: Promise<{ id: string; participantId: string }>;
|
||||||
|
}) {
|
||||||
|
const { id: eventId, participantId } = await params;
|
||||||
|
|
||||||
|
const event = await prisma.event.findUnique({
|
||||||
|
where: { id: parseInt(eventId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const participant = await prisma.participant.findUnique({
|
||||||
|
where: { id: parseInt(participantId) },
|
||||||
|
});
|
||||||
|
|
||||||
|
const user = await prisma.user.findUnique({
|
||||||
|
where: { id: participant?.userId },
|
||||||
|
});
|
||||||
|
|
||||||
|
if (!event) return <div>Event nicht gefunden</div>;
|
||||||
|
|
||||||
|
if (!participant || !user) {
|
||||||
|
return <Error title="Teilnehmer nicht gefunden" statusCode={404} />;
|
||||||
|
}
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<div className="my-3">
|
||||||
|
<div className="text-left">
|
||||||
|
<Link href={`/admin/event/${event.id}`} className="link-hover l-0 text-gray-500">
|
||||||
|
<ArrowLeft className="mb-1 mr-1 inline h-4 w-4" />
|
||||||
|
Zurück zum Event
|
||||||
|
</Link>
|
||||||
|
</div>
|
||||||
|
<p className="text-left text-2xl font-semibold">
|
||||||
|
<PersonIcon className="mr-2 inline h-5 w-5" /> Event-übersicht für{" "}
|
||||||
|
{`${user.firstname} ${user.lastname} #${user.publicId}`}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<ParticipantForm event={event} user={user} participant={participant} />
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
260
apps/hub/app/(app)/admin/event/_components/AppointmentForm.tsx
Normal file
260
apps/hub/app/(app)/admin/event/_components/AppointmentForm.tsx
Normal file
@@ -0,0 +1,260 @@
|
|||||||
|
"use client";
|
||||||
|
import {
|
||||||
|
EventAppointmentOptionalDefaults,
|
||||||
|
EventAppointmentOptionalDefaultsSchema,
|
||||||
|
} from "@repo/db/zod";
|
||||||
|
import { useRouter } from "next/navigation";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { useRef } from "react";
|
||||||
|
import { ColumnDef } from "@tanstack/react-table";
|
||||||
|
import { Event, EventAppointment, Participant, Prisma } from "@repo/db";
|
||||||
|
import { ArrowLeft, Calendar, Users } from "lucide-react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import Link from "next/link";
|
||||||
|
import { PaginatedTable, PaginatedTableRef } from "../../../_components/PaginatedTable";
|
||||||
|
import { Button } from "../../../_components/ui/Button";
|
||||||
|
import { DateInput } from "../../../_components/ui/DateInput";
|
||||||
|
import { upsertParticipant } from "../../events/actions";
|
||||||
|
import { upsertAppointment, deleteAppoinement } from "../action";
|
||||||
|
import { InputJsonValueType } from "@repo/db/zod";
|
||||||
|
|
||||||
|
interface AppointmentFormProps {
|
||||||
|
event: Event;
|
||||||
|
initialAppointment:
|
||||||
|
| (EventAppointment & {
|
||||||
|
Presenter?: any;
|
||||||
|
Participants?: (Participant & { User?: any })[];
|
||||||
|
})
|
||||||
|
| null;
|
||||||
|
appointmentId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
export const AppointmentForm = ({
|
||||||
|
event,
|
||||||
|
initialAppointment,
|
||||||
|
appointmentId,
|
||||||
|
}: AppointmentFormProps) => {
|
||||||
|
const router = useRouter();
|
||||||
|
const participantTableRef = useRef<PaginatedTableRef>(null);
|
||||||
|
|
||||||
|
const form = useForm<EventAppointmentOptionalDefaults>({
|
||||||
|
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
|
||||||
|
defaultValues: initialAppointment
|
||||||
|
? {
|
||||||
|
...initialAppointment,
|
||||||
|
eventId: event.id,
|
||||||
|
presenterId: initialAppointment.presenterId,
|
||||||
|
}
|
||||||
|
: undefined,
|
||||||
|
});
|
||||||
|
|
||||||
|
const onSubmit = async (values: EventAppointmentOptionalDefaults) => {
|
||||||
|
try {
|
||||||
|
await upsertAppointment({
|
||||||
|
...values,
|
||||||
|
eventId: event.id,
|
||||||
|
});
|
||||||
|
toast.success("Termin erfolgreich gespeichert");
|
||||||
|
router.back();
|
||||||
|
} catch {
|
||||||
|
toast.error("Fehler beim Speichern des Termins");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleDelete = async () => {
|
||||||
|
if (!confirm("Wirklich löschen?")) return;
|
||||||
|
try {
|
||||||
|
await deleteAppoinement(parseInt(appointmentId));
|
||||||
|
toast.success("Termin gelöscht");
|
||||||
|
router.back();
|
||||||
|
} catch {
|
||||||
|
toast.error("Fehler beim Löschen");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="bg-base-100 min-h-screen p-6">
|
||||||
|
<div className="mx-auto max-w-6xl">
|
||||||
|
{/* Header */}
|
||||||
|
<div className="mb-6">
|
||||||
|
<Link href={`/admin/event/${event.id}`} className="btn btn-ghost btn-sm mb-4">
|
||||||
|
<ArrowLeft className="h-4 w-4" /> Zurück
|
||||||
|
</Link>
|
||||||
|
<h1 className="text-3xl font-bold">
|
||||||
|
{appointmentId === "new" ? "Neuer Termin" : "Termin bearbeiten"}
|
||||||
|
</h1>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Main Content */}
|
||||||
|
<div className="grid grid-cols-1 gap-6 lg:grid-cols-3">
|
||||||
|
{/* Form Card */}
|
||||||
|
<form onSubmit={form.handleSubmit(onSubmit)} className="space-y-4 lg:col-span-1">
|
||||||
|
<div className="card bg-base-200 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Calendar className="h-5 w-5" /> Termin Details
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="form-control">
|
||||||
|
<label className="label">
|
||||||
|
<span className="label-text font-semibold">Datum & Zeit</span>
|
||||||
|
</label>
|
||||||
|
<DateInput
|
||||||
|
value={new Date(form.watch("appointmentDate") || Date.now())}
|
||||||
|
onChange={(date) => form.setValue("appointmentDate", date)}
|
||||||
|
/>
|
||||||
|
{form.formState.errors.appointmentDate && (
|
||||||
|
<span className="text-error mt-1 text-sm">
|
||||||
|
{form.formState.errors.appointmentDate.message}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="flex gap-2 pt-2">
|
||||||
|
<Button
|
||||||
|
type="submit"
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{appointmentId !== "new" && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onClick={handleDelete}
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
className="btn btn-error"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
|
||||||
|
{/* Participants Table */}
|
||||||
|
<div className="lg:col-span-2">
|
||||||
|
<div className="card bg-base-200 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Users className="h-5 w-5" /> Teilnehmer
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
{appointmentId !== "new" ? (
|
||||||
|
<PaginatedTable
|
||||||
|
ref={participantTableRef}
|
||||||
|
prismaModel={"participant"}
|
||||||
|
getFilter={() =>
|
||||||
|
({
|
||||||
|
eventAppointmentId: appointmentId,
|
||||||
|
}) as Prisma.ParticipantWhereInput
|
||||||
|
}
|
||||||
|
include={{ User: true }}
|
||||||
|
columns={
|
||||||
|
[
|
||||||
|
{
|
||||||
|
accessorKey: "User.firstname",
|
||||||
|
header: "Vorname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "User.lastname",
|
||||||
|
header: "Nachname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "enscriptionDate",
|
||||||
|
header: "Einschreibedatum",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<span>{new Date(row.original.enscriptionDate).toLocaleString()}</span>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Status",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
if (row.original.attended) {
|
||||||
|
return <span className="badge badge-success">Anwesend</span>;
|
||||||
|
} else if (row.original.appointmentCancelled) {
|
||||||
|
return <span className="badge badge-error">Abgesagt</span>;
|
||||||
|
} else {
|
||||||
|
return <span className="badge badge-ghost">Offen</span>;
|
||||||
|
}
|
||||||
|
},
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Aktionen",
|
||||||
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<Link
|
||||||
|
href={`/admin/event/${event.id}/participant/${row.original.id}`}
|
||||||
|
className="btn btn-sm btn-outline"
|
||||||
|
>
|
||||||
|
Anzeigen
|
||||||
|
</Link>
|
||||||
|
{!row.original.attended && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await upsertParticipant({
|
||||||
|
eventId: event.id,
|
||||||
|
userId: row.original.userId,
|
||||||
|
attended: true,
|
||||||
|
appointmentCancelled: false,
|
||||||
|
});
|
||||||
|
participantTableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-sm btn-info btn-outline"
|
||||||
|
>
|
||||||
|
✓
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
{!row.original.appointmentCancelled && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onClick={async () => {
|
||||||
|
await upsertParticipant({
|
||||||
|
eventId: event.id,
|
||||||
|
userId: row.original.userId,
|
||||||
|
attended: false,
|
||||||
|
appointmentCancelled: true,
|
||||||
|
statusLog: [
|
||||||
|
...(row.original.statusLog as InputJsonValueType[]),
|
||||||
|
{
|
||||||
|
event: "Gefehlt an Event",
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
user: "Admin",
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
participantTableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-sm btn-error btn-outline"
|
||||||
|
>
|
||||||
|
✗
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
] as ColumnDef<Participant>[]
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
) : (
|
||||||
|
<div className="py-8 text-center text-gray-500">
|
||||||
|
Speichern Sie den Termin um Teilnehmer hinzuzufügen
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
218
apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx
Normal file
218
apps/hub/app/(app)/admin/event/_components/ParticipantForm.tsx
Normal file
@@ -0,0 +1,218 @@
|
|||||||
|
"use client";
|
||||||
|
import { Participant, Event, User, ParticipantLog, Prisma } from "@repo/db";
|
||||||
|
import { Users, Activity, Bug } from "lucide-react";
|
||||||
|
import toast from "react-hot-toast";
|
||||||
|
import { InputJsonValueType, ParticipantOptionalDefaultsSchema } from "@repo/db/zod";
|
||||||
|
import { useForm } from "react-hook-form";
|
||||||
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
|
import { Switch } from "_components/ui/Switch";
|
||||||
|
import { useState } from "react";
|
||||||
|
import { Button } from "_components/ui/Button";
|
||||||
|
import { useMutation, useQueryClient } from "@tanstack/react-query";
|
||||||
|
import { upsertParticipant } from "(app)/events/actions";
|
||||||
|
import { deleteParticipant } from "../action";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
|
||||||
|
interface ParticipantFormProps {
|
||||||
|
event: Event;
|
||||||
|
participant: Participant;
|
||||||
|
user: User;
|
||||||
|
}
|
||||||
|
|
||||||
|
const checkEventCompleted = (participant: Participant, event: Event): boolean => {
|
||||||
|
if (event.hasPresenceEvents) {
|
||||||
|
return event.finisherMoodleCourseId
|
||||||
|
? participant.finisherMoodleCurseCompleted && participant.attended
|
||||||
|
: !!participant.attended;
|
||||||
|
}
|
||||||
|
return event.finisherMoodleCourseId ? participant.finisherMoodleCurseCompleted : false;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const ParticipantForm = ({ event, participant, user }: ParticipantFormProps) => {
|
||||||
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
|
const queryClient = useQueryClient();
|
||||||
|
|
||||||
|
const upsertParticipantMutation = useMutation({
|
||||||
|
mutationKey: ["upsertParticipant"],
|
||||||
|
mutationFn: async (newData: Prisma.ParticipantUncheckedCreateInput) => {
|
||||||
|
const data = await upsertParticipant(newData);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["participants", event.id] });
|
||||||
|
return data;
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const deleteParticipantMutation = useMutation({
|
||||||
|
mutationKey: ["deleteParticipant"],
|
||||||
|
mutationFn: async (participantId: number) => {
|
||||||
|
await deleteParticipant(participantId);
|
||||||
|
await queryClient.invalidateQueries({ queryKey: ["participants", event.id] });
|
||||||
|
},
|
||||||
|
});
|
||||||
|
|
||||||
|
const eventCompleted = checkEventCompleted(participant, event);
|
||||||
|
|
||||||
|
const form = useForm({
|
||||||
|
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||||
|
defaultValues: participant,
|
||||||
|
});
|
||||||
|
|
||||||
|
const handleEventFinished = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
await upsertParticipantMutation.mutateAsync({
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant.userId,
|
||||||
|
});
|
||||||
|
toast.success("Event als beendet markiert");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const handleCheckMoodle = async () => {
|
||||||
|
setIsLoading(true);
|
||||||
|
try {
|
||||||
|
toast.success("Moodle-Check durchgeführt");
|
||||||
|
} finally {
|
||||||
|
setIsLoading(false);
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<form
|
||||||
|
onSubmit={form.handleSubmit(async (formData) => {
|
||||||
|
const data = await upsertParticipantMutation.mutateAsync({
|
||||||
|
...formData,
|
||||||
|
statusLog: participant.statusLog as unknown as Prisma.ParticipantCreatestatusLogInput,
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant.userId,
|
||||||
|
});
|
||||||
|
form.reset(data);
|
||||||
|
toast.success("Teilnehmer aktualisiert");
|
||||||
|
})}
|
||||||
|
className="bg-base-100 flex flex-wrap gap-6 p-6"
|
||||||
|
>
|
||||||
|
{/* Status Section */}
|
||||||
|
<div className="card bg-base-200 shadow">
|
||||||
|
<div className="card-body">
|
||||||
|
<h3 className="card-title text-sm">
|
||||||
|
<Bug className="mr-2 inline h-5 w-5" />
|
||||||
|
Debug
|
||||||
|
</h3>
|
||||||
|
<Switch form={form} name={"attended"} label="Anwesend" />
|
||||||
|
<Switch form={form} name={"appointmentCancelled"} label="Termin Abgesagt" />
|
||||||
|
<Switch
|
||||||
|
form={form}
|
||||||
|
name={"finisherMoodleCurseCompleted"}
|
||||||
|
label="Moodle Kurs abgeschlossen"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
{/* Info Card */}
|
||||||
|
<div className="card bg-base-200 min-w-[200px] flex-1 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Users className="h-5 w-5" /> Informationen
|
||||||
|
</h2>
|
||||||
|
<div className="divider" />
|
||||||
|
<div>
|
||||||
|
<p className="text-sm text-gray-600">Kurs-status</p>
|
||||||
|
<p className="font-semibold">{eventCompleted ? "Abgeschlossen" : "In Bearbeitung"}</p>
|
||||||
|
<p className="text-sm text-gray-600">Einschreibedatum</p>
|
||||||
|
<p className="font-semibold">
|
||||||
|
{participant?.enscriptionDate
|
||||||
|
? new Date(participant.enscriptionDate).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="divider" />
|
||||||
|
|
||||||
|
<div className="flex flex-col gap-2">
|
||||||
|
<Button
|
||||||
|
onClick={handleEventFinished}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="btn btn-sm btn-success"
|
||||||
|
>
|
||||||
|
Event beendet
|
||||||
|
</Button>
|
||||||
|
<Button
|
||||||
|
onClick={handleCheckMoodle}
|
||||||
|
isLoading={isLoading}
|
||||||
|
className="btn btn-sm btn-info"
|
||||||
|
>
|
||||||
|
Moodle-Check
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* Activity Log */}
|
||||||
|
<div className="card bg-base-200 min-w-[300px] flex-1 shadow-lg">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title text-lg">
|
||||||
|
<Activity className="h-5 w-5" /> Aktivitätslog
|
||||||
|
</h2>
|
||||||
|
|
||||||
|
<div className="timeline timeline-vertical">
|
||||||
|
<table className="table-sm table w-full">
|
||||||
|
<thead>
|
||||||
|
<tr>
|
||||||
|
<th>Datum</th>
|
||||||
|
<th>Event</th>
|
||||||
|
<th>User</th>
|
||||||
|
</tr>
|
||||||
|
</thead>
|
||||||
|
<tbody>
|
||||||
|
{participant?.statusLog &&
|
||||||
|
Array.isArray(participant.statusLog) &&
|
||||||
|
(participant.statusLog as InputJsonValueType[])
|
||||||
|
.slice()
|
||||||
|
.reverse()
|
||||||
|
.map((log: InputJsonValueType, index: number) => {
|
||||||
|
const logEntry = log as unknown as ParticipantLog;
|
||||||
|
return (
|
||||||
|
<tr key={index}>
|
||||||
|
<td>
|
||||||
|
{logEntry.timestamp
|
||||||
|
? new Date(logEntry.timestamp).toLocaleDateString()
|
||||||
|
: "-"}
|
||||||
|
</td>
|
||||||
|
<td>{logEntry.event || "-"}</td>
|
||||||
|
<td>{logEntry.user || "-"}</td>
|
||||||
|
</tr>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</tbody>
|
||||||
|
</table>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 w-full shadow-xl">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex w-full gap-4">
|
||||||
|
<Button
|
||||||
|
disabled={!form.formState.isDirty}
|
||||||
|
isLoading={form.formState.isSubmitting}
|
||||||
|
type="submit"
|
||||||
|
className="btn btn-primary flex-1"
|
||||||
|
>
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{event && (
|
||||||
|
<Button
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteParticipantMutation.mutateAsync(participant.id);
|
||||||
|
redirect(`/admin/event/${event.id}`);
|
||||||
|
}}
|
||||||
|
className="btn btn-error"
|
||||||
|
>
|
||||||
|
Austragen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
);
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user