Merge branch 'main' of https://github.com/VAR-Virtual-Air-Rescue/var-monorepo
This commit is contained in:
@@ -8,6 +8,19 @@ export default async ({ params }: { params: Promise<{ id: string }> }) => {
|
|||||||
id: parseInt(id),
|
id: parseInt(id),
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstname: true,
|
||||||
|
lastname: true,
|
||||||
|
publicId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const appointments = await prisma.eventAppointment.findMany({
|
||||||
|
where: {
|
||||||
|
eventId: parseInt(id),
|
||||||
|
},
|
||||||
|
});
|
||||||
if (!event) return <div>Event not found</div>;
|
if (!event) return <div>Event not found</div>;
|
||||||
return <Form event={event} />;
|
return <Form event={event} users={users} appointments={appointments} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,28 +1,97 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from '@hookform/resolvers/zod';
|
||||||
import { EventOptionalDefaultsSchema } from '@repo/db/zod';
|
import {
|
||||||
|
EventAppointmentOptionalDefaults,
|
||||||
|
EventAppointmentOptionalDefaultsSchema,
|
||||||
|
EventOptionalDefaults,
|
||||||
|
EventOptionalDefaultsSchema,
|
||||||
|
ParticipantOptionalDefaultsSchema,
|
||||||
|
} from '@repo/db/zod';
|
||||||
import { set, useForm } from 'react-hook-form';
|
import { set, useForm } from 'react-hook-form';
|
||||||
import { z } from 'zod';
|
import { BADGES, Event, EventAppointment, User } from '@repo/db';
|
||||||
import { BosUse, Country, Event, prisma } from '@repo/db';
|
import { Bot, Calendar, FileText, UserIcon } from 'lucide-react';
|
||||||
import { FileText, LocateIcon, PlaneIcon, UserIcon } from 'lucide-react';
|
|
||||||
import { Input } from '../../../../_components/ui/Input';
|
import { Input } from '../../../../_components/ui/Input';
|
||||||
import { useState } from 'react';
|
import { useRef, useState } from 'react';
|
||||||
import { deleteEvent, upsertEvent } from '../action';
|
import { deleteEvent, upsertAppointment, upsertEvent } from '../action';
|
||||||
import { Button } from '../../../../_components/ui/Button';
|
import { Button } from '../../../../_components/ui/Button';
|
||||||
import { redirect } from 'next/navigation';
|
import { redirect, useRouter } from 'next/navigation';
|
||||||
import { Switch } from '../../../../_components/ui/Switch';
|
import { Switch } from '../../../../_components/ui/Switch';
|
||||||
import { PaginatedTable } from '../../../../_components/PaginatedTable';
|
import {
|
||||||
|
PaginatedTable,
|
||||||
|
PaginatedTableRef,
|
||||||
|
} from '../../../../_components/PaginatedTable';
|
||||||
|
import { Select } from '../../../../_components/ui/Select';
|
||||||
|
import { useSession } from 'next-auth/react';
|
||||||
|
|
||||||
export const Form = ({ event }: { event?: Event }) => {
|
export const Form = ({
|
||||||
const form = useForm<z.infer<typeof EventOptionalDefaultsSchema>>({
|
event,
|
||||||
|
users,
|
||||||
|
appointments = [],
|
||||||
|
}: {
|
||||||
|
event?: Event;
|
||||||
|
users: {
|
||||||
|
id: string;
|
||||||
|
firstname: string;
|
||||||
|
lastname: string;
|
||||||
|
publicId: string;
|
||||||
|
}[];
|
||||||
|
appointments?: EventAppointment[];
|
||||||
|
}) => {
|
||||||
|
const { data: session } = useSession();
|
||||||
|
const form = useForm({
|
||||||
resolver: zodResolver(EventOptionalDefaultsSchema),
|
resolver: zodResolver(EventOptionalDefaultsSchema),
|
||||||
defaultValues: event,
|
defaultValues: event,
|
||||||
});
|
});
|
||||||
|
const appointmentForm = useForm<EventAppointmentOptionalDefaults>({
|
||||||
|
resolver: zodResolver(EventAppointmentOptionalDefaultsSchema),
|
||||||
|
defaultValues: {
|
||||||
|
eventId: event?.id,
|
||||||
|
presenterId: session?.user?.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
console.log(form.formState.errors);
|
const addParticipantModal = useRef<HTMLDialogElement>(null);
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
|
<dialog ref={addParticipantModal} className="modal">
|
||||||
|
<div className="modal-box">
|
||||||
|
<form method="dialog">
|
||||||
|
{/* if there is a button in form, it will close the modal */}
|
||||||
|
<button className="btn btn-sm btn-circle btn-ghost absolute right-2 top-2">
|
||||||
|
✕
|
||||||
|
</button>
|
||||||
|
</form>
|
||||||
|
<h3 className="font-bold text-lg">Termin hinzufügen</h3>
|
||||||
|
<form
|
||||||
|
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||||
|
if (!event) return;
|
||||||
|
const createdAppointment = await upsertAppointment({
|
||||||
|
appointmentDate: values.appointmentDate,
|
||||||
|
eventId: event?.id,
|
||||||
|
presenterId: values.presenterId,
|
||||||
|
});
|
||||||
|
console.log(createdAppointment);
|
||||||
|
addParticipantModal.current?.close();
|
||||||
|
appointmentsTableRef.current?.refresh();
|
||||||
|
})}
|
||||||
|
className="flex flex-col"
|
||||||
|
>
|
||||||
|
<Input
|
||||||
|
form={appointmentForm}
|
||||||
|
label="Datum"
|
||||||
|
name="appointmentDate"
|
||||||
|
type="date"
|
||||||
|
/>
|
||||||
|
<div className="modal-action">
|
||||||
|
<Button type="submit" className="btn btn-primary">
|
||||||
|
Hinzufügen
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</form>
|
||||||
|
</div>
|
||||||
|
</dialog>
|
||||||
<form
|
<form
|
||||||
onSubmit={form.handleSubmit(async (values) => {
|
onSubmit={form.handleSubmit(async (values) => {
|
||||||
setLoading(true);
|
setLoading(true);
|
||||||
@@ -44,41 +113,134 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
name="description"
|
name="description"
|
||||||
className="input-sm"
|
className="input-sm"
|
||||||
/>
|
/>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
label="Maximale Teilnehmer (Nur für live Events)"
|
||||||
|
className="input-sm"
|
||||||
|
{...form.register('maxParticipants', {
|
||||||
|
valueAsNumber: true,
|
||||||
|
})}
|
||||||
|
/>
|
||||||
<Switch form={form} name="hidden" label="Versteckt" />
|
<Switch form={form} name="hidden" label="Versteckt" />
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
|
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
|
||||||
<div className="card-body">
|
<div className="card-body">
|
||||||
<h2 className="card-title">
|
<h2 className="card-title">
|
||||||
<UserIcon className="w-5 h-5" /> Teilnehmer
|
<Bot className="w-5 h-5" /> Automation
|
||||||
</h2>
|
</h2>
|
||||||
|
<Input
|
||||||
|
form={form}
|
||||||
|
name="starterMoodleCourseId"
|
||||||
|
label="Moodle Anmelde Kurs ID"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Input
|
||||||
|
name="finisherMoodleCourseId"
|
||||||
|
form={form}
|
||||||
|
label="Moodle Abschluss Kurs ID"
|
||||||
|
className="input-sm"
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
form={form}
|
||||||
|
name="finishedBadges"
|
||||||
|
label="Badges bei Abschluss"
|
||||||
|
options={Object.entries(BADGES).map(([key, value]) => ({
|
||||||
|
label: value,
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Select
|
||||||
|
isMulti
|
||||||
|
form={form}
|
||||||
|
name="requiredBadges"
|
||||||
|
label="Benötigte Badges"
|
||||||
|
options={Object.entries(BADGES).map(([key, value]) => ({
|
||||||
|
label: value,
|
||||||
|
value: key,
|
||||||
|
}))}
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
form={form}
|
||||||
|
name="hasPresenceEvents"
|
||||||
|
label="Hat Live Event"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<div className="card bg-base-200 shadow-xl col-span-2 max-xl:col-span-6">
|
||||||
|
<div className="card-body">
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<h2 className="card-title">
|
||||||
|
<Calendar className="w-5 h-5" /> Termine
|
||||||
|
</h2>
|
||||||
|
<button
|
||||||
|
className="btn btn-primary btn-outline"
|
||||||
|
onClick={() => addParticipantModal.current?.showModal()}
|
||||||
|
>
|
||||||
|
Hinzufügen
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
prismaModel={'participant'}
|
ref={appointmentsTableRef}
|
||||||
|
prismaModel={'eventAppointment'}
|
||||||
filter={{
|
filter={{
|
||||||
eventId: event?.id,
|
eventId: event?.id,
|
||||||
}}
|
}}
|
||||||
include={[
|
include={{
|
||||||
{
|
Presenter: true,
|
||||||
user: true,
|
Participants: true,
|
||||||
},
|
}}
|
||||||
]}
|
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: 'Vorname',
|
header: 'Datum',
|
||||||
accessorKey: 'user.firstName',
|
accessorKey: 'appointmentDate',
|
||||||
|
accessorFn: (date) =>
|
||||||
|
new Date(date.appointmentDate).toLocaleDateString(),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Nachname',
|
header: 'Presenter',
|
||||||
accessorKey: 'user.lastname',
|
accessorKey: 'presenter',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<span className="ml-2">
|
||||||
|
{(row.original as any).Presenter.firstname}{' '}
|
||||||
|
{(row.original as any).Presenter.lastname}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'VAR ID',
|
header: 'Teilnehmer',
|
||||||
accessorKey: 'user.publicId',
|
accessorKey: 'Participants',
|
||||||
|
cell: ({ row }) => (
|
||||||
|
<div className="flex items-center">
|
||||||
|
<UserIcon className="w-5 h-5" />
|
||||||
|
<span className="ml-2">
|
||||||
|
{row.original.Participants.length}
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
),
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: 'Status',
|
header: 'Aktionen',
|
||||||
accessorKey: 'status',
|
cell: ({ row }) => {
|
||||||
|
return (
|
||||||
|
<div className="flex gap-2">
|
||||||
|
<button
|
||||||
|
onSubmit={() => false}
|
||||||
|
type="button"
|
||||||
|
onClick={() => {
|
||||||
|
console.log(row.original);
|
||||||
|
}}
|
||||||
|
className="btn btn-sm btn-outline"
|
||||||
|
>
|
||||||
|
Bearbeiten
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
},
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
'use server';
|
'use server';
|
||||||
|
|
||||||
import { prisma, Prisma, Event } from '@repo/db';
|
import { prisma, Prisma, Event, Participant, EventAppointment } from '@repo/db';
|
||||||
|
|
||||||
export const upsertEvent = async (
|
export const upsertEvent = async (
|
||||||
event: Prisma.EventCreateInput,
|
event: Prisma.EventCreateInput,
|
||||||
@@ -18,3 +18,48 @@ export const upsertEvent = async (
|
|||||||
export const deleteEvent = async (id: Event['id']) => {
|
export const deleteEvent = async (id: Event['id']) => {
|
||||||
await prisma.event.delete({ where: { id: id } });
|
await prisma.event.delete({ where: { id: id } });
|
||||||
};
|
};
|
||||||
|
|
||||||
|
export const upsertAppointment = async (
|
||||||
|
eventAppointment: Prisma.XOR<
|
||||||
|
Prisma.EventAppointmentCreateInput,
|
||||||
|
Prisma.EventAppointmentUncheckedCreateInput
|
||||||
|
>,
|
||||||
|
id?: EventAppointment['id']
|
||||||
|
) => {
|
||||||
|
const newEventAppointment = id
|
||||||
|
? await prisma.eventAppointment.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: eventAppointment,
|
||||||
|
})
|
||||||
|
: await prisma.eventAppointment.create({ data: eventAppointment });
|
||||||
|
return newEventAppointment;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteAppoinement = async (id: Event['id']) => {
|
||||||
|
await prisma.eventAppointment.delete({ where: { id: id } });
|
||||||
|
prisma.eventAppointment.findMany({
|
||||||
|
where: {
|
||||||
|
eventId: id,
|
||||||
|
},
|
||||||
|
orderBy: {
|
||||||
|
// TODO: add order by in relation to table selected column
|
||||||
|
},
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
export const upsertParticipant = async (
|
||||||
|
participant: Prisma.ParticipantCreateInput,
|
||||||
|
id?: Participant['id']
|
||||||
|
) => {
|
||||||
|
const newParticipant = id
|
||||||
|
? await prisma.participant.update({
|
||||||
|
where: { id: id },
|
||||||
|
data: participant,
|
||||||
|
})
|
||||||
|
: await prisma.participant.create({ data: participant });
|
||||||
|
return newParticipant;
|
||||||
|
};
|
||||||
|
|
||||||
|
export const deleteParticipant = async (id: Participant['id']) => {
|
||||||
|
await prisma.participant.delete({ where: { id: id } });
|
||||||
|
};
|
||||||
|
|||||||
@@ -1,5 +1,14 @@
|
|||||||
|
import { prisma } from '@repo/db';
|
||||||
import { Form } from '../_components/Form';
|
import { Form } from '../_components/Form';
|
||||||
|
|
||||||
export default () => {
|
export default async () => {
|
||||||
return <Form />;
|
const users = await prisma.user.findMany({
|
||||||
|
select: {
|
||||||
|
id: true,
|
||||||
|
firstname: true,
|
||||||
|
lastname: true,
|
||||||
|
publicId: true,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
return <Form users={users} />;
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,16 +5,6 @@ import Link from 'next/link';
|
|||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
|
||||||
<span className="flex items-center gap-2">
|
|
||||||
<PartyPopperIcon className="w-5 h-5" /> Events
|
|
||||||
</span>
|
|
||||||
<Link href={'/admin/event/new'}>
|
|
||||||
<button className="btn btn-sm btn-outline btn-primary">
|
|
||||||
Erstellen
|
|
||||||
</button>
|
|
||||||
</Link>
|
|
||||||
</p>
|
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
showEditButton
|
showEditButton
|
||||||
prismaModel="event"
|
prismaModel="event"
|
||||||
@@ -28,6 +18,18 @@ export default () => {
|
|||||||
accessorKey: 'hidden',
|
accessorKey: 'hidden',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
leftOfSearch={
|
||||||
|
<span className="flex items-center gap-2">
|
||||||
|
<PartyPopperIcon className="w-5 h-5" /> Events
|
||||||
|
</span>
|
||||||
|
}
|
||||||
|
rightOfSearch={
|
||||||
|
<Link href={'/admin/event/new'}>
|
||||||
|
<button className="btn btn-sm btn-outline btn-primary">
|
||||||
|
Erstellen
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,42 +1,46 @@
|
|||||||
import { DatabaseBackupIcon } from "lucide-react";
|
import { DatabaseBackupIcon } from 'lucide-react';
|
||||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||||
import Link from "next/link";
|
import Link from 'next/link';
|
||||||
|
|
||||||
export default () => {
|
export default () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-semibold text-left flex items-center gap-2 justify-between">
|
<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">
|
<span className="flex items-center gap-2">
|
||||||
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
|
<DatabaseBackupIcon className="w-5 h-5" /> Stationen
|
||||||
</span>
|
</span>
|
||||||
<Link href={"/admin/station/new"}>
|
}
|
||||||
|
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">
|
<button className="btn btn-sm btn-outline btn-primary">
|
||||||
Erstellen
|
Erstellen
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
</p>
|
</p>
|
||||||
<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",
|
|
||||||
},
|
|
||||||
]}
|
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
@@ -1,34 +1,36 @@
|
|||||||
import { User2 } from "lucide-react";
|
import { User2 } from 'lucide-react';
|
||||||
import { PaginatedTable } from "../../../_components/PaginatedTable";
|
import { PaginatedTable } from '../../../_components/PaginatedTable';
|
||||||
|
|
||||||
export default async () => {
|
export default async () => {
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
|
||||||
<User2 className="w-5 h-5" /> Benutzer
|
|
||||||
</p>
|
|
||||||
<PaginatedTable
|
<PaginatedTable
|
||||||
showEditButton
|
showEditButton
|
||||||
prismaModel="user"
|
prismaModel="user"
|
||||||
searchFields={["publicId", "firstname", "lastname", "email"]}
|
searchFields={['publicId', 'firstname', 'lastname', 'email']}
|
||||||
columns={[
|
columns={[
|
||||||
{
|
{
|
||||||
header: "ID",
|
header: 'ID',
|
||||||
accessorKey: "publicId",
|
accessorKey: 'publicId',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Vorname",
|
header: 'Vorname',
|
||||||
accessorKey: "firstname",
|
accessorKey: 'firstname',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Nachname",
|
header: 'Nachname',
|
||||||
accessorKey: "lastname",
|
accessorKey: 'lastname',
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
header: "Email",
|
header: 'Email',
|
||||||
accessorKey: "email",
|
accessorKey: 'email',
|
||||||
},
|
},
|
||||||
]}
|
]}
|
||||||
|
leftOfSearch={
|
||||||
|
<p className="text-2xl font-semibold text-left flex items-center gap-2">
|
||||||
|
<User2 className="w-5 h-5" /> Benutzer
|
||||||
|
</p>
|
||||||
|
}
|
||||||
/>
|
/>
|
||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|||||||
93
apps/hub/app/(app)/events/_components/item.tsx
Normal file
93
apps/hub/app/(app)/events/_components/item.tsx
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
"use client";
|
||||||
|
import { DrawingPinFilledIcon, EnterIcon } from "@radix-ui/react-icons";
|
||||||
|
import { User } from "@repo/db";
|
||||||
|
import ModalBtn from "./modalBtn";
|
||||||
|
|
||||||
|
export const KursItem = ({
|
||||||
|
user,
|
||||||
|
title,
|
||||||
|
type,
|
||||||
|
beschreibung,
|
||||||
|
badge,
|
||||||
|
moodleReq,
|
||||||
|
}: {
|
||||||
|
user: User;
|
||||||
|
title: string;
|
||||||
|
type: string;
|
||||||
|
beschreibung: string;
|
||||||
|
badge: string;
|
||||||
|
moodleReq: boolean;
|
||||||
|
}) => {
|
||||||
|
return (
|
||||||
|
<div className="col-span-full">
|
||||||
|
<div className="card bg-base-200 shadow-xl mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">{title}</h2>
|
||||||
|
<div className="absolute top-0 right-0 m-4">
|
||||||
|
<span className="badge badge-info badge-outline">
|
||||||
|
Zusatzqualifikation
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-6 gap-4">
|
||||||
|
<div className="col-span-4">
|
||||||
|
<p className="text-left text-balance">{beschreibung}</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">{badge}</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions flex justify-between items-center mt-5">
|
||||||
|
<p className="text-gray-600 text-left flex items-center gap-2">
|
||||||
|
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b>
|
||||||
|
<a className="link link-info" href="">
|
||||||
|
Moodle Kurs /MOODLEKURSTITLE\
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<ModalBtn
|
||||||
|
title={title}
|
||||||
|
dates={["Dienstag, 25 Februar 2025", "Mittwoch, 26 Februar 2025"]}
|
||||||
|
modalId={title + "_modal" + Math.random()}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const PilotKurs = ({ user }: { user: User }) => {
|
||||||
|
{
|
||||||
|
/* STATISCH, DA FÜR ALLE NEUEN MITGLIEDER MANDATORY, WIRD AUSGEBLENDET WENN ABSOLVIERT */
|
||||||
|
}
|
||||||
|
return (
|
||||||
|
<div className="col-span-full">
|
||||||
|
<div className="card card-bordered border-secondary bg-base-200 shadow-xl mb-4">
|
||||||
|
<div className="card-body">
|
||||||
|
<h2 className="card-title">Einsteigerkurs für Piloten</h2>
|
||||||
|
<div className="absolute top-0 right-0 m-4">
|
||||||
|
<span className="badge badge-secondary badge-outline">
|
||||||
|
Verpflichtend
|
||||||
|
</span>
|
||||||
|
</div>
|
||||||
|
<div className="grid grid-cols-6 gap-4">
|
||||||
|
<div className="col-span-4">
|
||||||
|
<p className="text-left text-balance">
|
||||||
|
In diesem Kurs lernen Piloten die Grundlagen der Luftrettung,
|
||||||
|
Einsatzverfahren, den Umgang mit dem BOS-Funk und einige
|
||||||
|
medizinische Basics. Der Kurs bietet eine ideale Vorbereitung
|
||||||
|
für alle Standard Operations bei Virtual Air Rescue.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="col-span-2">Badge</div>
|
||||||
|
</div>
|
||||||
|
<div className="card-actions flex justify-between items-center mt-5">
|
||||||
|
<p className="text-gray-600 text-left flex items-center gap-2">
|
||||||
|
<DrawingPinFilledIcon /> <b>Teilnahmevoraussetzungen:</b> Keine
|
||||||
|
</p>
|
||||||
|
<button className="btn btn-outline btn-secondary btn-wide">
|
||||||
|
<EnterIcon /> Zum Moodle Kurs
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
87
apps/hub/app/(app)/events/_components/modalBtn.tsx
Normal file
87
apps/hub/app/(app)/events/_components/modalBtn.tsx
Normal file
@@ -0,0 +1,87 @@
|
|||||||
|
"use client";
|
||||||
|
import { useEffect } from "react";
|
||||||
|
import {
|
||||||
|
CheckCircledIcon,
|
||||||
|
CalendarIcon,
|
||||||
|
EnterIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
interface ModalBtnProps {
|
||||||
|
title: string;
|
||||||
|
dates: string[];
|
||||||
|
modalId: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const ModalBtn = ({ title, dates, modalId }: ModalBtnProps) => {
|
||||||
|
useEffect(() => {
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
|
const handleOpen = () => {
|
||||||
|
document.body.classList.add("modal-open");
|
||||||
|
};
|
||||||
|
const handleClose = () => {
|
||||||
|
document.body.classList.remove("modal-open");
|
||||||
|
};
|
||||||
|
modal?.addEventListener("show", handleOpen);
|
||||||
|
modal?.addEventListener("close", handleClose);
|
||||||
|
return () => {
|
||||||
|
modal?.removeEventListener("show", handleOpen);
|
||||||
|
modal?.removeEventListener("close", handleClose);
|
||||||
|
};
|
||||||
|
}, [modalId]);
|
||||||
|
|
||||||
|
const openModal = () => {
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
|
document.body.classList.add("modal-open");
|
||||||
|
modal?.showModal();
|
||||||
|
};
|
||||||
|
|
||||||
|
const closeModal = () => {
|
||||||
|
const modal = document.getElementById(modalId) as HTMLDialogElement;
|
||||||
|
document.body.classList.remove("modal-open");
|
||||||
|
modal?.close();
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button className="btn btn-outline btn-info btn-wide" onClick={openModal}>
|
||||||
|
<EnterIcon /> Anmelden
|
||||||
|
</button>
|
||||||
|
<dialog id={modalId} className="modal">
|
||||||
|
<div className="modal-box">
|
||||||
|
<h3 className="font-bold text-lg">{title}</h3>
|
||||||
|
<p className="py-4 flex items-center gap-2 justify-center">
|
||||||
|
<CheckCircledIcon className="text-success" />
|
||||||
|
Moodle Kurs abgeschlossen
|
||||||
|
</p>
|
||||||
|
<div>
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<CalendarIcon />
|
||||||
|
<select className="select w-full max-w-xs" defaultValue={0}>
|
||||||
|
<option disabled>Bitte wähle einen Termin aus</option>
|
||||||
|
{dates.map((date, index) => (
|
||||||
|
<option key={index}>{date}</option>
|
||||||
|
))}
|
||||||
|
</select>
|
||||||
|
</div>
|
||||||
|
<p className="mt-3 text-center">
|
||||||
|
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<div className="modal-action flex justify-between">
|
||||||
|
<button className="btn" onClick={closeModal}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
<button className="btn btn-info btn-outline btn-wide">
|
||||||
|
<EnterIcon /> Anmelden
|
||||||
|
</button>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
<button className="modal-backdrop" onClick={closeModal}>
|
||||||
|
Abbrechen
|
||||||
|
</button>
|
||||||
|
</dialog>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export default ModalBtn;
|
||||||
45
apps/hub/app/(app)/events/page.tsx
Normal file
45
apps/hub/app/(app)/events/page.tsx
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
import { getServerSession } from "../../api/auth/[...nextauth]/auth";
|
||||||
|
import { PrismaClient } from "@repo/db";
|
||||||
|
import { PilotKurs, KursItem } from "./_components/item";
|
||||||
|
import {
|
||||||
|
RocketIcon,
|
||||||
|
DrawingPinFilledIcon,
|
||||||
|
EnterIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
|
||||||
|
export default async () => {
|
||||||
|
const prisma = new PrismaClient();
|
||||||
|
const session = await getServerSession();
|
||||||
|
if (!session) return null;
|
||||||
|
const user = await prisma.user.findFirst({
|
||||||
|
where: {
|
||||||
|
id: session.user.id,
|
||||||
|
},
|
||||||
|
});
|
||||||
|
if (!user) return null;
|
||||||
|
return (
|
||||||
|
<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">
|
||||||
|
<RocketIcon className="w-5 h-5" /> Events & Kurse
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
<PilotKurs user={user} />
|
||||||
|
<KursItem
|
||||||
|
user={user}
|
||||||
|
title="Einsteigerkurs für Disponenten"
|
||||||
|
type="1"
|
||||||
|
beschreibung="In diesem Kurs lernen Teilnehmer die Aufgaben eines
|
||||||
|
Disponenten in der Rettungsfliegerei kennen. Dazu gehören die
|
||||||
|
Koordination von Notfalleinsätzen, die effiziente Planung von
|
||||||
|
Ressourcen und die Kommunikation mit Piloten sowie
|
||||||
|
Rettungsdiensten. Der Kurs vermittelt praxisnahe Kenntnisse
|
||||||
|
für die schnelle und präzise Entscheidungsfindung unter
|
||||||
|
Zeitdruck, um eine reibungslose Abwicklung von
|
||||||
|
Rettungseinsätzen zu gewährleisten."
|
||||||
|
badge="Badge"
|
||||||
|
moodleReq={true}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
@@ -4,8 +4,11 @@ import {
|
|||||||
InstagramLogoIcon,
|
InstagramLogoIcon,
|
||||||
ReaderIcon,
|
ReaderIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { HorizontalNav, VerticalNav } from "../_components/ui/Nav";
|
import { HorizontalNav, VerticalNav } from "../_components/Nav";
|
||||||
import { Toaster } from "react-hot-toast";
|
import { Toaster } from "react-hot-toast";
|
||||||
|
import { redirect } from "next/navigation";
|
||||||
|
import { getServerSession } from "../api/auth/[...nextauth]/auth";
|
||||||
|
import { headers } from "next/headers";
|
||||||
|
|
||||||
export const metadata: Metadata = {
|
export const metadata: Metadata = {
|
||||||
title: "Create Next App",
|
title: "Create Next App",
|
||||||
@@ -17,6 +20,10 @@ export default async function RootLayout({
|
|||||||
}: Readonly<{
|
}: Readonly<{
|
||||||
children: React.ReactNode;
|
children: React.ReactNode;
|
||||||
}>) {
|
}>) {
|
||||||
|
const session = await getServerSession();
|
||||||
|
|
||||||
|
if (!session) redirect(`/login`);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div
|
<div
|
||||||
className="hero min-h-screen"
|
className="hero min-h-screen"
|
||||||
@@ -35,12 +42,12 @@ export default async function RootLayout({
|
|||||||
<HorizontalNav />
|
<HorizontalNav />
|
||||||
|
|
||||||
{/* Hauptlayout: Sidebar + Content (nimmt Resthöhe ein) */}
|
{/* Hauptlayout: Sidebar + Content (nimmt Resthöhe ein) */}
|
||||||
<div className="flex flex-grow overflow-hidden">
|
<div className="flex grow overflow-hidden">
|
||||||
{/* Linke Sidebar */}
|
{/* Linke Sidebar */}
|
||||||
<VerticalNav />
|
<VerticalNav />
|
||||||
|
|
||||||
{/* Scrollbarer Content-Bereich */}
|
{/* Scrollbarer Content-Bereich */}
|
||||||
<div className="flex-grow bg-base-100 p-6 rounded-lg shadow-md ml-4 overflow-auto h-full">
|
<div className="flex-grow bg-base-100 p-6 rounded-lg shadow-md ml-4 overflow-auto h-full max-w-full w-full">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { DiscordAccount, User } from '@repo/db';
|
import { DiscordAccount, User } from "@repo/db";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from "react-hook-form";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
import { unlinkDiscord, updateUser, changePassword } from '../actions';
|
import { unlinkDiscord, updateUser, changePassword } from "../actions";
|
||||||
import { Toaster, toast } from 'react-hot-toast';
|
import { Toaster, toast } from "react-hot-toast";
|
||||||
import { useSession } from 'next-auth/react';
|
import { useSession } from "next-auth/react";
|
||||||
import { useRouter } from 'next/navigation';
|
import { useRouter } from "next/navigation";
|
||||||
import { Button } from '../../../_components/ui/Button';
|
import { Button } from "../../../_components/ui/Button";
|
||||||
import {
|
import {
|
||||||
PersonIcon,
|
PersonIcon,
|
||||||
EnvelopeClosedIcon,
|
EnvelopeClosedIcon,
|
||||||
@@ -20,14 +20,14 @@ import {
|
|||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
LockOpen2Icon,
|
LockOpen2Icon,
|
||||||
LockOpen1Icon,
|
LockOpen1Icon,
|
||||||
} from '@radix-ui/react-icons';
|
} from "@radix-ui/react-icons";
|
||||||
|
|
||||||
export const ProfileForm = ({ user }: { user: User }) => {
|
export const ProfileForm = ({ user }: { user: User }) => {
|
||||||
const schema = z.object({
|
const schema = z.object({
|
||||||
firstname: z.string().min(2).max(30),
|
firstname: z.string().min(2).max(30),
|
||||||
lastname: z.string().min(2).max(30),
|
lastname: z.string().min(2).max(30),
|
||||||
email: z.string().email({
|
email: z.string().email({
|
||||||
message: 'Bitte gebe eine gültige E-Mail Adresse ein',
|
message: "Bitte gebe eine gültige E-Mail Adresse ein",
|
||||||
}),
|
}),
|
||||||
});
|
});
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -49,12 +49,10 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
await updateUser(values);
|
await updateUser(values);
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
toast.success('Deine Änderungen wurden gespeichert!', {
|
toast.success("Deine Änderungen wurden gespeichert!", {
|
||||||
style: {
|
style: {
|
||||||
background:
|
background: "var(--color-base-100)",
|
||||||
'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
color: "var(--color-base-content)",
|
||||||
color:
|
|
||||||
'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
@@ -62,15 +60,13 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
<h2 className="card-title">
|
<h2 className="card-title">
|
||||||
<MixerHorizontalIcon className="w-5 h-5" /> Persönliche Informationen
|
<MixerHorizontalIcon className="w-5 h-5" /> Persönliche Informationen
|
||||||
</h2>
|
</h2>
|
||||||
<div className="">
|
<div className="text-left">
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full mb-5 mt-5">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<PersonIcon /> Vorname
|
<PersonIcon /> Vorname
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register('firstname')}
|
{...form.register("firstname")}
|
||||||
type="text"
|
type="text"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
defaultValue={user.firstname}
|
defaultValue={user.firstname}
|
||||||
@@ -82,14 +78,12 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
{form.formState.errors.firstname.message}
|
{form.formState.errors.firstname.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full mb-5">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<PersonIcon /> Nachname
|
<PersonIcon /> Nachname
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register('lastname')}
|
{...form.register("lastname")}
|
||||||
type="text"
|
type="text"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
defaultValue={user.lastname}
|
defaultValue={user.lastname}
|
||||||
@@ -101,14 +95,12 @@ export const ProfileForm = ({ user }: { user: User }) => {
|
|||||||
{form.formState.errors.lastname?.message}
|
{form.formState.errors.lastname?.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<EnvelopeClosedIcon /> E-Mail
|
<EnvelopeClosedIcon /> E-Mail
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register('email')}
|
{...form.register("email")}
|
||||||
type="text"
|
type="text"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
defaultValue={user.email}
|
defaultValue={user.email}
|
||||||
@@ -158,6 +150,7 @@ export const SocialForm = ({
|
|||||||
});
|
});
|
||||||
|
|
||||||
if (!user) return null;
|
if (!user) return null;
|
||||||
|
console.log("Dirty", form.formState.isDirty);
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="card-body"
|
className="card-body"
|
||||||
@@ -170,12 +163,10 @@ export const SocialForm = ({
|
|||||||
});
|
});
|
||||||
setVatsimLoading(false);
|
setVatsimLoading(false);
|
||||||
form.reset(values);
|
form.reset(values);
|
||||||
toast.success('Deine Änderungen wurden gespeichert!', {
|
toast.success("Deine Änderungen wurden gespeichert!", {
|
||||||
style: {
|
style: {
|
||||||
background:
|
background: "var(--color-base-100)",
|
||||||
'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
color: "var(--color-base-content)",
|
||||||
color:
|
|
||||||
'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
})}
|
})}
|
||||||
@@ -207,12 +198,16 @@ export const SocialForm = ({
|
|||||||
Verbunden mit {discordAccount.username}
|
Verbunden mit {discordAccount.username}
|
||||||
</span>
|
</span>
|
||||||
<span className="hidden group-hover:inline">
|
<span className="hidden group-hover:inline">
|
||||||
Verbindung trennen{isLoading && '...'}
|
Verbindung trennen{isLoading && "..."}
|
||||||
</span>
|
</span>
|
||||||
</Button>
|
</Button>
|
||||||
) : (
|
) : (
|
||||||
<a href={process.env.NEXT_PUBLIC_DISCORD_URL}>
|
<a href={process.env.NEXT_PUBLIC_DISCORD_URL}>
|
||||||
<button className="btn btn-primary btn-block">
|
<button
|
||||||
|
type="button"
|
||||||
|
className="btn btn-primary btn-block"
|
||||||
|
onSubmit={() => false}
|
||||||
|
>
|
||||||
<DiscordLogoIcon className="w-5 h-5" /> Mit Discord verbinden
|
<DiscordLogoIcon className="w-5 h-5" /> Mit Discord verbinden
|
||||||
</button>
|
</button>
|
||||||
</a>
|
</a>
|
||||||
@@ -220,18 +215,16 @@ export const SocialForm = ({
|
|||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
<div className="content-center">
|
<div className="content-center">
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full mt-5">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<PaperPlaneIcon /> VATSIM-CID
|
<PaperPlaneIcon /> VATSIM-CID
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
type="number"
|
type="number"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
placeholder="1445241"
|
placeholder="1445241"
|
||||||
defaultValue={user.vatsimCid as number | undefined}
|
defaultValue={user.vatsimCid as number | undefined}
|
||||||
{...form.register('vatsimCid', {
|
{...form.register("vatsimCid", {
|
||||||
valueAsNumber: true,
|
valueAsNumber: true,
|
||||||
})}
|
})}
|
||||||
/>
|
/>
|
||||||
@@ -269,7 +262,6 @@ export const PasswordForm = ({ user }: { user: User }) => {
|
|||||||
defaultValues: {},
|
defaultValues: {},
|
||||||
resolver: zodResolver(schema),
|
resolver: zodResolver(schema),
|
||||||
});
|
});
|
||||||
console.log(form.formState.errors);
|
|
||||||
return (
|
return (
|
||||||
<form
|
<form
|
||||||
className="card-body"
|
className="card-body"
|
||||||
@@ -282,17 +274,15 @@ export const PasswordForm = ({ user }: { user: User }) => {
|
|||||||
form.reset(values);
|
form.reset(values);
|
||||||
setIsLoading(false);
|
setIsLoading(false);
|
||||||
if (result.error) {
|
if (result.error) {
|
||||||
form.setError('password', {
|
form.setError("password", {
|
||||||
type: 'manual',
|
type: "manual",
|
||||||
message: result.error,
|
message: result.error,
|
||||||
});
|
});
|
||||||
} else if (result.success) {
|
} else if (result.success) {
|
||||||
toast.success('Dein Passwort wurde geändert!', {
|
toast.success("Dein Passwort wurde geändert!", {
|
||||||
style: {
|
style: {
|
||||||
background:
|
background: "var(--color-base-100)",
|
||||||
'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
color: "var(--color-base-content)",
|
||||||
color:
|
|
||||||
'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -302,33 +292,29 @@ export const PasswordForm = ({ user }: { user: User }) => {
|
|||||||
<LockClosedIcon className="w-5 h-5" /> Password Ändern
|
<LockClosedIcon className="w-5 h-5" /> Password Ändern
|
||||||
</h2>
|
</h2>
|
||||||
<div className="">
|
<div className="">
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full mt-5 mb-5">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<LockOpen2Icon /> Aktuelles Passwort
|
<LockOpen2Icon /> Aktuelles Passwort
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register('password')}
|
{...form.register("password")}
|
||||||
type="text"
|
type="text"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
defaultValue={''}
|
defaultValue={""}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{form.formState.errors.password && (
|
{form.formState.errors.password && (
|
||||||
<p className="text-error">{form.formState.errors.password.message}</p>
|
<p className="text-error">{form.formState.errors.password.message}</p>
|
||||||
)}
|
)}
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full mb-5">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<LockOpen1Icon /> Neues Passwort
|
<LockOpen1Icon /> Neues Passwort
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register('newPassword')}
|
{...form.register("newPassword")}
|
||||||
type="text"
|
type="text"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
defaultValue={''}
|
defaultValue={""}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{form.formState.errors.newPassword && (
|
{form.formState.errors.newPassword && (
|
||||||
@@ -336,17 +322,15 @@ export const PasswordForm = ({ user }: { user: User }) => {
|
|||||||
{form.formState.errors.newPassword?.message}
|
{form.formState.errors.newPassword?.message}
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
<LockOpen1Icon /> Passwort wiederholen
|
<LockOpen1Icon /> Passwort wiederholen
|
||||||
</span>
|
</span>
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register('newPasswordConfirm')}
|
{...form.register("newPasswordConfirm")}
|
||||||
type="text"
|
type="text"
|
||||||
className="input input-bordered w-full"
|
className="input input-bordered w-full"
|
||||||
defaultValue={''}
|
defaultValue={""}
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
{form.formState.errors.newPasswordConfirm && (
|
{form.formState.errors.newPasswordConfirm && (
|
||||||
|
|||||||
@@ -13,10 +13,10 @@ const AuthLayout: NextPage<
|
|||||||
'url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp)',
|
'url(https://img.daisyui.com/images/stock/photo-1507358522600-9f71e620c44e.webp)',
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
<div className="hero-overlay bg-opacity-60"></div>
|
<div className="hero-overlay bg-neutral/60"></div>
|
||||||
<div className="hero-content text-neutral-content text-center ">
|
<div className="hero-content text-center ">
|
||||||
<div className="max-w-lg">
|
<div className="max-w-lg">
|
||||||
<div className="card bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
<div className="card rounded-2xl bg-base-100 w-full min-w-[500px] shadow-2xl max-md:min-w-[400px]">
|
||||||
{children}
|
{children}
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -1,12 +1,12 @@
|
|||||||
'use client';
|
"use client";
|
||||||
import { zodResolver } from '@hookform/resolvers/zod';
|
import { zodResolver } from "@hookform/resolvers/zod";
|
||||||
import { signIn } from 'next-auth/react';
|
import { signIn } from "next-auth/react";
|
||||||
import Link from 'next/link';
|
import Link from "next/link";
|
||||||
import { useSearchParams } from 'next/navigation';
|
import { redirect, useSearchParams } from "next/navigation";
|
||||||
import { useState } from 'react';
|
import { useState } from "react";
|
||||||
import { useForm } from 'react-hook-form';
|
import { useForm } from "react-hook-form";
|
||||||
import { Toaster, toast } from 'react-hot-toast';
|
import { Toaster, toast } from "react-hot-toast";
|
||||||
import { z } from 'zod';
|
import { z } from "zod";
|
||||||
|
|
||||||
export const Login = () => {
|
export const Login = () => {
|
||||||
const [isLoading, setIsLoading] = useState(false);
|
const [isLoading, setIsLoading] = useState(false);
|
||||||
@@ -27,23 +27,22 @@ export const Login = () => {
|
|||||||
className="card-body"
|
className="card-body"
|
||||||
onSubmit={form.handleSubmit(async () => {
|
onSubmit={form.handleSubmit(async () => {
|
||||||
setIsLoading(true);
|
setIsLoading(true);
|
||||||
const data = await signIn('credentials', {
|
const data = await signIn("credentials", {
|
||||||
redirect: false,
|
redirect: false,
|
||||||
callbackUrl: searchParams.get('redirect') || '/',
|
email: form.getValues("email"),
|
||||||
email: form.getValues('email'),
|
password: form.getValues("password"),
|
||||||
password: form.getValues('password'),
|
|
||||||
});
|
});
|
||||||
|
setIsLoading(false);
|
||||||
if (!data || data.error) {
|
if (!data || data.error) {
|
||||||
toast.error('E-Mail / Passwort ist falsch!', {
|
toast.error("E-Mail / Passwort ist falsch!", {
|
||||||
style: {
|
style: {
|
||||||
background:
|
background: "var(--color-base-100)",
|
||||||
'var(--fallback-b1, oklch(var(--b1) / var(--tw-bg-opacity, 1)))',
|
color: "var(--color-base-content)",
|
||||||
color:
|
|
||||||
'var(--fallback-nc, oklch(var(--nc) / var(--tw-text-opacity, 1)))',
|
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
|
return;
|
||||||
}
|
}
|
||||||
setIsLoading(false);
|
redirect(searchParams.get("redirect") || "/");
|
||||||
})}
|
})}
|
||||||
>
|
>
|
||||||
<div>
|
<div>
|
||||||
@@ -51,12 +50,12 @@ export const Login = () => {
|
|||||||
</div>
|
</div>
|
||||||
<h1 className="text-3xl font-bold">Login</h1>
|
<h1 className="text-3xl font-bold">Login</h1>
|
||||||
<span className="text-sm font-medium">
|
<span className="text-sm font-medium">
|
||||||
Noch keinen Account? Zur{' '}
|
Noch keinen Account? Zur{" "}
|
||||||
<Link href="/register" className="link link-accent link-hover">
|
<Link href="/register" className="link link-accent link-hover">
|
||||||
Registrierung
|
Registrierung
|
||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<label className="input input-bordered flex items-center gap-2">
|
<label className="input input-bordered flex items-center gap-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -69,16 +68,16 @@ export const Login = () => {
|
|||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
className="grow"
|
className="grow"
|
||||||
{...form.register('email')}
|
{...form.register("email")}
|
||||||
placeholder="Email"
|
placeholder="Email"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<p className="text-error">
|
<p className="text-error">
|
||||||
{typeof form.formState.errors.email?.message === 'string'
|
{typeof form.formState.errors.email?.message === "string"
|
||||||
? form.formState.errors.email.message
|
? form.formState.errors.email.message
|
||||||
: ''}
|
: ""}
|
||||||
</p>
|
</p>
|
||||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -94,21 +93,17 @@ export const Login = () => {
|
|||||||
<input
|
<input
|
||||||
autoComplete="current-password"
|
autoComplete="current-password"
|
||||||
type="password"
|
type="password"
|
||||||
{...form.register('password')}
|
{...form.register("password")}
|
||||||
placeholder="Passwort"
|
placeholder="Passwort"
|
||||||
className="grow"
|
className="grow"
|
||||||
/>
|
/>
|
||||||
</label>
|
</label>
|
||||||
<div className="form-control mt-6">
|
<div className="card-actions mt-6">
|
||||||
<button
|
<button className="btn btn-primary btn-block" disabled={isLoading}>
|
||||||
className="btn btn-primary"
|
|
||||||
name="loginBtn"
|
|
||||||
disabled={isLoading}
|
|
||||||
>
|
|
||||||
{isLoading && (
|
{isLoading && (
|
||||||
<span className="loading loading-spinner loading-sm"></span>
|
<span className="loading loading-spinner loading-sm"></span>
|
||||||
)}
|
)}
|
||||||
Login{isLoading && '...'}
|
Login{isLoading && "..."}
|
||||||
</button>
|
</button>
|
||||||
</div>
|
</div>
|
||||||
</form>
|
</form>
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ export const Register = () => {
|
|||||||
</Link>
|
</Link>
|
||||||
</span>
|
</span>
|
||||||
<div className="mt-5 mb-2">
|
<div className="mt-5 mb-2">
|
||||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -92,7 +92,7 @@ export const Register = () => {
|
|||||||
? form.formState.errors.firstname.message
|
? form.formState.errors.firstname.message
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -113,8 +113,8 @@ export const Register = () => {
|
|||||||
? form.formState.errors.lastname.message
|
? form.formState.errors.lastname.message
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
<div className="divider divider-neutral">Account</div>
|
<div className="divider">Account</div>
|
||||||
<label className="input input-bordered flex items-center gap-2">
|
<label className="input input-bordered flex items-center gap-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -137,7 +137,7 @@ export const Register = () => {
|
|||||||
? form.formState.errors.email.message
|
? form.formState.errors.email.message
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -163,7 +163,7 @@ export const Register = () => {
|
|||||||
? form.formState.errors.password.message
|
? form.formState.errors.password.message
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
<label className="input input-bordered flex items-center gap-2 mt-2">
|
<label className="input input-bordered flex items-center gap-2 mt-2 w-full">
|
||||||
<svg
|
<svg
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
xmlns="http://www.w3.org/2000/svg"
|
||||||
viewBox="0 0 16 16"
|
viewBox="0 0 16 16"
|
||||||
@@ -189,9 +189,9 @@ export const Register = () => {
|
|||||||
? form.formState.errors.passwordConfirm.message
|
? form.formState.errors.passwordConfirm.message
|
||||||
: ''}
|
: ''}
|
||||||
</p>
|
</p>
|
||||||
<div className="form-control mt-6">
|
<div className="card-actions mt-6">
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary"
|
className="btn btn-primary btn-block"
|
||||||
name="registerBtn"
|
name="registerBtn"
|
||||||
disabled={isLoading}
|
disabled={isLoading}
|
||||||
>
|
>
|
||||||
|
|||||||
85
apps/hub/app/_components/Nav.tsx
Normal file
85
apps/hub/app/_components/Nav.tsx
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
import {
|
||||||
|
HomeIcon,
|
||||||
|
PersonIcon,
|
||||||
|
GearIcon,
|
||||||
|
ExitIcon,
|
||||||
|
LockClosedIcon,
|
||||||
|
RocketIcon,
|
||||||
|
} from "@radix-ui/react-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
|
export const VerticalNav = () => {
|
||||||
|
return (
|
||||||
|
<ul className="menu w-64 bg-base-300 p-4 rounded-lg shadow-md">
|
||||||
|
<li>
|
||||||
|
<Link href="/">
|
||||||
|
<HomeIcon /> Dashboard
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/profile">
|
||||||
|
<PersonIcon /> Profile
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/events">
|
||||||
|
<RocketIcon />
|
||||||
|
Events & Kurse
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<details open>
|
||||||
|
<summary>
|
||||||
|
<LockClosedIcon />
|
||||||
|
Admin
|
||||||
|
</summary>
|
||||||
|
<ul>
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/user">Benutzer</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/station">Stationen</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/admin/event">Events</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</details>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/settings">
|
||||||
|
<GearIcon />
|
||||||
|
Einstellungen
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
export const HorizontalNav = () => (
|
||||||
|
<div className="navbar bg-base-200 shadow-md rounded-lg mb-4">
|
||||||
|
<div className="flex items-center">
|
||||||
|
<a className="btn btn-ghost normal-case text-xl">
|
||||||
|
Virtual Air Rescue - HUB
|
||||||
|
</a>
|
||||||
|
</div>
|
||||||
|
<div className="flex items-center ml-auto">
|
||||||
|
<ul className="flex space-x-2 px-1">
|
||||||
|
<li>
|
||||||
|
<Link href="/">
|
||||||
|
<button className="btn btn-sm btn-outline btn-primary">
|
||||||
|
Zur Leitstelle
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/logout">
|
||||||
|
<button className="btn btn-sm btn-ghost">
|
||||||
|
<ExitIcon /> Logout
|
||||||
|
</button>
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
</ul>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
@@ -1,9 +1,19 @@
|
|||||||
'use client';
|
'use client';
|
||||||
import { useEffect, useState, useCallback } from 'react';
|
import {
|
||||||
|
useEffect,
|
||||||
|
useState,
|
||||||
|
useCallback,
|
||||||
|
Ref,
|
||||||
|
useImperativeHandle,
|
||||||
|
} from 'react';
|
||||||
import SortableTable, { Pagination, SortableTableProps } from './Table';
|
import SortableTable, { Pagination, SortableTableProps } from './Table';
|
||||||
import { PrismaClient } from '@repo/db';
|
import { PrismaClient } from '@repo/db';
|
||||||
import { getData } from './pagiantedTableActions';
|
import { getData } from './pagiantedTableActions';
|
||||||
|
|
||||||
|
export interface PaginatedTableRef {
|
||||||
|
refresh: () => void;
|
||||||
|
}
|
||||||
|
|
||||||
interface PaginatedTableProps<TData>
|
interface PaginatedTableProps<TData>
|
||||||
extends Omit<SortableTableProps<TData>, 'data'> {
|
extends Omit<SortableTableProps<TData>, 'data'> {
|
||||||
prismaModel: keyof PrismaClient;
|
prismaModel: keyof PrismaClient;
|
||||||
@@ -11,7 +21,10 @@ interface PaginatedTableProps<TData>
|
|||||||
rowsPerPage?: number;
|
rowsPerPage?: number;
|
||||||
showEditButton?: boolean;
|
showEditButton?: boolean;
|
||||||
searchFields?: string[];
|
searchFields?: string[];
|
||||||
include?: Record<string, boolean>[];
|
include?: Record<string, boolean>;
|
||||||
|
leftOfSearch?: React.ReactNode;
|
||||||
|
rightOfSearch?: React.ReactNode;
|
||||||
|
ref?: Ref<PaginatedTableRef>;
|
||||||
}
|
}
|
||||||
|
|
||||||
export function PaginatedTable<TData>({
|
export function PaginatedTable<TData>({
|
||||||
@@ -21,6 +34,9 @@ export function PaginatedTable<TData>({
|
|||||||
searchFields = [],
|
searchFields = [],
|
||||||
filter,
|
filter,
|
||||||
include,
|
include,
|
||||||
|
ref,
|
||||||
|
leftOfSearch,
|
||||||
|
rightOfSearch,
|
||||||
...restProps
|
...restProps
|
||||||
}: PaginatedTableProps<TData>) {
|
}: PaginatedTableProps<TData>) {
|
||||||
const [data, setData] = useState<TData[]>([]);
|
const [data, setData] = useState<TData[]>([]);
|
||||||
@@ -29,6 +45,30 @@ export function PaginatedTable<TData>({
|
|||||||
const [searchTerm, setSearchTerm] = useState('');
|
const [searchTerm, setSearchTerm] = useState('');
|
||||||
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
const [debouncedSearchTerm, setDebouncedSearchTerm] = useState(searchTerm);
|
||||||
|
|
||||||
|
const RefreshTableData = async () => {
|
||||||
|
getData(
|
||||||
|
prismaModel,
|
||||||
|
rowsPerPage,
|
||||||
|
page * rowsPerPage,
|
||||||
|
debouncedSearchTerm,
|
||||||
|
searchFields,
|
||||||
|
filter,
|
||||||
|
include
|
||||||
|
).then((result) => {
|
||||||
|
if (result) {
|
||||||
|
setData(result.data);
|
||||||
|
setTotal(result.total);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
};
|
||||||
|
|
||||||
|
useImperativeHandle(ref, () => ({
|
||||||
|
refresh: () => {
|
||||||
|
console.log('refresh');
|
||||||
|
RefreshTableData();
|
||||||
|
},
|
||||||
|
}));
|
||||||
|
|
||||||
const debounce = (func: Function, delay: number) => {
|
const debounce = (func: Function, delay: number) => {
|
||||||
let timer: NodeJS.Timeout;
|
let timer: NodeJS.Timeout;
|
||||||
return (...args: any[]) => {
|
return (...args: any[]) => {
|
||||||
@@ -45,25 +85,14 @@ export function PaginatedTable<TData>({
|
|||||||
);
|
);
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getData(
|
RefreshTableData();
|
||||||
prismaModel,
|
|
||||||
rowsPerPage,
|
|
||||||
page * rowsPerPage,
|
|
||||||
debouncedSearchTerm,
|
|
||||||
searchFields,
|
|
||||||
filter
|
|
||||||
).then((result) => {
|
|
||||||
if (result) {
|
|
||||||
setData(result.data);
|
|
||||||
setTotal(result.total);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
}, [page, debouncedSearchTerm]);
|
}, [page, debouncedSearchTerm]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className="space-y-4 m-4">
|
<div className="space-y-4 m-4">
|
||||||
{searchFields.length > 0 && (
|
{searchFields.length > 0 && (
|
||||||
<div className="flex justify-end">
|
<div className="flex items-center gap-2">
|
||||||
|
<div className="flex-1">{leftOfSearch}</div>
|
||||||
<input
|
<input
|
||||||
type="text"
|
type="text"
|
||||||
placeholder="Suchen..."
|
placeholder="Suchen..."
|
||||||
@@ -74,6 +103,7 @@ export function PaginatedTable<TData>({
|
|||||||
}}
|
}}
|
||||||
className="input input-bordered w-full max-w-xs justify-end"
|
className="input input-bordered w-full max-w-xs justify-end"
|
||||||
/>
|
/>
|
||||||
|
<div className="flex justify-center">{rightOfSearch}</div>
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
<SortableTable
|
<SortableTable
|
||||||
|
|||||||
@@ -116,7 +116,7 @@ export const Pagination = ({
|
|||||||
<ArrowLeft size={16} />
|
<ArrowLeft size={16} />
|
||||||
</button>
|
</button>
|
||||||
<select
|
<select
|
||||||
className="select join-item"
|
className="select join-item w-16"
|
||||||
value={page}
|
value={page}
|
||||||
onChange={(e) => setPage(Number(e.target.value))}
|
onChange={(e) => setPage(Number(e.target.value))}
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ export async function getData(
|
|||||||
searchTerm: string,
|
searchTerm: string,
|
||||||
searchFields: string[],
|
searchFields: string[],
|
||||||
filter?: Record<string, any>,
|
filter?: Record<string, any>,
|
||||||
include?: Record<string, boolean>[]
|
include?: Record<string, boolean>
|
||||||
) {
|
) {
|
||||||
if (!model || !prisma[model]) {
|
if (!model || !prisma[model]) {
|
||||||
return { data: [], total: 0 };
|
return { data: [], total: 0 };
|
||||||
|
|||||||
@@ -1,14 +1,14 @@
|
|||||||
import React from 'react';
|
import React from "react";
|
||||||
import {
|
import {
|
||||||
FieldValues,
|
FieldValues,
|
||||||
Path,
|
Path,
|
||||||
RegisterOptions,
|
RegisterOptions,
|
||||||
UseFormReturn,
|
UseFormReturn,
|
||||||
} from 'react-hook-form';
|
} from "react-hook-form";
|
||||||
import { cn } from '../../../helper/cn';
|
import { cn } from "../../../helper/cn";
|
||||||
|
|
||||||
interface InputProps<T extends FieldValues>
|
interface InputProps<T extends FieldValues>
|
||||||
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, 'form'> {
|
extends Omit<React.InputHTMLAttributes<HTMLInputElement>, "form"> {
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
form: UseFormReturn<T>;
|
form: UseFormReturn<T>;
|
||||||
formOptions?: RegisterOptions<T>;
|
formOptions?: RegisterOptions<T>;
|
||||||
@@ -26,17 +26,13 @@ export const Input = <T extends FieldValues>({
|
|||||||
...inputProps
|
...inputProps
|
||||||
}: InputProps<T>) => {
|
}: InputProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<label className="form-control w-full">
|
<label className="floating-label w-full mt-5">
|
||||||
<div className="label">
|
<span className="text-lg flex items-center gap-2">{label}</span>
|
||||||
<span className="label-text text-lg flex items-center gap-2">
|
|
||||||
{label}
|
|
||||||
</span>
|
|
||||||
</div>
|
|
||||||
<input
|
<input
|
||||||
{...form.register(name, formOptions)}
|
{...form.register(name, formOptions)}
|
||||||
type="text"
|
type="text"
|
||||||
className={cn(
|
className={cn(
|
||||||
'input input-bordered w-full placeholder:text-neutral-600',
|
"input input-bordered w-full placeholder:text-neutral-600",
|
||||||
className
|
className
|
||||||
)}
|
)}
|
||||||
placeholder={placeholder}
|
placeholder={placeholder}
|
||||||
|
|||||||
@@ -4,8 +4,9 @@ import {
|
|||||||
GearIcon,
|
GearIcon,
|
||||||
ExitIcon,
|
ExitIcon,
|
||||||
LockClosedIcon,
|
LockClosedIcon,
|
||||||
} from '@radix-ui/react-icons';
|
RocketIcon,
|
||||||
import Link from 'next/link';
|
} from "@radix-ui/react-icons";
|
||||||
|
import Link from "next/link";
|
||||||
|
|
||||||
export const VerticalNav = () => {
|
export const VerticalNav = () => {
|
||||||
return (
|
return (
|
||||||
@@ -18,7 +19,13 @@ export const VerticalNav = () => {
|
|||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
<Link href="/profile">
|
<Link href="/profile">
|
||||||
<PersonIcon /> Profile
|
<PersonIcon /> Profil
|
||||||
|
</Link>
|
||||||
|
</li>
|
||||||
|
<li>
|
||||||
|
<Link href="/events">
|
||||||
|
<RocketIcon />
|
||||||
|
Events & Kurse
|
||||||
</Link>
|
</Link>
|
||||||
</li>
|
</li>
|
||||||
<li>
|
<li>
|
||||||
@@ -62,7 +69,7 @@ export const HorizontalNav = () => (
|
|||||||
<ul className="flex space-x-2 px-1">
|
<ul className="flex space-x-2 px-1">
|
||||||
<li>
|
<li>
|
||||||
<Link href="/">
|
<Link href="/">
|
||||||
<button className="btn btn-sm btn-outline btn-primary">
|
<button className="btn btn-sm btn-outline btn-info">
|
||||||
Zur Leitstelle
|
Zur Leitstelle
|
||||||
</button>
|
</button>
|
||||||
</Link>
|
</Link>
|
||||||
|
|||||||
62
apps/hub/app/_components/ui/Select.tsx
Normal file
62
apps/hub/app/_components/ui/Select.tsx
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
'use client';
|
||||||
|
import {
|
||||||
|
FieldValues,
|
||||||
|
Path,
|
||||||
|
RegisterOptions,
|
||||||
|
UseFormReturn,
|
||||||
|
} from 'react-hook-form';
|
||||||
|
import SelectTemplate, { Props as SelectTemplateProps } from 'react-select';
|
||||||
|
import { cn } from '../../../helper/cn';
|
||||||
|
import dynamic from 'next/dynamic';
|
||||||
|
|
||||||
|
interface SelectProps<T extends FieldValues> {
|
||||||
|
name: Path<T>;
|
||||||
|
form: UseFormReturn<T>;
|
||||||
|
formOptions?: RegisterOptions<T>;
|
||||||
|
label?: string;
|
||||||
|
placeholder?: string;
|
||||||
|
className?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
const SelectCom = <T extends FieldValues>({
|
||||||
|
name,
|
||||||
|
label = name,
|
||||||
|
placeholder = label,
|
||||||
|
form,
|
||||||
|
formOptions,
|
||||||
|
className,
|
||||||
|
...inputProps
|
||||||
|
}: SelectProps<T>) => {
|
||||||
|
return (
|
||||||
|
<div>
|
||||||
|
<span className="label-text text-lg flex items-center gap-2">
|
||||||
|
{label}
|
||||||
|
</span>
|
||||||
|
<SelectTemplate
|
||||||
|
{...form.register(name, formOptions)}
|
||||||
|
onChange={(newValue: any) => {
|
||||||
|
if ('value' in newValue) {
|
||||||
|
form.setValue(name, newValue.value);
|
||||||
|
} else {
|
||||||
|
form.setValue(name, newValue);
|
||||||
|
}
|
||||||
|
form.trigger(name);
|
||||||
|
}}
|
||||||
|
className={cn('w-full placeholder:text-neutral-600', className)}
|
||||||
|
placeholder={placeholder}
|
||||||
|
{...inputProps}
|
||||||
|
/>
|
||||||
|
{form.formState.errors[name] && (
|
||||||
|
<p className="text-error">
|
||||||
|
{form.formState.errors[name].message as string}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
};
|
||||||
|
|
||||||
|
const SelectWrapper = (props: any) => <SelectCom {...props} />;
|
||||||
|
|
||||||
|
export const Select = dynamic(() => Promise.resolve(SelectWrapper), {
|
||||||
|
ssr: false,
|
||||||
|
});
|
||||||
@@ -1,9 +1,31 @@
|
|||||||
@tailwind base;
|
@import 'tailwindcss';
|
||||||
@tailwind components;
|
@plugin "daisyui";
|
||||||
@tailwind utilities;
|
|
||||||
|
/*
|
||||||
|
The default border color has changed to `currentColor` in Tailwind CSS v4,
|
||||||
|
so we've added these compatibility styles to make sure everything still
|
||||||
|
looks the same as it did with Tailwind CSS v3.
|
||||||
|
|
||||||
|
If we ever want to remove these styles, we need to add an explicit border
|
||||||
|
color utility to any element that depends on these defaults.
|
||||||
|
*/
|
||||||
|
@layer base {
|
||||||
|
*,
|
||||||
|
::after,
|
||||||
|
::before,
|
||||||
|
::backdrop,
|
||||||
|
::file-selector-button {
|
||||||
|
border-color: var(--color-gray-200, currentColor);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
:root {
|
:root {
|
||||||
--background: #ffffff;
|
--background: #ffffff;
|
||||||
--foreground: #171717;
|
--foreground: #171717;
|
||||||
/* --p: 47.67% 0.2484 267.02; */
|
/* --p: 47.67% 0.2484 267.02; */
|
||||||
|
--nc: #a6adbb;
|
||||||
|
}
|
||||||
|
|
||||||
|
body.modal-open {
|
||||||
|
overflow: hidden;
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,8 +11,8 @@
|
|||||||
"dependencies": {
|
"dependencies": {
|
||||||
"@hookform/resolvers": "^3.10.0",
|
"@hookform/resolvers": "^3.10.0",
|
||||||
"@next-auth/prisma-adapter": "^1.0.7",
|
"@next-auth/prisma-adapter": "^1.0.7",
|
||||||
"@repo/ui": "*",
|
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
|
"@repo/ui": "*",
|
||||||
"@tanstack/react-table": "^8.20.6",
|
"@tanstack/react-table": "^8.20.6",
|
||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
@@ -23,24 +23,27 @@
|
|||||||
"next": "15.1.4",
|
"next": "15.1.4",
|
||||||
"next-auth": "^4.24.11",
|
"next-auth": "^4.24.11",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-day-picker": "^9.5.1",
|
||||||
"react-dom": "^19.0.0",
|
"react-dom": "^19.0.0",
|
||||||
"react-hook-form": "^7.54.2",
|
"react-hook-form": "^7.54.2",
|
||||||
"react-hot-toast": "^2.5.1",
|
"react-hot-toast": "^2.5.1",
|
||||||
|
"react-select": "^5.10.0",
|
||||||
"tailwind-merge": "^2.6.0",
|
"tailwind-merge": "^2.6.0",
|
||||||
"zod": "^3.24.1"
|
"zod": "^3.24.1"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@eslint/eslintrc": "^3",
|
"@eslint/eslintrc": "^3",
|
||||||
|
"@tailwindcss/postcss": "^4.0.8",
|
||||||
"@types/bcryptjs": "^2.4.6",
|
"@types/bcryptjs": "^2.4.6",
|
||||||
"@types/jsonwebtoken": "^9.0.8",
|
"@types/jsonwebtoken": "^9.0.8",
|
||||||
"@types/node": "^20",
|
"@types/node": "^20",
|
||||||
"@types/react": "^19",
|
"@types/react": "^19",
|
||||||
"@types/react-dom": "^19",
|
"@types/react-dom": "^19",
|
||||||
"daisyui": "^4.12.23",
|
"daisyui": "^5.0.0-beta.8",
|
||||||
"eslint": "^9",
|
"eslint": "^9",
|
||||||
"eslint-config-next": "15.1.4",
|
"eslint-config-next": "15.1.4",
|
||||||
"postcss": "^8",
|
"postcss": "^8",
|
||||||
"tailwindcss": "^3.4.1",
|
"tailwindcss": "^4.0.8",
|
||||||
"typescript": "^5"
|
"typescript": "^5"
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,7 @@
|
|||||||
/** @type {import('postcss-load-config').Config} */
|
/** @type {import('postcss-load-config').Config} */
|
||||||
const config = {
|
const config = {
|
||||||
plugins: {
|
plugins: {
|
||||||
tailwindcss: {},
|
'@tailwindcss/postcss': {},
|
||||||
},
|
},
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
@@ -1,10 +0,0 @@
|
|||||||
import type { Config } from 'tailwindcss';
|
|
||||||
|
|
||||||
export default {
|
|
||||||
content: [
|
|
||||||
'./pages/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'./components/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
'./app/**/*.{js,ts,jsx,tsx,mdx}',
|
|
||||||
],
|
|
||||||
plugins: [require('daisyui')],
|
|
||||||
} satisfies Config;
|
|
||||||
1545
package-lock.json
generated
1545
package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,17 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- Made the column `eventId` on table `Participant` required. This step will fail if there are existing NULL values in that column.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Participant" DROP CONSTRAINT "Participant_eventId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" ALTER COLUMN "maxParticipants" SET DEFAULT 0;
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Participant" ALTER COLUMN "eventId" SET NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Participant" ADD CONSTRAINT "Participant_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,4 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" ALTER COLUMN "discordRoleId" SET DEFAULT '',
|
||||||
|
ALTER COLUMN "starterMoodleCourseId" SET DATA TYPE TEXT,
|
||||||
|
ALTER COLUMN "finisherMoodleCourseId" SET DATA TYPE TEXT;
|
||||||
@@ -0,0 +1,3 @@
|
|||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Event" ALTER COLUMN "starterMoodleCourseId" SET DEFAULT '',
|
||||||
|
ALTER COLUMN "finisherMoodleCourseId" SET DEFAULT '';
|
||||||
@@ -0,0 +1,9 @@
|
|||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "BADGES" AS ENUM ('P1', 'P2', 'P3', 'D1', 'D2', 'D3', 'DAY1');
|
||||||
|
|
||||||
|
-- CreateEnum
|
||||||
|
CREATE TYPE "PERMISSION" AS ENUM ('ADMIN_EVENT', 'ADMIN_USER', 'SUSPENDED');
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "users" ADD COLUMN "badges" "BADGES"[] DEFAULT ARRAY[]::"BADGES"[],
|
||||||
|
ADD COLUMN "permissions" "PERMISSION"[] DEFAULT ARRAY[]::"PERMISSION"[];
|
||||||
@@ -0,0 +1,57 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the column `selectedForParticipatioon` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- You are about to drop the column `status` on the `Participant` table. All the data in the column will be lost.
|
||||||
|
- Added the required column `eventAppointmentId` to the `Participant` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- AlterEnum
|
||||||
|
-- This migration adds more than one value to an enum.
|
||||||
|
-- With PostgreSQL versions 11 and earlier, this is not possible
|
||||||
|
-- in a single migration. This can be worked around by creating
|
||||||
|
-- multiple migrations, each migration adding only one value to
|
||||||
|
-- the enum.
|
||||||
|
|
||||||
|
|
||||||
|
ALTER TYPE "PERMISSION" ADD VALUE 'PILOT';
|
||||||
|
ALTER TYPE "PERMISSION" ADD VALUE 'DISPO';
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Participant" DROP COLUMN "selectedForParticipatioon",
|
||||||
|
DROP COLUMN "status",
|
||||||
|
ADD COLUMN "eventAppointmentId" INTEGER NOT NULL,
|
||||||
|
ADD COLUMN "finisherMoodleCurseCompleted" BOOLEAN NOT NULL DEFAULT false,
|
||||||
|
ADD COLUMN "starterMoodleCurseCompleted" BOOLEAN NOT NULL DEFAULT false;
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "EventAppointment" (
|
||||||
|
"id" SERIAL NOT NULL,
|
||||||
|
"eventId" INTEGER NOT NULL,
|
||||||
|
"appointmentDate" TIMESTAMP(3) NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "EventAppointment_pkey" PRIMARY KEY ("id")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_EventAppointmentToUser" (
|
||||||
|
"A" INTEGER NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_EventAppointmentToUser_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_EventAppointmentToUser_B_index" ON "_EventAppointmentToUser"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EventAppointment" ADD CONSTRAINT "EventAppointment_eventId_fkey" FOREIGN KEY ("eventId") REFERENCES "Event"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Participant" ADD CONSTRAINT "Participant_eventAppointmentId_fkey" FOREIGN KEY ("eventAppointmentId") REFERENCES "EventAppointment"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentToUser" ADD CONSTRAINT "_EventAppointmentToUser_A_fkey" FOREIGN KEY ("A") REFERENCES "EventAppointment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentToUser" ADD CONSTRAINT "_EventAppointmentToUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,8 @@
|
|||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "Participant" DROP CONSTRAINT "Participant_eventAppointmentId_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "Participant" ALTER COLUMN "eventAppointmentId" DROP NOT NULL;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "Participant" ADD CONSTRAINT "Participant_eventAppointmentId_fkey" FOREIGN KEY ("eventAppointmentId") REFERENCES "EventAppointment"("id") ON DELETE SET NULL ON UPDATE CASCADE;
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
/*
|
||||||
|
Warnings:
|
||||||
|
|
||||||
|
- You are about to drop the `_EventAppointmentToUser` table. If the table is not empty, all the data it contains will be lost.
|
||||||
|
- Added the required column `presenterId` to the `EventAppointment` table without a default value. This is not possible if the table is not empty.
|
||||||
|
|
||||||
|
*/
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentToUser" DROP CONSTRAINT "_EventAppointmentToUser_A_fkey";
|
||||||
|
|
||||||
|
-- DropForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentToUser" DROP CONSTRAINT "_EventAppointmentToUser_B_fkey";
|
||||||
|
|
||||||
|
-- AlterTable
|
||||||
|
ALTER TABLE "EventAppointment" ADD COLUMN "presenterId" TEXT NOT NULL;
|
||||||
|
|
||||||
|
-- DropTable
|
||||||
|
DROP TABLE "_EventAppointmentToUser";
|
||||||
|
|
||||||
|
-- CreateTable
|
||||||
|
CREATE TABLE "_EventAppointmentUser" (
|
||||||
|
"A" INTEGER NOT NULL,
|
||||||
|
"B" TEXT NOT NULL,
|
||||||
|
|
||||||
|
CONSTRAINT "_EventAppointmentUser_AB_pkey" PRIMARY KEY ("A","B")
|
||||||
|
);
|
||||||
|
|
||||||
|
-- CreateIndex
|
||||||
|
CREATE INDEX "_EventAppointmentUser_B_index" ON "_EventAppointmentUser"("B");
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "EventAppointment" ADD CONSTRAINT "EventAppointment_presenterId_fkey" FOREIGN KEY ("presenterId") REFERENCES "users"("id") ON DELETE RESTRICT ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentUser" ADD CONSTRAINT "_EventAppointmentUser_A_fkey" FOREIGN KEY ("A") REFERENCES "EventAppointment"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
|
|
||||||
|
-- AddForeignKey
|
||||||
|
ALTER TABLE "_EventAppointmentUser" ADD CONSTRAINT "_EventAppointmentUser_B_fkey" FOREIGN KEY ("B") REFERENCES "users"("id") ON DELETE CASCADE ON UPDATE CASCADE;
|
||||||
@@ -10,27 +10,41 @@ enum PARTICIPANT_STATUS {
|
|||||||
WAVED
|
WAVED
|
||||||
}
|
}
|
||||||
|
|
||||||
|
model EventAppointment {
|
||||||
|
id Int @id @default(autoincrement())
|
||||||
|
eventId Int
|
||||||
|
appointmentDate DateTime
|
||||||
|
presenterId String
|
||||||
|
// relations:
|
||||||
|
Users User[] @relation("EventAppointmentUser")
|
||||||
|
Participants Participant[]
|
||||||
|
Event Event @relation(fields: [eventId], references: [id])
|
||||||
|
Presenter User @relation(fields: [presenterId], references: [id])
|
||||||
|
}
|
||||||
|
|
||||||
model Participant {
|
model Participant {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
userId String @map(name: "user_id")
|
userId String @map(name: "user_id")
|
||||||
status PARTICIPANT_STATUS
|
starterMoodleCurseCompleted Boolean @default(false)
|
||||||
selectedForParticipatioon Boolean @default(false)
|
finisherMoodleCurseCompleted Boolean @default(false)
|
||||||
|
eventAppointmentId Int?
|
||||||
statusLog Json[]
|
statusLog Json[]
|
||||||
eventId Int
|
eventId Int
|
||||||
// relations:
|
// relations:
|
||||||
user User @relation(fields: [userId], references: [id])
|
User User @relation(fields: [userId], references: [id])
|
||||||
Event Event? @relation(fields: [eventId], references: [id])
|
Event Event @relation(fields: [eventId], references: [id])
|
||||||
|
EventAppointment EventAppointment? @relation(fields: [eventAppointmentId], references: [id])
|
||||||
}
|
}
|
||||||
|
|
||||||
model Event {
|
model Event {
|
||||||
id Int @id @default(autoincrement())
|
id Int @id @default(autoincrement())
|
||||||
name String
|
name String
|
||||||
description String
|
description String
|
||||||
discordRoleId String?
|
discordRoleId String? @default("")
|
||||||
hasPresenceEvents Boolean @default(false)
|
hasPresenceEvents Boolean @default(false)
|
||||||
maxParticipants Int? @default(0)
|
maxParticipants Int? @default(0)
|
||||||
starterMoodleCourseId Int?
|
starterMoodleCourseId String? @default("")
|
||||||
finisherMoodleCourseId Int?
|
finisherMoodleCourseId String? @default("")
|
||||||
finishedBadges String[] @default([])
|
finishedBadges String[] @default([])
|
||||||
requiredBadges String[] @default([])
|
requiredBadges String[] @default([])
|
||||||
finishedPermissions String[] @default([])
|
finishedPermissions String[] @default([])
|
||||||
@@ -38,6 +52,7 @@ model Event {
|
|||||||
|
|
||||||
// relations:
|
// relations:
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
|
appointments EventAppointment[]
|
||||||
}
|
}
|
||||||
|
|
||||||
model File {
|
model File {
|
||||||
|
|||||||
@@ -1,3 +1,21 @@
|
|||||||
|
enum BADGES {
|
||||||
|
P1
|
||||||
|
P2
|
||||||
|
P3
|
||||||
|
D1
|
||||||
|
D2
|
||||||
|
D3
|
||||||
|
DAY1
|
||||||
|
}
|
||||||
|
|
||||||
|
enum PERMISSION {
|
||||||
|
ADMIN_EVENT
|
||||||
|
ADMIN_USER
|
||||||
|
SUSPENDED
|
||||||
|
PILOT
|
||||||
|
DISPO
|
||||||
|
}
|
||||||
|
|
||||||
model User {
|
model User {
|
||||||
id String @id @default(uuid())
|
id String @id @default(uuid())
|
||||||
publicId String @unique
|
publicId String @unique
|
||||||
@@ -8,6 +26,8 @@ model User {
|
|||||||
vatsimCid Int? @map(name: "vatsim_cid")
|
vatsimCid Int? @map(name: "vatsim_cid")
|
||||||
emailVerified DateTime? @map(name: "email_verified")
|
emailVerified DateTime? @map(name: "email_verified")
|
||||||
image String?
|
image String?
|
||||||
|
badges BADGES[] @default([])
|
||||||
|
permissions PERMISSION[] @default([])
|
||||||
createdAt DateTime @default(now()) @map(name: "created_at")
|
createdAt DateTime @default(now()) @map(name: "created_at")
|
||||||
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
updatedAt DateTime @default(now()) @map(name: "updated_at")
|
||||||
|
|
||||||
@@ -15,6 +35,8 @@ model User {
|
|||||||
oauthTokens OAuthToken[]
|
oauthTokens OAuthToken[]
|
||||||
discordAccounts DiscordAccount[]
|
discordAccounts DiscordAccount[]
|
||||||
participants Participant[]
|
participants Participant[]
|
||||||
|
EventAppointmentUser EventAppointment[] @relation("EventAppointmentUser")
|
||||||
|
EventAppointment EventAppointment[]
|
||||||
|
|
||||||
@@map(name: "users")
|
@@map(name: "users")
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user