added participant overview, (admin), Date input

This commit is contained in:
PxlLoewe
2025-03-01 17:09:09 +01:00
parent 4824ade795
commit e964c7d175
10 changed files with 385 additions and 161 deletions

View File

@@ -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",

View File

@@ -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"
> >

View File

@@ -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 } });
}; };

View File

@@ -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 && (

View File

@@ -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>
); );
} }

View File

@@ -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> );
);
}; };

View File

@@ -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
} }

View File

@@ -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
View 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
View File

@@ -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",