added participant overview, (admin), Date input
This commit is contained in:
@@ -9,7 +9,6 @@
|
|||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@repo/db": "*",
|
"@repo/db": "*",
|
||||||
"@repo/hub": "*",
|
|
||||||
"@repo/typescript-config": "*",
|
"@repo/typescript-config": "*",
|
||||||
"@types/node": "^22.13.5",
|
"@types/node": "^22.13.5",
|
||||||
"concurrently": "^9.1.2",
|
"concurrently": "^9.1.2",
|
||||||
|
|||||||
@@ -7,15 +7,30 @@ import {
|
|||||||
EventOptionalDefaultsSchema,
|
EventOptionalDefaultsSchema,
|
||||||
ParticipantOptionalDefaultsSchema,
|
ParticipantOptionalDefaultsSchema,
|
||||||
} from "@repo/db/zod";
|
} from "@repo/db/zod";
|
||||||
import { set, useForm } from "react-hook-form";
|
import { Controller, set, useForm } from "react-hook-form";
|
||||||
import { BADGES, Event, EVENT_TYPE, PERMISSION } from "@repo/db";
|
import {
|
||||||
|
BADGES,
|
||||||
|
Event,
|
||||||
|
EVENT_TYPE,
|
||||||
|
Participant,
|
||||||
|
PERMISSION,
|
||||||
|
prisma,
|
||||||
|
Prisma,
|
||||||
|
} from "@repo/db";
|
||||||
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
import { Bot, Calendar, FileText, UserIcon } from "lucide-react";
|
||||||
import { Input } from "../../../../_components/ui/Input";
|
import { Input } from "../../../../_components/ui/Input";
|
||||||
import { useRef, useState } from "react";
|
import { useRef, useState } from "react";
|
||||||
import { deleteEvent, upsertAppointment, upsertEvent } from "../action";
|
import {
|
||||||
|
deleteAppoinement,
|
||||||
|
deleteEvent,
|
||||||
|
upsertAppointment,
|
||||||
|
upsertEvent,
|
||||||
|
} from "../action";
|
||||||
import { Button } from "../../../../_components/ui/Button";
|
import { Button } from "../../../../_components/ui/Button";
|
||||||
import { redirect, useRouter } from "next/navigation";
|
import { redirect, useRouter } from "next/navigation";
|
||||||
import { Switch } from "../../../../_components/ui/Switch";
|
import { Switch } from "../../../../_components/ui/Switch";
|
||||||
|
import DatePicker, { registerLocale } from "react-datepicker";
|
||||||
|
import "react-datepicker/dist/react-datepicker.css";
|
||||||
import {
|
import {
|
||||||
PaginatedTable,
|
PaginatedTable,
|
||||||
PaginatedTableRef,
|
PaginatedTableRef,
|
||||||
@@ -23,6 +38,10 @@ import {
|
|||||||
import { Select } from "../../../../_components/ui/Select";
|
import { Select } from "../../../../_components/ui/Select";
|
||||||
import { useSession } from "next-auth/react";
|
import { useSession } from "next-auth/react";
|
||||||
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
import { MarkdownEditor } from "../../../../_components/ui/MDEditor";
|
||||||
|
import { CellContext } from "@tanstack/react-table";
|
||||||
|
import { upsertParticipant } from "../../../events/actions";
|
||||||
|
import { de } from "date-fns/locale";
|
||||||
|
registerLocale("de", de);
|
||||||
|
|
||||||
export const Form = ({ event }: { event?: Event }) => {
|
export const Form = ({ event }: { event?: Event }) => {
|
||||||
const { data: session } = useSession();
|
const { data: session } = useSession();
|
||||||
@@ -38,12 +57,16 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
},
|
},
|
||||||
});
|
});
|
||||||
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
|
const appointmentsTableRef = useRef<PaginatedTableRef>(null);
|
||||||
|
const participantTableRef = useRef<PaginatedTableRef>(null);
|
||||||
const [loading, setLoading] = useState(false);
|
const [loading, setLoading] = useState(false);
|
||||||
const [deleteLoading, setDeleteLoading] = useState(false);
|
const [deleteLoading, setDeleteLoading] = useState(false);
|
||||||
const addParticipantModal = useRef<HTMLDialogElement>(null);
|
const appointmentModal = useRef<HTMLDialogElement>(null);
|
||||||
|
const participantForm = useForm<Participant>({
|
||||||
|
resolver: zodResolver(ParticipantOptionalDefaultsSchema),
|
||||||
|
});
|
||||||
return (
|
return (
|
||||||
<>
|
<>
|
||||||
<dialog ref={addParticipantModal} className="modal">
|
<dialog ref={appointmentModal} className="modal">
|
||||||
<div className="modal-box">
|
<div className="modal-box">
|
||||||
<form method="dialog">
|
<form method="dialog">
|
||||||
{/* if there is a button in form, it will close the modal */}
|
{/* if there is a button in form, it will close the modal */}
|
||||||
@@ -51,32 +74,167 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
✕
|
✕
|
||||||
</button>
|
</button>
|
||||||
</form>
|
</form>
|
||||||
<h3 className="font-bold text-lg">Termin hinzufügen</h3>
|
<h3 className="font-bold text-lg">
|
||||||
|
Termin {appointmentForm.watch("id")}
|
||||||
|
</h3>
|
||||||
<form
|
<form
|
||||||
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
onSubmit={appointmentForm.handleSubmit(async (values) => {
|
||||||
if (!event) return;
|
if (!event) return;
|
||||||
const createdAppointment = await upsertAppointment({
|
const createdAppointment = await upsertAppointment(values);
|
||||||
appointmentDate: values.appointmentDate,
|
appointmentModal.current?.close();
|
||||||
eventId: event?.id,
|
|
||||||
presenterId: values.presenterId,
|
|
||||||
});
|
|
||||||
addParticipantModal.current?.close();
|
|
||||||
appointmentsTableRef.current?.refresh();
|
appointmentsTableRef.current?.refresh();
|
||||||
})}
|
})}
|
||||||
className="flex flex-col"
|
className="flex flex-col"
|
||||||
>
|
>
|
||||||
<Input
|
<Controller
|
||||||
|
control={appointmentForm.control}
|
||||||
|
name="appointmentDate"
|
||||||
|
render={({ field }) => (
|
||||||
|
<DatePicker
|
||||||
|
locale={"de"}
|
||||||
|
showTimeCaption
|
||||||
|
showTimeInput
|
||||||
|
showTimeSelect
|
||||||
|
placeholderText="Select date"
|
||||||
|
onChange={(date) => field.onChange(date)}
|
||||||
|
selected={field.value}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
/>
|
||||||
|
{/* <Input
|
||||||
form={appointmentForm}
|
form={appointmentForm}
|
||||||
|
type="datetime-local"
|
||||||
label="Datum"
|
label="Datum"
|
||||||
name="appointmentDate"
|
name="appointmentDate"
|
||||||
type="date"
|
formOptions={{
|
||||||
/>
|
valueAsDate: true,
|
||||||
<div className="modal-action">
|
}}
|
||||||
<Button type="submit" className="btn btn-primary">
|
/> */}
|
||||||
Hinzufügen
|
<div>
|
||||||
</Button>
|
{appointmentForm.watch("id") && (
|
||||||
|
<PaginatedTable
|
||||||
|
ref={participantTableRef}
|
||||||
|
columns={[
|
||||||
|
{
|
||||||
|
accessorKey: "User.firstname",
|
||||||
|
header: "Vorname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
accessorKey: "User.lastname",
|
||||||
|
header: "Nachname",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
header: "Aktion",
|
||||||
|
cell: ({ row }: CellContext<Participant, any>) => {
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<button
|
||||||
|
onClick={() => {
|
||||||
|
participantForm.reset(row.original);
|
||||||
|
}}
|
||||||
|
className="btn btn-outline btn-sm"
|
||||||
|
>
|
||||||
|
anzeigen
|
||||||
|
</button>
|
||||||
|
{!row.original.attended &&
|
||||||
|
event?.hasPresenceEvents && (
|
||||||
|
<button
|
||||||
|
type="button"
|
||||||
|
onSubmit={() => {}}
|
||||||
|
onClick={async () => {
|
||||||
|
await upsertParticipant({
|
||||||
|
eventId: event!.id,
|
||||||
|
userId: participantForm.watch("userId"),
|
||||||
|
attended: true,
|
||||||
|
});
|
||||||
|
participantTableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-outline btn-info btn-sm"
|
||||||
|
>
|
||||||
|
Anwesend
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
},
|
||||||
|
},
|
||||||
|
]}
|
||||||
|
prismaModel={"participant"}
|
||||||
|
filter={{
|
||||||
|
eventAppointmentId: appointmentForm.watch("id"),
|
||||||
|
}}
|
||||||
|
include={{ User: true }}
|
||||||
|
leftOfPagination={
|
||||||
|
<div className="space-x-1">
|
||||||
|
<Button type="submit" className="btn btn-primary">
|
||||||
|
Speichern
|
||||||
|
</Button>
|
||||||
|
{appointmentForm.watch("id") && (
|
||||||
|
<Button
|
||||||
|
type="button"
|
||||||
|
onSubmit={() => {}}
|
||||||
|
onClick={async () => {
|
||||||
|
await deleteAppoinement(
|
||||||
|
appointmentForm.watch("id")!,
|
||||||
|
);
|
||||||
|
appointmentModal.current?.close();
|
||||||
|
appointmentsTableRef.current?.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-error btn-outline"
|
||||||
|
>
|
||||||
|
Löschen
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
}
|
||||||
|
/>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
|
<div className="modal-action"></div>
|
||||||
</form>
|
</form>
|
||||||
|
{participantForm.watch("id") && (
|
||||||
|
<form
|
||||||
|
onSubmit={participantForm.handleSubmit(async (data) => {
|
||||||
|
await upsertParticipant({
|
||||||
|
...data,
|
||||||
|
statusLog: data.statusLog as Prisma.InputJsonValue[],
|
||||||
|
});
|
||||||
|
participantTableRef.current?.refresh();
|
||||||
|
})}
|
||||||
|
className="space-y-1"
|
||||||
|
>
|
||||||
|
<h1 className="text-2xl">Teilnehmer bearbeiten</h1>
|
||||||
|
<Switch
|
||||||
|
form={participantForm}
|
||||||
|
name="appointmentCancelled"
|
||||||
|
label="Termin abgesagt"
|
||||||
|
/>
|
||||||
|
<Switch
|
||||||
|
form={participantForm}
|
||||||
|
name="finisherMoodleCurseCompleted"
|
||||||
|
label="Abschluss-Moodle kurs abgeschlossen"
|
||||||
|
/>
|
||||||
|
{event?.hasPresenceEvents && (
|
||||||
|
<Switch
|
||||||
|
form={participantForm}
|
||||||
|
name="attended"
|
||||||
|
label="An Presenstermin teilgenommen"
|
||||||
|
/>
|
||||||
|
)}
|
||||||
|
<div className="flex flex-col">
|
||||||
|
<h3 className="text-xl">Verlauf</h3>
|
||||||
|
{participantForm.watch("statusLog").map((s) => {
|
||||||
|
return (
|
||||||
|
<div className="flex justify-between">
|
||||||
|
<p>{(s as any).event}</p>
|
||||||
|
<p>{new Date((s as any).timestamp).toLocaleString()}</p>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
})}
|
||||||
|
</div>
|
||||||
|
<Button>Speichern</Button>
|
||||||
|
</form>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
</dialog>
|
</dialog>
|
||||||
<form
|
<form
|
||||||
@@ -173,7 +331,12 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
{event && (
|
{event && (
|
||||||
<button
|
<button
|
||||||
className="btn btn-primary btn-outline"
|
className="btn btn-primary btn-outline"
|
||||||
onClick={() => addParticipantModal.current?.showModal()}
|
onClick={() => {
|
||||||
|
appointmentModal.current?.showModal();
|
||||||
|
appointmentForm.reset({
|
||||||
|
id: undefined,
|
||||||
|
});
|
||||||
|
}}
|
||||||
>
|
>
|
||||||
Hinzufügen
|
Hinzufügen
|
||||||
</button>
|
</button>
|
||||||
@@ -230,8 +393,8 @@ export const Form = ({ event }: { event?: Event }) => {
|
|||||||
onSubmit={() => false}
|
onSubmit={() => false}
|
||||||
type="button"
|
type="button"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
console.log(row.original);
|
appointmentForm.reset(row.original);
|
||||||
// TODO: open modal to edit appointment
|
appointmentModal.current?.showModal();
|
||||||
}}
|
}}
|
||||||
className="btn btn-sm btn-outline"
|
className="btn btn-sm btn-outline"
|
||||||
>
|
>
|
||||||
|
|||||||
@@ -1,65 +1,50 @@
|
|||||||
'use server';
|
"use server";
|
||||||
|
|
||||||
import { prisma, Prisma, Event, Participant, EventAppointment } 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,
|
||||||
id?: Event['id']
|
id?: Event["id"],
|
||||||
) => {
|
) => {
|
||||||
const newEvent = id
|
const newEvent = id
|
||||||
? await prisma.event.update({
|
? await prisma.event.update({
|
||||||
where: { id: id },
|
where: { id: id },
|
||||||
data: event,
|
data: event,
|
||||||
})
|
})
|
||||||
: await prisma.event.create({ data: event });
|
: await prisma.event.create({ data: event });
|
||||||
return newEvent;
|
return newEvent;
|
||||||
};
|
};
|
||||||
|
|
||||||
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 (
|
export const upsertAppointment = async (
|
||||||
eventAppointment: Prisma.XOR<
|
eventAppointment: Prisma.XOR<
|
||||||
Prisma.EventAppointmentCreateInput,
|
Prisma.EventAppointmentCreateInput,
|
||||||
Prisma.EventAppointmentUncheckedCreateInput
|
Prisma.EventAppointmentUncheckedCreateInput
|
||||||
>,
|
>,
|
||||||
id?: EventAppointment['id']
|
|
||||||
) => {
|
) => {
|
||||||
const newEventAppointment = id
|
const newEventAppointment = eventAppointment.id
|
||||||
? await prisma.eventAppointment.update({
|
? await prisma.eventAppointment.update({
|
||||||
where: { id: id },
|
where: { id: eventAppointment.id },
|
||||||
data: eventAppointment,
|
data: eventAppointment,
|
||||||
})
|
})
|
||||||
: await prisma.eventAppointment.create({ data: eventAppointment });
|
: await prisma.eventAppointment.create({ data: eventAppointment });
|
||||||
return newEventAppointment;
|
return newEventAppointment;
|
||||||
};
|
};
|
||||||
|
|
||||||
export const deleteAppoinement = async (id: Event['id']) => {
|
export const deleteAppoinement = async (id: Event["id"]) => {
|
||||||
await prisma.eventAppointment.delete({ where: { id: id } });
|
await prisma.eventAppointment.delete({ where: { id: id } });
|
||||||
prisma.eventAppointment.findMany({
|
prisma.eventAppointment.findMany({
|
||||||
where: {
|
where: {
|
||||||
eventId: id,
|
eventId: id,
|
||||||
},
|
},
|
||||||
orderBy: {
|
orderBy: {
|
||||||
// TODO: add order by in relation to table selected column
|
// TODO: add order by in relation to table selected column
|
||||||
},
|
},
|
||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
export const deleteParticipant = async (id: Participant["id"]) => {
|
||||||
export const upsertParticipant = async (
|
await prisma.participant.delete({ where: { id: id } });
|
||||||
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 } });
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -5,10 +5,9 @@ import {
|
|||||||
CalendarIcon,
|
CalendarIcon,
|
||||||
EnterIcon,
|
EnterIcon,
|
||||||
} from "@radix-ui/react-icons";
|
} from "@radix-ui/react-icons";
|
||||||
import { Event, EventAppointment, Participant, User } from "@repo/db";
|
import { Event, EventAppointment, Participant, prisma, User } from "@repo/db";
|
||||||
import { cn } from "../../../../helper/cn";
|
import { cn } from "../../../../helper/cn";
|
||||||
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
import { inscribeToMoodleCourse, upsertParticipant } from "../actions";
|
||||||
import { useSession } from "next-auth/react";
|
|
||||||
import { Clock10Icon, Cross } from "lucide-react";
|
import { Clock10Icon, Cross } from "lucide-react";
|
||||||
import { useForm } from "react-hook-form";
|
import { useForm } from "react-hook-form";
|
||||||
import {
|
import {
|
||||||
@@ -22,6 +21,7 @@ import { zodResolver } from "@hookform/resolvers/zod";
|
|||||||
import { Select } from "../../../_components/ui/Select";
|
import { Select } from "../../../_components/ui/Select";
|
||||||
import toast from "react-hot-toast";
|
import toast from "react-hot-toast";
|
||||||
import { useRouter } from "next/navigation";
|
import { useRouter } from "next/navigation";
|
||||||
|
import { JsonArray } from "../../../../../../packages/database/generated/client/runtime/library";
|
||||||
|
|
||||||
interface ModalBtnProps {
|
interface ModalBtnProps {
|
||||||
title: string;
|
title: string;
|
||||||
@@ -122,50 +122,54 @@ const ModalBtn = ({
|
|||||||
)}
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{selectedAppointment && !participant?.appointmentCancelled && (
|
{!canSelectDate && participant?.attended && (
|
||||||
<div className="flex items-center gap-2 justify-center">
|
<p className="py-4 flex items-center gap-2 justify-center">
|
||||||
<p>Dein Ausgewähler Termin</p>
|
<CheckCircledIcon className="text-success" />
|
||||||
|
Du hast an dem Presenztermin teilgenommen
|
||||||
<p>
|
|
||||||
{new Date(
|
|
||||||
selectedAppointment.appointmentDate,
|
|
||||||
).toLocaleString()}
|
|
||||||
</p>
|
|
||||||
<button
|
|
||||||
onClick={async () => {
|
|
||||||
await upsertParticipant({
|
|
||||||
eventId: event.id,
|
|
||||||
userId: user.id,
|
|
||||||
appointmentCancelled: true,
|
|
||||||
statusLog: [
|
|
||||||
...(participant?.statusLog.filter(
|
|
||||||
(l) => l !== null,
|
|
||||||
) || []),
|
|
||||||
{
|
|
||||||
data: {
|
|
||||||
appointmentId: selectedAppointment.id,
|
|
||||||
appointmentDate:
|
|
||||||
selectedAppointment.appointmentDate,
|
|
||||||
},
|
|
||||||
event: "APPOINTMENT_CANCELLED",
|
|
||||||
timestamp: new Date(),
|
|
||||||
},
|
|
||||||
],
|
|
||||||
});
|
|
||||||
toast.success("Termin abgesagt");
|
|
||||||
router.refresh();
|
|
||||||
}}
|
|
||||||
className="btn btn-error btn-outline btn-sm"
|
|
||||||
>
|
|
||||||
absagen
|
|
||||||
</button>
|
|
||||||
</div>
|
|
||||||
)}
|
|
||||||
{!!dates.length && (
|
|
||||||
<p className="mt-3 text-center">
|
|
||||||
Bitte finde dich an diesem Termin in unserem Discord ein.
|
|
||||||
</p>
|
</p>
|
||||||
)}
|
)}
|
||||||
|
{selectedAppointment && !participant?.appointmentCancelled && (
|
||||||
|
<>
|
||||||
|
<div className="flex items-center gap-2 justify-center">
|
||||||
|
<p>Dein Ausgewähler Termin</p>
|
||||||
|
|
||||||
|
<p>
|
||||||
|
{new Date(
|
||||||
|
selectedAppointment.appointmentDate,
|
||||||
|
).toLocaleString()}
|
||||||
|
</p>
|
||||||
|
<button
|
||||||
|
onClick={async () => {
|
||||||
|
await upsertParticipant({
|
||||||
|
eventId: event.id,
|
||||||
|
userId: participant!.userId,
|
||||||
|
appointmentCancelled: true,
|
||||||
|
statusLog: [
|
||||||
|
...(participant?.statusLog as any),
|
||||||
|
{
|
||||||
|
data: {
|
||||||
|
appointmentId: selectedAppointment.id,
|
||||||
|
appointmentDate:
|
||||||
|
selectedAppointment.appointmentDate,
|
||||||
|
},
|
||||||
|
event: "Termin abgesagt",
|
||||||
|
timestamp: new Date(),
|
||||||
|
},
|
||||||
|
],
|
||||||
|
});
|
||||||
|
toast.success("Termin abgesagt");
|
||||||
|
router.refresh();
|
||||||
|
}}
|
||||||
|
className="btn btn-error btn-outline btn-sm"
|
||||||
|
>
|
||||||
|
absagen
|
||||||
|
</button>
|
||||||
|
<p className="mt-3 text-center">
|
||||||
|
Bitte finde dich an diesem Termin in unserem Discord ein.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
)}
|
||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
{event.finisherMoodleCourseId && (
|
{event.finisherMoodleCourseId && (
|
||||||
|
|||||||
@@ -24,6 +24,7 @@ interface PaginatedTableProps<TData>
|
|||||||
include?: Record<string, boolean>;
|
include?: Record<string, boolean>;
|
||||||
leftOfSearch?: React.ReactNode;
|
leftOfSearch?: React.ReactNode;
|
||||||
rightOfSearch?: React.ReactNode;
|
rightOfSearch?: React.ReactNode;
|
||||||
|
leftOfPagination?: React.ReactNode;
|
||||||
ref?: Ref<PaginatedTableRef>;
|
ref?: Ref<PaginatedTableRef>;
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -37,6 +38,7 @@ export function PaginatedTable<TData>({
|
|||||||
ref,
|
ref,
|
||||||
leftOfSearch,
|
leftOfSearch,
|
||||||
rightOfSearch,
|
rightOfSearch,
|
||||||
|
leftOfPagination,
|
||||||
...restProps
|
...restProps
|
||||||
}: PaginatedTableProps<TData>) {
|
}: PaginatedTableProps<TData>) {
|
||||||
const [data, setData] = useState<TData[]>([]);
|
const [data, setData] = useState<TData[]>([]);
|
||||||
@@ -62,6 +64,10 @@ export function PaginatedTable<TData>({
|
|||||||
});
|
});
|
||||||
};
|
};
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
RefreshTableData();
|
||||||
|
}, [filter]);
|
||||||
|
|
||||||
useImperativeHandle(ref, () => ({
|
useImperativeHandle(ref, () => ({
|
||||||
refresh: () => {
|
refresh: () => {
|
||||||
RefreshTableData();
|
RefreshTableData();
|
||||||
@@ -111,11 +117,14 @@ export function PaginatedTable<TData>({
|
|||||||
showEditButton={showEditButton}
|
showEditButton={showEditButton}
|
||||||
{...restProps}
|
{...restProps}
|
||||||
/>
|
/>
|
||||||
<Pagination
|
<div className="flex items-between">
|
||||||
totalPages={Math.ceil(total / rowsPerPage)}
|
{leftOfPagination}
|
||||||
page={page}
|
<Pagination
|
||||||
setPage={setPage}
|
totalPages={Math.ceil(total / rowsPerPage)}
|
||||||
/>
|
page={page}
|
||||||
|
setPage={setPage}
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,48 +1,47 @@
|
|||||||
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>;
|
||||||
label?: string;
|
label?: string;
|
||||||
placeholder?: string;
|
placeholder?: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
export const Input = <T extends FieldValues>({
|
export const Input = <T extends FieldValues>({
|
||||||
name,
|
name,
|
||||||
label = name,
|
label = name,
|
||||||
placeholder = label,
|
placeholder = label,
|
||||||
form,
|
form,
|
||||||
formOptions,
|
formOptions,
|
||||||
className,
|
className,
|
||||||
...inputProps
|
...inputProps
|
||||||
}: InputProps<T>) => {
|
}: InputProps<T>) => {
|
||||||
return (
|
return (
|
||||||
<label className="floating-label w-full mt-5">
|
<label className="floating-label w-full mt-5">
|
||||||
<span className="text-lg flex items-center gap-2">{label}</span>
|
<span className="text-lg flex items-center gap-2">{label}</span>
|
||||||
<input
|
<input
|
||||||
{...form.register(name, formOptions)}
|
{...form.register(name, formOptions)}
|
||||||
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}
|
{...inputProps}
|
||||||
{...inputProps}
|
/>
|
||||||
/>
|
{form.formState.errors[name] && (
|
||||||
{form.formState.errors[name] && (
|
<p className="text-error">
|
||||||
<p className="text-error">
|
{form.formState.errors[name].message as string}
|
||||||
{form.formState.errors[name].message as string}
|
</p>
|
||||||
</p>
|
)}
|
||||||
)}
|
</label>
|
||||||
</label>
|
);
|
||||||
);
|
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ interface SelectProps<T extends FieldValues>
|
|||||||
extends Omit<SelectTemplateProps, "form"> {
|
extends Omit<SelectTemplateProps, "form"> {
|
||||||
label?: any;
|
label?: any;
|
||||||
name: Path<T>;
|
name: Path<T>;
|
||||||
form: UseFormReturn<T>;
|
form: UseFormReturn<T> | any;
|
||||||
formOptions?: RegisterOptions<T>;
|
formOptions?: RegisterOptions<T>;
|
||||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -18,6 +18,7 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -27,6 +28,7 @@
|
|||||||
"next-remove-imports": "^1.0.12",
|
"next-remove-imports": "^1.0.12",
|
||||||
"npm": "^11.1.0",
|
"npm": "^11.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-datepicker": "^8.1.0",
|
||||||
"react-day-picker": "^9.5.1",
|
"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",
|
||||||
|
|||||||
11
apps/hub/types/prisma.d.ts
vendored
Normal file
11
apps/hub/types/prisma.d.ts
vendored
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
import { Prisma } from "@prisma/client";
|
||||||
|
|
||||||
|
declare module "@prisma/client" {
|
||||||
|
export type InputJsonValue =
|
||||||
|
| string
|
||||||
|
| number
|
||||||
|
| boolean
|
||||||
|
| null
|
||||||
|
| JsonObject
|
||||||
|
| JsonArray;
|
||||||
|
}
|
||||||
52
package-lock.json
generated
52
package-lock.json
generated
@@ -100,6 +100,7 @@
|
|||||||
"axios": "^1.7.9",
|
"axios": "^1.7.9",
|
||||||
"bcryptjs": "^2.4.3",
|
"bcryptjs": "^2.4.3",
|
||||||
"clsx": "^2.1.1",
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0",
|
||||||
"i": "^0.3.7",
|
"i": "^0.3.7",
|
||||||
"jsonwebtoken": "^9.0.2",
|
"jsonwebtoken": "^9.0.2",
|
||||||
"lodash": "^4.17.21",
|
"lodash": "^4.17.21",
|
||||||
@@ -109,6 +110,7 @@
|
|||||||
"next-remove-imports": "^1.0.12",
|
"next-remove-imports": "^1.0.12",
|
||||||
"npm": "^11.1.0",
|
"npm": "^11.1.0",
|
||||||
"react": "^19.0.0",
|
"react": "^19.0.0",
|
||||||
|
"react-datepicker": "^8.1.0",
|
||||||
"react-day-picker": "^9.5.1",
|
"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",
|
||||||
@@ -880,6 +882,34 @@
|
|||||||
"@floating-ui/utils": "^0.2.9"
|
"@floating-ui/utils": "^0.2.9"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/@floating-ui/react": {
|
||||||
|
"version": "0.27.5",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react/-/react-0.27.5.tgz",
|
||||||
|
"integrity": "sha512-BX3jKxo39Ba05pflcQmqPPwc0qdNsdNi/eweAFtoIdrJWNen2sVEWMEac3i6jU55Qfx+lOcdMNKYn2CtWmlnOQ==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react-dom": "^2.1.2",
|
||||||
|
"@floating-ui/utils": "^0.2.9",
|
||||||
|
"tabbable": "^6.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=17.0.0",
|
||||||
|
"react-dom": ">=17.0.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
|
"node_modules/@floating-ui/react-dom": {
|
||||||
|
"version": "2.1.2",
|
||||||
|
"resolved": "https://registry.npmjs.org/@floating-ui/react-dom/-/react-dom-2.1.2.tgz",
|
||||||
|
"integrity": "sha512-06okr5cgPzMNBy+Ycse2A6udMi4bqwW/zgBF/rwjcNqWkyr82Mcg8b0vjX8OJpZFy/FKjJmw6wV7t44kK6kW7A==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/dom": "^1.0.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": ">=16.8.0",
|
||||||
|
"react-dom": ">=16.8.0"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/@floating-ui/utils": {
|
"node_modules/@floating-ui/utils": {
|
||||||
"version": "0.2.9",
|
"version": "0.2.9",
|
||||||
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
"resolved": "https://registry.npmjs.org/@floating-ui/utils/-/utils-0.2.9.tgz",
|
||||||
@@ -4067,6 +4097,7 @@
|
|||||||
"version": "4.1.0",
|
"version": "4.1.0",
|
||||||
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-4.1.0.tgz",
|
||||||
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
"integrity": "sha512-Ukq0owbQXxa/U3EGtsdVBkR1w7KOQ5gIBqdH2hkvknzZPYvBxb/aa6E8L7tmjFtkwZBu3UXBbjIgPo/Ez4xaNg==",
|
||||||
|
"license": "MIT",
|
||||||
"funding": {
|
"funding": {
|
||||||
"type": "github",
|
"type": "github",
|
||||||
"url": "https://github.com/sponsors/kossnocorp"
|
"url": "https://github.com/sponsors/kossnocorp"
|
||||||
@@ -12515,6 +12546,21 @@
|
|||||||
"node": ">=0.10.0"
|
"node": ">=0.10.0"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/react-datepicker": {
|
||||||
|
"version": "8.1.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/react-datepicker/-/react-datepicker-8.1.0.tgz",
|
||||||
|
"integrity": "sha512-11gIOrBGK1MOvl4+wxGv4YxTqXf+uoRPtKstYhb/P1cBdRdOP1sL26VE31apmDnvw8wSYfJe9AWwWbKqmM9tzw==",
|
||||||
|
"license": "MIT",
|
||||||
|
"dependencies": {
|
||||||
|
"@floating-ui/react": "^0.27.3",
|
||||||
|
"clsx": "^2.1.1",
|
||||||
|
"date-fns": "^4.1.0"
|
||||||
|
},
|
||||||
|
"peerDependencies": {
|
||||||
|
"react": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc",
|
||||||
|
"react-dom": "^16.9.0 || ^17 || ^18 || ^19 || ^19.0.0-rc"
|
||||||
|
}
|
||||||
|
},
|
||||||
"node_modules/react-day-picker": {
|
"node_modules/react-day-picker": {
|
||||||
"version": "9.5.1",
|
"version": "9.5.1",
|
||||||
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.5.1.tgz",
|
"resolved": "https://registry.npmjs.org/react-day-picker/-/react-day-picker-9.5.1.tgz",
|
||||||
@@ -13872,6 +13918,12 @@
|
|||||||
"upper-case": "^1.1.1"
|
"upper-case": "^1.1.1"
|
||||||
}
|
}
|
||||||
},
|
},
|
||||||
|
"node_modules/tabbable": {
|
||||||
|
"version": "6.2.0",
|
||||||
|
"resolved": "https://registry.npmjs.org/tabbable/-/tabbable-6.2.0.tgz",
|
||||||
|
"integrity": "sha512-Cat63mxsVJlzYvN51JmVXIgNoUokrIaT2zLclCXjRd8boZ0004U4KCs/sToJ75C6sdlByWxpYnb5Boif1VSFew==",
|
||||||
|
"license": "MIT"
|
||||||
|
},
|
||||||
"node_modules/tailwind-merge": {
|
"node_modules/tailwind-merge": {
|
||||||
"version": "2.6.0",
|
"version": "2.6.0",
|
||||||
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
"resolved": "https://registry.npmjs.org/tailwind-merge/-/tailwind-merge-2.6.0.tgz",
|
||||||
|
|||||||
Reference in New Issue
Block a user