Ajout de la prise en charge de PostgreSQL, configuration de Prisma, et création d'une API pour gérer les calendriers par défaut et le partage.

This commit is contained in:
Kevin 2025-02-28 17:25:12 +01:00
parent c967b58981
commit 9f0b49a0cc
23 changed files with 2486 additions and 13 deletions

6
.gitignore vendored
View File

@ -16,4 +16,8 @@ copy.sh
start.sh start.sh
# Playbook développement # Playbook développement
ansible/playbooks/dev ansible/playbooks/dev
# Migrations Prisma
#TODO: Supprimer en prod
front/prisma/migrations

View File

@ -103,6 +103,15 @@
retries: 3 retries: 3
delay: 5 delay: 5
#TODO: Supprimer en prod
- name: Supprimer tous les conteneurs
command: "docker compose down -v"
register: rm_status
until: rm_status is success
retries: 3
delay: 5
ignore_errors: yes
- name: Lancer le service Traefik - name: Lancer le service Traefik
command: "docker compose up -d --build --remove-orphans {{ traefik_service_name }}" command: "docker compose up -d --build --remove-orphans {{ traefik_service_name }}"
args: args:

View File

@ -0,0 +1,33 @@
---
- name: Installer et configurer PostgreSQL
hosts: servers
become: true
gather_facts: true
vars:
git_repo: "https://gite.slm-lab.net/Chabdeltsang/Neah-Enkun.git"
git_dest: "/opt/Neah-Enkun"
git_branch: "master"
pre_tasks:
- name: Cloner le dépôt Git
git:
repo: "{{ git_repo }}"
dest: "{{ git_dest }}"
version: "{{ git_branch }}"
update: true
force: true
register: git_status
until: git_status is success
retries: 3
delay: 5
tasks:
- name: Lancer le service PostgreSQL
command: "docker compose up -d --build --remove-orphans postgresql"
args:
chdir: "{{ git_dest }}"
register: postgresql_launch
until: postgresql_launch is success
retries: 3
delay: 5

View File

@ -17,6 +17,7 @@ PLAYBOOKS=(
"playbooks/dev/3_1_keycloak_dev.yml" "playbooks/dev/3_1_keycloak_dev.yml"
"playbooks/4_mysql.yml" "playbooks/4_mysql.yml"
"playbooks/5_nextcloud.yml" "playbooks/5_nextcloud.yml"
"playbooks/6_postgresql.yml"
"playbooks/0_front.yml" "playbooks/0_front.yml"
) )

View File

@ -7,4 +7,6 @@ KEYCLOAK_REALM=master
KEYCLOAK_ISSUER=http://172.16.32.141:8090/realms/master KEYCLOAK_ISSUER=http://172.16.32.141:8090/realms/master
KEYCLOAK_BASE_URL=http://172.16.32.141:8090 KEYCLOAK_BASE_URL=http://172.16.32.141:8090
NEXTCLOUD_URL=http://cloud.neah.local NEXTCLOUD_URL=http://cloud.neah.local
DATABASE_URL="postgresql://enkun:183d9ad665c9257703c2e0703f111d240266a56b33e10df04fb8c565e55e0b94@172.16.32.141:5432/enkun?schema=public"

View File

@ -13,6 +13,10 @@ RUN npm ci
# Copy the rest of the application # Copy the rest of the application
COPY . . COPY . .
# Initialize Prisma
RUN npx prisma generate
RUN npx prisma migrate dev --name init
# Build the Next.js application # Build the Next.js application
RUN npm run build RUN npm run build

View File

@ -0,0 +1,270 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
/**
* Handles the GET request to retrieve a specific event from a calendar.
*
* @param req - The incoming Next.js request object.
* @param params - An object containing the route parameters.
* @param params.id - The ID of the calendar.
* @param params.eventId - The ID of the event.
* @returns A JSON response containing the event data or an error message.
*
* The function performs the following steps:
* 1. Checks if the user is authenticated.
* 2. Verifies that the calendar exists and belongs to the authenticated user.
* 3. Verifies that the event exists and belongs to the specified calendar.
* 4. Returns the event data if all checks pass.
*
* Possible error responses:
* - 401: User is not authenticated.
* - 403: User is not authorized to access the calendar.
* - 404: Calendar or event not found.
* - 500: Server error occurred while retrieving the event.
*/
export async function GET(
req: NextRequest,
{ params }: { params: { id: string; eventId: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier appartient à l'utilisateur
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
const event = await prisma.event.findUnique({
where: {
id: params.eventId,
},
});
if (!event) {
return NextResponse.json(
{ error: "Événement non trouvé" },
{ status: 404 }
);
}
// Vérifier que l'événement appartient bien au calendrier
if (event.calendarId !== params.id) {
return NextResponse.json(
{ error: "Événement non trouvé dans ce calendrier" },
{ status: 404 }
);
}
return NextResponse.json(event);
} catch (error) {
console.error("Erreur lors de la récupération de l'événement:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the PUT request to update an event in a calendar.
*
* @param req - The incoming request object.
* @param params - The route parameters containing the calendar ID and event ID.
* @returns A JSON response indicating the result of the update operation.
*
* The function performs the following steps:
* 1. Retrieves the server session to check if the user is authenticated.
* 2. Verifies that the calendar belongs to the authenticated user.
* 3. Checks if the event exists and belongs to the specified calendar.
* 4. Validates the request payload to ensure required fields are present.
* 5. Updates the event with the provided data.
* 6. Returns the updated event or an appropriate error response.
*
* Possible error responses:
* - 401: User is not authenticated.
* - 403: User is not authorized to update the calendar.
* - 404: Calendar or event not found.
* - 400: Validation error for missing required fields.
* - 500: Server error during the update process.
*/
export async function PUT(
req: NextRequest,
{ params }: { params: { id: string; eventId: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier appartient à l'utilisateur
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
// Vérifier que l'événement existe et appartient au calendrier
const existingEvent = await prisma.event.findUnique({
where: {
id: params.eventId,
},
});
if (!existingEvent) {
return NextResponse.json(
{ error: "Événement non trouvé" },
{ status: 404 }
);
}
if (existingEvent.calendarId !== params.id) {
return NextResponse.json(
{ error: "Événement non trouvé dans ce calendrier" },
{ status: 404 }
);
}
const { title, description, start, end, location, isAllDay } =
await req.json();
// Validation
if (!title) {
return NextResponse.json(
{ error: "Le titre est requis" },
{ status: 400 }
);
}
if (!start || !end) {
return NextResponse.json(
{ error: "Les dates de début et de fin sont requises" },
{ status: 400 }
);
}
const updatedEvent = await prisma.event.update({
where: {
id: params.eventId,
},
data: {
title,
description,
start: new Date(start),
end: new Date(end),
location,
isAllDay: isAllDay || false,
},
});
return NextResponse.json(updatedEvent);
} catch (error) {
console.error("Erreur lors de la mise à jour de l'événement:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the DELETE request to remove an event from a calendar.
*
* @param req - The incoming Next.js request object.
* @param params - An object containing the parameters from the request URL.
* @param params.id - The ID of the calendar.
* @param params.eventId - The ID of the event to be deleted.
* @returns A JSON response indicating the result of the deletion operation.
*
* @throws Will return a 401 status if the user is not authenticated.
* @throws Will return a 404 status if the calendar or event is not found.
* @throws Will return a 403 status if the user is not authorized to delete the event.
* @throws Will return a 500 status if there is a server error during the deletion process.
*/
export async function DELETE(
req: NextRequest,
{ params }: { params: { id: string; eventId: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier appartient à l'utilisateur
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
// Vérifier que l'événement existe et appartient au calendrier
const existingEvent = await prisma.event.findUnique({
where: {
id: params.eventId,
},
});
if (!existingEvent) {
return NextResponse.json(
{ error: "Événement non trouvé" },
{ status: 404 }
);
}
if (existingEvent.calendarId !== params.id) {
return NextResponse.json(
{ error: "Événement non trouvé dans ce calendrier" },
{ status: 404 }
);
}
await prisma.event.delete({
where: {
id: params.eventId,
},
});
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error("Erreur lors de la suppression de l'événement:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@ -0,0 +1,171 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
/**
* Handles the GET request to retrieve events for a specific calendar.
*
* @param req - The incoming request object.
* @param params - An object containing the route parameters.
* @param params.id - The ID of the calendar.
* @returns A JSON response containing the events or an error message.
*
* The function performs the following steps:
* 1. Retrieves the server session to check if the user is authenticated.
* 2. Verifies that the calendar exists and belongs to the authenticated user.
* 3. Retrieves and filters events based on optional date parameters (`start` and `end`).
* 4. Returns the filtered events in ascending order of their start date.
*
* Possible response statuses:
* - 200: Successfully retrieved events.
* - 401: User is not authenticated.
* - 403: User is not authorized to access the calendar.
* - 404: Calendar not found.
* - 500: Server error occurred while retrieving events.
*/
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier appartient à l'utilisateur
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
// Récupérer les paramètres de filtrage de date s'ils existent
const { searchParams } = new URL(req.url);
const startParam = searchParams.get("start");
const endParam = searchParams.get("end");
let whereClause: any = {
calendarId: params.id,
};
if (startParam && endParam) {
whereClause.AND = [
{
start: {
lte: new Date(endParam),
},
},
{
end: {
gte: new Date(startParam),
},
},
];
}
const events = await prisma.event.findMany({
where: whereClause,
orderBy: {
start: "asc",
},
});
return NextResponse.json(events);
} catch (error) {
console.error("Erreur lors de la récupération des événements:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the creation of a new event for a specific calendar.
*
* @param req - The incoming request object.
* @param params - An object containing the route parameters.
* @param params.id - The ID of the calendar to which the event will be added.
* @returns A JSON response with the created event data or an error message.
*
* @throws {401} If the user is not authenticated.
* @throws {404} If the specified calendar is not found.
* @throws {403} If the user is not authorized to add events to the specified calendar.
* @throws {400} If the required fields (title, start, end) are missing.
* @throws {500} If there is a server error during event creation.
*/
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
const { title, description, start, end, location, isAllDay } =
await req.json();
// Validation
if (!title) {
return NextResponse.json(
{ error: "Le titre est requis" },
{ status: 400 }
);
}
if (!start || !end) {
return NextResponse.json(
{ error: "Les dates de début et de fin sont requises" },
{ status: 400 }
);
}
const event = await prisma.event.create({
data: {
title,
description,
start: new Date(start),
end: new Date(end),
location,
isAllDay: isAllDay || false,
calendarId: params.id,
},
});
return NextResponse.json(event, { status: 201 });
} catch (error) {
console.error("Erreur lors de la création de l'événement:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@ -0,0 +1,181 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
/**
* Handles GET requests to retrieve a calendar by its ID.
*
* @param req - The incoming request object.
* @param params - An object containing the route parameters.
* @param params.id - The ID of the calendar to retrieve.
* @returns A JSON response containing the calendar data if found and authorized,
* or an error message with the appropriate HTTP status code.
*
* - 401: If the user is not authenticated.
* - 403: If the user is not authorized to access the calendar.
* - 404: If the calendar is not found.
* - 500: If there is a server error during the retrieval process.
*/
export async function GET(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
// Vérification que l'utilisateur est bien le propriétaire
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
return NextResponse.json(calendar);
} catch (error) {
console.error("Erreur lors de la récupération du calendrier:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the PUT request to update a calendar.
*
* @param req - The incoming request object.
* @param params - An object containing the route parameters.
* @param params.id - The ID of the calendar to update.
* @returns A JSON response with the updated calendar data or an error message.
*
* @throws {401} If the user is not authenticated.
* @throws {404} If the calendar is not found.
* @throws {403} If the user is not authorized to update the calendar.
* @throws {400} If the calendar name is not provided.
* @throws {500} If there is a server error during the update process.
*/
export async function PUT(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier existe et appartient à l'utilisateur
const existingCalendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!existingCalendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (existingCalendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
const { name, color, description } = await req.json();
// Validation
if (!name) {
return NextResponse.json(
{ error: "Le nom du calendrier est requis" },
{ status: 400 }
);
}
const updatedCalendar = await prisma.calendar.update({
where: {
id: params.id,
},
data: {
name,
color,
description,
},
});
return NextResponse.json(updatedCalendar);
} catch (error) {
console.error("Erreur lors de la mise à jour du calendrier:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the DELETE request to remove a calendar by its ID.
*
* @param req - The incoming Next.js request object.
* @param params - An object containing the route parameters.
* @param params.id - The ID of the calendar to be deleted.
* @returns A JSON response indicating the result of the deletion operation.
*
* - If the user is not authenticated, returns a 401 status with an error message.
* - If the calendar does not exist, returns a 404 status with an error message.
* - If the calendar does not belong to the authenticated user, returns a 403 status with an error message.
* - If the calendar is successfully deleted, returns a 204 status with no content.
* - If an error occurs during the deletion process, returns a 500 status with an error message.
*/
export async function DELETE(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier existe et appartient à l'utilisateur
const existingCalendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!existingCalendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (existingCalendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
await prisma.calendar.delete({
where: {
id: params.id,
},
});
return new NextResponse(null, { status: 204 });
} catch (error) {
console.error("Erreur lors de la suppression du calendrier:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@ -0,0 +1,53 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
import crypto from "crypto";
// Non testé, généré automatiquement par IA
export async function POST(
req: NextRequest,
{ params }: { params: { id: string } }
) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier que le calendrier appartient à l'utilisateur
const calendar = await prisma.calendar.findUnique({
where: {
id: params.id,
},
});
if (!calendar) {
return NextResponse.json(
{ error: "Calendrier non trouvé" },
{ status: 404 }
);
}
if (calendar.userId !== session.user.username) {
return NextResponse.json({ error: "Non autorisé" }, { status: 403 });
}
// Générer un token de partage
const shareToken = crypto.randomBytes(32).toString("hex");
// Dans une implémentation réelle, on stockerait ce token dans la base de données
// avec une date d'expiration et des permissions
const shareUrl = `${process.env.NEXT_PUBLIC_BASE_URL}/calendars/shared/${shareToken}`;
return NextResponse.json({
shareUrl,
shareToken,
});
} catch (error) {
console.error("Erreur lors de la création du lien de partage:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@ -0,0 +1,57 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
/**
* Handles the creation of a default calendar for an authenticated user.
*
* This function checks if the user already has a default calendar named "Calendrier principal".
* If such a calendar exists, it returns the existing calendar.
* Otherwise, it creates a new default calendar for the user.
*
* @param req - The incoming request object.
* @returns A JSON response containing the existing or newly created calendar, or an error message.
*
* @throws Will return a 401 status if the user is not authenticated.
* @throws Will return a 500 status if there is a server error during the calendar creation process.
*/
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
// Vérifier si l'utilisateur a déjà un calendrier par défaut
const existingCalendar = await prisma.calendar.findFirst({
where: {
userId: session.user.username,
name: "Calendrier principal",
},
});
if (existingCalendar) {
return NextResponse.json(existingCalendar);
}
// Créer un calendrier par défaut
const calendar = await prisma.calendar.create({
data: {
name: "Calendrier principal",
color: "#0082c9",
description: "Calendrier principal",
userId: session.user.username,
},
});
return NextResponse.json(calendar, { status: 201 });
} catch (error) {
console.error(
"Erreur lors de la création du calendrier par défaut:",
error
);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@ -0,0 +1,93 @@
import { NextRequest, NextResponse } from "next/server";
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { prisma } from "@/lib/prisma";
/**
* Handles the GET request to retrieve calendars for the authenticated user.
*
* @param {NextRequest} req - The incoming request object.
* @returns {Promise<NextResponse>} - A promise that resolves to a JSON response containing the calendars or an error message.
*
* The function performs the following steps:
* 1. Retrieves the server session using `getServerSession`.
* 2. Checks if the user is authenticated by verifying the presence of `session.user.username`.
* - If not authenticated, returns a 401 response with an error message.
* 3. Attempts to fetch the calendars associated with the authenticated user from the database.
* - If successful, returns the calendars in a JSON response.
* - If an error occurs during the database query, logs the error and returns a 500 response with an error message.
*/
export async function GET(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const calendars = await prisma.calendar.findMany({
where: {
userId: session.user.username,
},
orderBy: {
createdAt: "desc",
},
});
return NextResponse.json(calendars);
} catch (error) {
console.error("Erreur lors de la récupération des calendriers:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}
/**
* Handles the POST request to create a new calendar.
*
* @param {NextRequest} req - The incoming request object.
* @returns {Promise<NextResponse>} The response object containing the created calendar or an error message.
*
* @throws {Error} If there is an issue with the request or server.
*
* The function performs the following steps:
* 1. Retrieves the server session using `getServerSession`.
* 2. Checks if the user is authenticated by verifying the presence of `session.user.username`.
* 3. Parses the request body to extract `name`, `color`, and `description`.
* 4. Validates that the `name` field is provided.
* 5. Creates a new calendar entry in the database using Prisma.
* 6. Returns the created calendar with a 201 status code.
* 7. Catches and logs any errors, returning a 500 status code with an error message.
*/
export async function POST(req: NextRequest) {
const session = await getServerSession(authOptions);
if (!session?.user?.username) {
return NextResponse.json({ error: "Non authentifié" }, { status: 401 });
}
try {
const { name, color, description } = await req.json();
// Validation
if (!name) {
return NextResponse.json(
{ error: "Le nom du calendrier est requis" },
{ status: 400 }
);
}
const calendar = await prisma.calendar.create({
data: {
name,
color: color || "#0082c9",
description,
userId: session.user.username,
},
});
return NextResponse.json(calendar, { status: 201 });
} catch (error) {
console.error("Erreur lors de la création du calendrier:", error);
return NextResponse.json({ error: "Erreur serveur" }, { status: 500 });
}
}

View File

@ -0,0 +1,54 @@
import { getServerSession } from "next-auth/next";
import { authOptions } from "@/app/api/auth/[...nextauth]/route";
import { redirect } from "next/navigation";
import { prisma } from "@/lib/prisma";
import { CalendarClient } from "@/components/calendar/calendar-client";
export const metadata = {
title: "Enkun - Calendrier",
description: "Gérez vos rendez-vous et événements",
};
export default async function CalendarPage() {
const session = await getServerSession(authOptions);
if (!session?.user) {
redirect("/api/auth/signin");
}
// Récupérer tous les calendriers de l'utilisateur
const userCalendars = await prisma.calendar.findMany({
where: {
userId: session.user.username || session.user.email,
},
orderBy: {
createdAt: "desc",
},
});
// Si aucun calendrier n'existe, en créer un par défaut
let calendars = userCalendars;
if (calendars.length === 0) {
const defaultCalendar = await prisma.calendar.create({
data: {
name: "Calendrier principal",
color: "#0082c9",
description: "Calendrier par défaut",
userId: session.user.username || session.user.email,
},
});
calendars = [defaultCalendar];
}
return (
<div className='container mx-auto py-8'>
<div className='mb-6'>
<h1 className='text-3xl font-bold'>Calendrier</h1>
<p className='text-muted-foreground'>
Gérez vos rendez-vous et événements
</p>
</div>
<CalendarClient initialCalendars={calendars} />
</div>
);
}

View File

@ -0,0 +1,268 @@
"use client";
import { useState, useEffect, useRef } from "react";
import FullCalendar from "@fullcalendar/react";
import dayGridPlugin from "@fullcalendar/daygrid";
import timeGridPlugin from "@fullcalendar/timegrid";
import interactionPlugin from "@fullcalendar/interaction";
import frLocale from "@fullcalendar/core/locales/fr";
import { Card } from "@/components/ui/card";
import { Button } from "@/components/ui/button";
import { Tabs, TabsContent, TabsList, TabsTrigger } from "@/components/ui/tabs";
import { Loader2, Plus } from "lucide-react";
import { useCalendarEvents } from "@/hooks/use-calendar-events";
import { EventDialog } from "@/components/calendar/event-dialog";
import { Calendar as CalendarType } from "@prisma/client";
import { useToast } from "@/components/ui/use-toast";
interface CalendarClientProps {
initialCalendars: CalendarType[];
}
export function CalendarClient({ initialCalendars }: CalendarClientProps) {
const [calendars, setCalendars] = useState<CalendarType[]>(initialCalendars);
const [selectedCalendarId, setSelectedCalendarId] = useState<string>(
initialCalendars[0]?.id || ""
);
const [view, setView] = useState<
"dayGridMonth" | "timeGridWeek" | "timeGridDay"
>("dayGridMonth");
const [isDialogOpen, setIsDialogOpen] = useState(false);
const [selectedEvent, setSelectedEvent] = useState<any>(null);
const [dateRange, setDateRange] = useState({
start: new Date(),
end: new Date(new Date().setMonth(new Date().getMonth() + 1)),
});
const calendarRef = useRef<any>(null);
const { toast } = useToast();
const {
events,
loading,
error,
refresh,
createEvent,
updateEvent,
deleteEvent,
} = useCalendarEvents(selectedCalendarId, dateRange.start, dateRange.end);
// Mettre à jour la plage de dates lorsque la vue change
const handleDatesSet = (arg: any) => {
setDateRange({
start: arg.start,
end: arg.end,
});
};
// Gérer la sélection d'une plage de dates pour créer un événement
const handleDateSelect = (selectInfo: any) => {
setSelectedEvent({
start: selectInfo.startStr,
end: selectInfo.endStr,
allDay: selectInfo.allDay,
});
setIsDialogOpen(true);
};
// Gérer le clic sur un événement existant
const handleEventClick = (clickInfo: any) => {
setSelectedEvent({
id: clickInfo.event.id,
title: clickInfo.event.title,
description: clickInfo.event.extendedProps.description,
start: clickInfo.event.startStr,
end: clickInfo.event.endStr,
location: clickInfo.event.extendedProps.location,
allDay: clickInfo.event.allDay,
});
setIsDialogOpen(true);
};
// Gérer la création ou mise à jour d'un événement
const handleEventSave = async (eventData: any) => {
try {
if (eventData.id) {
await updateEvent(eventData);
toast({
title: "Événement mis à jour",
description: "L'événement a été modifié avec succès.",
});
} else {
await createEvent({
...eventData,
calendarId: selectedCalendarId,
});
toast({
title: "Événement créé",
description: "L'événement a été ajouté au calendrier.",
});
}
setIsDialogOpen(false);
refresh();
} catch (error) {
console.error("Erreur lors de la sauvegarde de l'événement:", error);
toast({
title: "Erreur",
description: "Impossible d'enregistrer l'événement.",
variant: "destructive",
});
}
};
// Gérer la suppression d'un événement
const handleEventDelete = async (eventId: string) => {
try {
await deleteEvent(eventId);
toast({
title: "Événement supprimé",
description: "L'événement a été supprimé du calendrier.",
});
setIsDialogOpen(false);
refresh();
} catch (error) {
console.error("Erreur lors de la suppression de l'événement:", error);
toast({
title: "Erreur",
description: "Impossible de supprimer l'événement.",
variant: "destructive",
});
}
};
// Changer la vue du calendrier
const handleViewChange = (
newView: "dayGridMonth" | "timeGridWeek" | "timeGridDay"
) => {
setView(newView);
if (calendarRef.current) {
const calendarApi = calendarRef.current.getApi();
calendarApi.changeView(newView);
}
};
// Changer le calendrier sélectionné
const handleCalendarChange = (calendarId: string) => {
setSelectedCalendarId(calendarId);
};
return (
<div className='space-y-4'>
{/* Options et filtres du calendrier */}
<div className='flex flex-wrap justify-between items-center gap-4 mb-4'>
<div className='flex flex-wrap gap-2'>
{calendars.map((calendar) => (
<Button
key={calendar.id}
variant={
calendar.id === selectedCalendarId ? "default" : "outline"
}
onClick={() => handleCalendarChange(calendar.id)}
style={{
backgroundColor:
calendar.id === selectedCalendarId
? calendar.color
: undefined,
borderColor: calendar.color,
}}
>
{calendar.name}
</Button>
))}
</div>
<Button
onClick={() => {
setSelectedEvent(null);
setIsDialogOpen(true);
}}
>
<Plus className='mr-2 h-4 w-4' />
Nouvel événement
</Button>
</div>
{/* Sélecteur de vue */}
<Tabs value={view} className='w-full'>
<TabsList className='mb-4'>
<TabsTrigger
value='dayGridMonth'
onClick={() => handleViewChange("dayGridMonth")}
>
Mois
</TabsTrigger>
<TabsTrigger
value='timeGridWeek'
onClick={() => handleViewChange("timeGridWeek")}
>
Semaine
</TabsTrigger>
<TabsTrigger
value='timeGridDay'
onClick={() => handleViewChange("timeGridDay")}
>
Jour
</TabsTrigger>
</TabsList>
{/* Affichage du calendrier */}
<Card className='p-4'>
{error && (
<div className='p-4 mb-4 text-red-500 bg-red-50 rounded-md'>
Erreur: {error.message}
</div>
)}
{loading && !events.length ? (
<div className='h-96 flex items-center justify-center'>
<Loader2 className='h-8 w-8 animate-spin text-primary' />
<span className='ml-2'>Chargement des événements...</span>
</div>
) : (
<FullCalendar
ref={calendarRef}
plugins={[dayGridPlugin, timeGridPlugin, interactionPlugin]}
initialView={view}
headerToolbar={{
left: "prev,next today",
center: "title",
right: "",
}}
events={events.map((event) => ({
id: event.id,
title: event.title,
start: event.start,
end: event.end,
allDay: event.isAllDay,
extendedProps: {
description: event.description,
location: event.location,
},
}))}
selectable={true}
selectMirror={true}
dayMaxEvents={true}
weekends={true}
locale={frLocale}
select={handleDateSelect}
eventClick={handleEventClick}
datesSet={handleDatesSet}
height='auto'
aspectRatio={1.8}
/>
)}
</Card>
</Tabs>
{/* Dialogue pour créer/modifier un événement */}
{isDialogOpen && (
<EventDialog
open={isDialogOpen}
event={selectedEvent}
onClose={() => setIsDialogOpen(false)}
onSave={handleEventSave}
onDelete={selectedEvent?.id ? handleEventDelete : undefined}
/>
)}
</div>
);
}

View File

@ -0,0 +1,220 @@
"use client";
import { useState } from "react";
import {
Dialog,
DialogContent,
DialogHeader,
DialogTitle,
DialogFooter,
} from "@/components/ui/dialog";
import { Button } from "@/components/ui/button";
import { Input } from "@/components/ui/input";
import { Label } from "@/components/ui/label";
import { Textarea } from "@/components/ui/textarea";
import { Checkbox } from "@/components/ui/checkbox";
import { parseISO, format } from "date-fns";
import { fr } from "date-fns/locale";
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "../ui/alert-dialog";
interface EventDialogProps {
open: boolean;
event?: any;
onClose: () => void;
onSave: (event: any) => void;
onDelete?: (eventId: string) => void;
}
export function EventDialog({
open,
event,
onClose,
onSave,
onDelete,
}: EventDialogProps) {
const [title, setTitle] = useState(event?.title || "");
const [description, setDescription] = useState(event?.description || "");
const [location, setLocation] = useState(event?.location || "");
const [start, setStart] = useState(event?.start || "");
const [end, setEnd] = useState(event?.end || "");
const [allDay, setAllDay] = useState(event?.allDay || false);
const [confirmDelete, setConfirmDelete] = useState(false);
// Formater les dates pour l'affichage
const formatDate = (dateStr: string) => {
if (!dateStr) return "";
try {
const date = parseISO(dateStr);
return format(date, allDay ? "yyyy-MM-dd" : "yyyy-MM-dd'T'HH:mm", {
locale: fr,
});
} catch (e) {
return dateStr;
}
};
// Gérer le changement de l'option "Toute la journée"
const handleAllDayChange = (checked: boolean) => {
setAllDay(checked);
// Ajuster les dates si nécessaire
if (checked && start) {
const startDate = parseISO(start);
setStart(format(startDate, "yyyy-MM-dd"));
if (end) {
const endDate = parseISO(end);
setEnd(format(endDate, "yyyy-MM-dd"));
}
}
};
// Enregistrer l'événement
const handleSave = () => {
onSave({
id: event?.id,
title,
description,
location,
start,
end,
isAllDay: allDay,
});
};
// Supprimer l'événement
const handleDelete = () => {
if (onDelete && event?.id) {
onDelete(event.id);
}
};
return (
<>
<Dialog open={open} onOpenChange={onClose}>
<DialogContent className='sm:max-w-[550px]'>
<DialogHeader>
<DialogTitle>
{event?.id ? "Modifier l'événement" : "Nouvel événement"}
</DialogTitle>
</DialogHeader>
<div className='grid gap-4 py-4'>
<div className='grid gap-2'>
<Label htmlFor='title'>Titre *</Label>
<Input
id='title'
value={title}
onChange={(e) => setTitle(e.target.value)}
placeholder='Ajouter un titre'
required
/>
</div>
<div className='grid gap-2'>
<Label htmlFor='description'>Description</Label>
<Textarea
id='description'
value={description}
onChange={(e) => setDescription(e.target.value)}
placeholder="Description de l'événement"
className='min-h-[100px]'
/>
</div>
<div className='grid gap-2'>
<Label htmlFor='location'>Lieu</Label>
<Input
id='location'
value={location}
onChange={(e) => setLocation(e.target.value)}
placeholder='Emplacement'
/>
</div>
<div className='flex items-center space-x-2'>
<Checkbox
id='allDay'
checked={allDay}
onCheckedChange={handleAllDayChange}
/>
<Label htmlFor='allDay'>Toute la journée</Label>
</div>
<div className='grid grid-cols-2 gap-4'>
<div className='grid gap-2'>
<Label htmlFor='start'>Début *</Label>
<Input
id='start'
type={allDay ? "date" : "datetime-local"}
value={formatDate(start)}
onChange={(e) => setStart(e.target.value)}
required
/>
</div>
<div className='grid gap-2'>
<Label htmlFor='end'>Fin *</Label>
<Input
id='end'
type={allDay ? "date" : "datetime-local"}
value={formatDate(end)}
onChange={(e) => setEnd(e.target.value)}
required
/>
</div>
</div>
</div>
<DialogFooter className='flex justify-between'>
<div>
{event?.id && onDelete && (
<Button
variant='destructive'
onClick={() => setConfirmDelete(true)}
>
Supprimer
</Button>
)}
</div>
<div className='flex space-x-2'>
<Button variant='outline' onClick={onClose}>
Annuler
</Button>
<Button onClick={handleSave} disabled={!title || !start || !end}>
Enregistrer
</Button>
</div>
</DialogFooter>
</DialogContent>
</Dialog>
{/* Dialogue de confirmation de suppression */}
<AlertDialog open={confirmDelete} onOpenChange={setConfirmDelete}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>Confirmer la suppression</AlertDialogTitle>
<AlertDialogDescription>
Êtes-vous sûr de vouloir supprimer cet événement ? Cette action
est irréversible.
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>Annuler</AlertDialogCancel>
<AlertDialogAction onClick={handleDelete}>
Supprimer
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
);
}

View File

@ -10,7 +10,6 @@ import {
GitFork, GitFork,
Building2, Building2,
Users, Users,
User,
Calendar, Calendar,
} from "lucide-react"; } from "lucide-react";
import { Button } from "@/components/ui/button"; import { Button } from "@/components/ui/button";
@ -27,12 +26,13 @@ const menuItems = [
title: "Users", title: "Users",
icon: Users, icon: Users,
href: "/users", href: "/users",
external: false,
}, },
{ {
title: "Calendar", title: "Calendar",
icon: Calendar, icon: Calendar,
href: "http://cloud.neah.local/apps/calendar/dayGridMonth/now", href: "/calendar",
external: true, external: false,
}, },
{ {
title: "Flow", title: "Flow",

View File

@ -0,0 +1,106 @@
import { useState, useEffect } from "react";
import { Event } from "@prisma/client";
export function useCalendarEvents(calendarId: string, start: Date, end: Date) {
const [events, setEvents] = useState<Event[]>([]);
const [loading, setLoading] = useState(false);
const [error, setError] = useState<Error | null>(null);
// Charger les événements
const fetchEvents = async () => {
if (!calendarId) return;
try {
setLoading(true);
setError(null);
const response = await fetch(
`/api/calendars/${calendarId}/events?` +
`start=${start.toISOString()}&end=${end.toISOString()}`
);
if (!response.ok) {
throw new Error(`Erreur ${response.status}: ${await response.text()}`);
}
const data = await response.json();
setEvents(data);
} catch (err) {
console.error("Erreur lors du chargement des événements:", err);
setError(err as Error);
} finally {
setLoading(false);
}
};
// Créer un événement
const createEvent = async (eventData: any) => {
const response = await fetch(`/api/calendars/${calendarId}/events`, {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventData),
});
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return await response.json();
};
// Mettre à jour un événement
const updateEvent = async (eventData: any) => {
const response = await fetch(
`/api/calendars/${calendarId}/events/${eventData.id}`,
{
method: "PUT",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(eventData),
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return await response.json();
};
// Supprimer un événement
const deleteEvent = async (eventId: string) => {
const response = await fetch(
`/api/calendars/${calendarId}/events/${eventId}`,
{
method: "DELETE",
}
);
if (!response.ok) {
const errorText = await response.text();
throw new Error(errorText);
}
return true;
};
// Charger les événements quand le calendrier ou les dates changent
useEffect(() => {
fetchEvents();
}, [calendarId, start.toISOString(), end.toISOString()]);
return {
events,
loading,
error,
refresh: fetchEvents,
createEvent,
updateEvent,
deleteEvent,
};
}

10
front/lib/prisma.ts Normal file
View File

@ -0,0 +1,10 @@
// front/lib/prisma.ts
import { PrismaClient } from "@prisma/client";
declare global {
var prisma: PrismaClient | undefined;
}
export const prisma = global.prisma || new PrismaClient();
if (process.env.NODE_ENV !== "production") global.prisma = prisma;

710
front/package-lock.json generated
View File

@ -8,7 +8,9 @@
"name": "neah", "name": "neah",
"version": "0.1.0", "version": "0.1.0",
"dependencies": { "dependencies": {
"@fullcalendar/react": "^6.1.15",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@prisma/client": "^6.4.1",
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.1",
@ -40,8 +42,9 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
"date-fns": "3.0.0", "date-fns": "^3.0.0",
"embla-carousel-react": "8.5.1", "embla-carousel-react": "8.5.1",
"fullcalendar": "^6.1.15",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"next": "14.2.24", "next": "14.2.24",
@ -65,6 +68,7 @@
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"postcss": "^8", "postcss": "^8",
"prisma": "^6.4.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5" "typescript": "^5"
} }
@ -91,6 +95,431 @@
"node": ">=6.9.0" "node": ">=6.9.0"
} }
}, },
"node_modules/@esbuild/aix-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/aix-ppc64/-/aix-ppc64-0.25.0.tgz",
"integrity": "sha512-O7vun9Sf8DFjH2UtqK8Ku3LkquL9SZL8OLY1T5NZkA34+wG3OQF7cl4Ql8vdNzM6fzBbYfLaiRLIOZ+2FOCgBQ==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"aix"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm/-/android-arm-0.25.0.tgz",
"integrity": "sha512-PTyWCYYiU0+1eJKmw21lWtC+d08JDZPQ5g+kFyxP0V+es6VPPSUhM6zk8iImp2jbV6GwjX4pap0JFbUQN65X1g==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-arm64/-/android-arm64-0.25.0.tgz",
"integrity": "sha512-grvv8WncGjDSyUBjN9yHXNt+cq0snxXbDxy5pJtzMKGmmpPxeAmAhWxXI+01lU5rwZomDgD3kJwulEnhTRUd6g==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/android-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/android-x64/-/android-x64-0.25.0.tgz",
"integrity": "sha512-m/ix7SfKG5buCnxasr52+LI78SQ+wgdENi9CqyCXwjVR2X4Jkz+BpC3le3AoBPYTC9NHklwngVXvbJ9/Akhrfg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"android"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-arm64/-/darwin-arm64-0.25.0.tgz",
"integrity": "sha512-mVwdUb5SRkPayVadIOI78K7aAnPamoeFR2bT5nszFUZ9P8UpK4ratOdYbZZXYSqPKMHfS1wdHCJk1P1EZpRdvw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/darwin-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz",
"integrity": "sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"darwin"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-arm64/-/freebsd-arm64-0.25.0.tgz",
"integrity": "sha512-VN4ocxy6dxefN1MepBx/iD1dH5K8qNtNe227I0mnTRjry8tj5MRk4zprLEdG8WPyAPb93/e4pSgi1SoHdgOa4w==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/freebsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/freebsd-x64/-/freebsd-x64-0.25.0.tgz",
"integrity": "sha512-mrSgt7lCh07FY+hDD1TxiTyIHyttn6vnjesnPoVDNmDfOmggTLXRv8Id5fNZey1gl/V2dyVK1VXXqVsQIiAk+A==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"freebsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm/-/linux-arm-0.25.0.tgz",
"integrity": "sha512-vkB3IYj2IDo3g9xX7HqhPYxVkNQe8qTK55fraQyTzTX/fxaDtXiEnavv9geOsonh2Fd2RMB+i5cbhu2zMNWJwg==",
"cpu": [
"arm"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-arm64/-/linux-arm64-0.25.0.tgz",
"integrity": "sha512-9QAQjTWNDM/Vk2bgBl17yWuZxZNQIF0OUUuPZRKoDtqF2k4EtYbpyiG5/Dk7nqeK6kIJWPYldkOcBqjXjrUlmg==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ia32/-/linux-ia32-0.25.0.tgz",
"integrity": "sha512-43ET5bHbphBegyeqLb7I1eYn2P/JYGNmzzdidq/w0T8E2SsYL1U6un2NFROFRg1JZLTzdCoRomg8Rvf9M6W6Gg==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-loong64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-loong64/-/linux-loong64-0.25.0.tgz",
"integrity": "sha512-fC95c/xyNFueMhClxJmeRIj2yrSMdDfmqJnyOY4ZqsALkDrrKJfIg5NTMSzVBr5YW1jf+l7/cndBfP3MSDpoHw==",
"cpu": [
"loong64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-mips64el": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-mips64el/-/linux-mips64el-0.25.0.tgz",
"integrity": "sha512-nkAMFju7KDW73T1DdH7glcyIptm95a7Le8irTQNO/qtkoyypZAnjchQgooFUDQhNAy4iu08N79W4T4pMBwhPwQ==",
"cpu": [
"mips64el"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-ppc64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-ppc64/-/linux-ppc64-0.25.0.tgz",
"integrity": "sha512-NhyOejdhRGS8Iwv+KKR2zTq2PpysF9XqY+Zk77vQHqNbo/PwZCzB5/h7VGuREZm1fixhs4Q/qWRSi5zmAiO4Fw==",
"cpu": [
"ppc64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-riscv64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-riscv64/-/linux-riscv64-0.25.0.tgz",
"integrity": "sha512-5S/rbP5OY+GHLC5qXp1y/Mx//e92L1YDqkiBbO9TQOvuFXM+iDqUNG5XopAnXoRH3FjIUDkeGcY1cgNvnXp/kA==",
"cpu": [
"riscv64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-s390x": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-s390x/-/linux-s390x-0.25.0.tgz",
"integrity": "sha512-XM2BFsEBz0Fw37V0zU4CXfcfuACMrppsMFKdYY2WuTS3yi8O1nFOhil/xhKTmE1nPmVyvQJjJivgDT+xh8pXJA==",
"cpu": [
"s390x"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/linux-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/linux-x64/-/linux-x64-0.25.0.tgz",
"integrity": "sha512-9yl91rHw/cpwMCNytUDxwj2XjFpxML0y9HAOH9pNVQDpQrBxHy01Dx+vaMu0N1CKa/RzBD2hB4u//nfc+Sd3Cw==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"linux"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-arm64/-/netbsd-arm64-0.25.0.tgz",
"integrity": "sha512-RuG4PSMPFfrkH6UwCAqBzauBWTygTvb1nxWasEJooGSJ/NwRw7b2HOwyRTQIU97Hq37l3npXoZGYMy3b3xYvPw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/netbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/netbsd-x64/-/netbsd-x64-0.25.0.tgz",
"integrity": "sha512-jl+qisSB5jk01N5f7sPCsBENCOlPiS/xptD5yxOx2oqQfyourJwIKLRA2yqWdifj3owQZCL2sn6o08dBzZGQzA==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"netbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-arm64/-/openbsd-arm64-0.25.0.tgz",
"integrity": "sha512-21sUNbq2r84YE+SJDfaQRvdgznTD8Xc0oc3p3iW/a1EVWeNj/SdUCbm5U0itZPQYRuRTW20fPMWMpcrciH2EJw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/openbsd-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/openbsd-x64/-/openbsd-x64-0.25.0.tgz",
"integrity": "sha512-2gwwriSMPcCFRlPlKx3zLQhfN/2WjJ2NSlg5TKLQOJdV0mSxIcYNTMhk3H3ulL/cak+Xj0lY1Ym9ysDV1igceg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"openbsd"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/sunos-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/sunos-x64/-/sunos-x64-0.25.0.tgz",
"integrity": "sha512-bxI7ThgLzPrPz484/S9jLlvUAHYMzy6I0XiU1ZMeAEOBcS0VePBFxh1JjTQt3Xiat5b6Oh4x7UC7IwKQKIJRIg==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"sunos"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-arm64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-arm64/-/win32-arm64-0.25.0.tgz",
"integrity": "sha512-ZUAc2YK6JW89xTbXvftxdnYy3m4iHIkDtK3CLce8wg8M2L+YZhIvO1DKpxrd0Yr59AeNNkTiic9YLf6FTtXWMw==",
"cpu": [
"arm64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-ia32": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-ia32/-/win32-ia32-0.25.0.tgz",
"integrity": "sha512-eSNxISBu8XweVEWG31/JzjkIGbGIJN/TrRoiSVZwZ6pkC6VX4Im/WV2cz559/TXLcYbcrDN8JtKgd9DJVIo8GA==",
"cpu": [
"ia32"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@esbuild/win32-x64": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/@esbuild/win32-x64/-/win32-x64-0.25.0.tgz",
"integrity": "sha512-ZENoHJBxA20C2zFzh6AI4fT6RraMzjYw4xKWemRTRmRVtN9c5DcH9r/f2ihEkMjOW5eGgrwCslG/+Y/3bL+DHQ==",
"cpu": [
"x64"
],
"dev": true,
"license": "MIT",
"optional": true,
"os": [
"win32"
],
"engines": {
"node": ">=18"
}
},
"node_modules/@floating-ui/core": { "node_modules/@floating-ui/core": {
"version": "1.6.9", "version": "1.6.9",
"resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz", "resolved": "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz",
@ -125,6 +554,87 @@
"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",
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==" "integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg=="
}, },
"node_modules/@fullcalendar/core": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz",
"integrity": "sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==",
"license": "MIT",
"dependencies": {
"preact": "~10.12.1"
}
},
"node_modules/@fullcalendar/core/node_modules/preact": {
"version": "10.12.1",
"resolved": "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz",
"integrity": "sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==",
"license": "MIT",
"funding": {
"type": "opencollective",
"url": "https://opencollective.com/preact"
}
},
"node_modules/@fullcalendar/daygrid": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz",
"integrity": "sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.15"
}
},
"node_modules/@fullcalendar/interaction": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz",
"integrity": "sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.15"
}
},
"node_modules/@fullcalendar/list": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.15.tgz",
"integrity": "sha512-U1bce04tYDwkFnuVImJSy2XalYIIQr6YusOWRPM/5ivHcJh67Gm8CIMSWpi3KdRSNKFkqBxLPkfZGBMaOcJYug==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.15"
}
},
"node_modules/@fullcalendar/multimonth": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/multimonth/-/multimonth-6.1.15.tgz",
"integrity": "sha512-sEZY6jbOYkeF9TwhUldG+UUVv+hiPlGkS8zZEgPR7ypcjhipyA03c5rPjx7N6huOHqh6lCMH59zlohLooQRlaw==",
"license": "MIT",
"dependencies": {
"@fullcalendar/daygrid": "~6.1.15"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.15"
}
},
"node_modules/@fullcalendar/react": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.15.tgz",
"integrity": "sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w==",
"license": "MIT",
"peerDependencies": {
"@fullcalendar/core": "~6.1.15",
"react": "^16.7.0 || ^17 || ^18 || ^19",
"react-dom": "^16.7.0 || ^17 || ^18 || ^19"
}
},
"node_modules/@fullcalendar/timegrid": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.15.tgz",
"integrity": "sha512-61ORr3A148RtxQ2FNG7JKvacyA/TEVZ7z6I+3E9Oeu3dqTf6M928bFcpehRTIK6zIA6Yifs7BeWHgOE9dFnpbw==",
"license": "MIT",
"dependencies": {
"@fullcalendar/daygrid": "~6.1.15"
},
"peerDependencies": {
"@fullcalendar/core": "~6.1.15"
}
},
"node_modules/@hookform/resolvers": { "node_modules/@hookform/resolvers": {
"version": "3.10.0", "version": "3.10.0",
"resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz", "resolved": "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz",
@ -392,6 +902,78 @@
"node": ">=14" "node": ">=14"
} }
}, },
"node_modules/@prisma/client": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/client/-/client-6.4.1.tgz",
"integrity": "sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==",
"hasInstallScript": true,
"license": "Apache-2.0",
"engines": {
"node": ">=18.18"
},
"peerDependencies": {
"prisma": "*",
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"prisma": {
"optional": true
},
"typescript": {
"optional": true
}
}
},
"node_modules/@prisma/debug": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.1.tgz",
"integrity": "sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/engines": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.1.tgz",
"integrity": "sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.1",
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"@prisma/fetch-engine": "6.4.1",
"@prisma/get-platform": "6.4.1"
}
},
"node_modules/@prisma/engines-version": {
"version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"resolved": "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d.tgz",
"integrity": "sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==",
"devOptional": true,
"license": "Apache-2.0"
},
"node_modules/@prisma/fetch-engine": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.1.tgz",
"integrity": "sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.1",
"@prisma/engines-version": "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d",
"@prisma/get-platform": "6.4.1"
}
},
"node_modules/@prisma/get-platform": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.1.tgz",
"integrity": "sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==",
"devOptional": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/debug": "6.4.1"
}
},
"node_modules/@radix-ui/number": { "node_modules/@radix-ui/number": {
"version": "1.1.0", "version": "1.1.0",
"resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz", "resolved": "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz",
@ -2667,11 +3249,30 @@
"version": "3.0.0", "version": "3.0.0",
"resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.0.0.tgz", "resolved": "https://registry.npmjs.org/date-fns/-/date-fns-3.0.0.tgz",
"integrity": "sha512-xjDz3rNN9jp+Lh3P/4MeY4E5HkaRnEnrJCcrdRZnKdn42gJlIe6hwrrwVXePRwVR2kh1UcMnz00erYBnHF8PFA==", "integrity": "sha512-xjDz3rNN9jp+Lh3P/4MeY4E5HkaRnEnrJCcrdRZnKdn42gJlIe6hwrrwVXePRwVR2kh1UcMnz00erYBnHF8PFA==",
"license": "MIT",
"funding": { "funding": {
"type": "github", "type": "github",
"url": "https://github.com/sponsors/kossnocorp" "url": "https://github.com/sponsors/kossnocorp"
} }
}, },
"node_modules/debug": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz",
"integrity": "sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"ms": "^2.1.3"
},
"engines": {
"node": ">=6.0"
},
"peerDependenciesMeta": {
"supports-color": {
"optional": true
}
}
},
"node_modules/decimal.js-light": { "node_modules/decimal.js-light": {
"version": "2.5.1", "version": "2.5.1",
"resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz", "resolved": "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz",
@ -2741,6 +3342,60 @@
"resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz",
"integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==" "integrity": "sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg=="
}, },
"node_modules/esbuild": {
"version": "0.25.0",
"resolved": "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz",
"integrity": "sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==",
"devOptional": true,
"hasInstallScript": true,
"license": "MIT",
"bin": {
"esbuild": "bin/esbuild"
},
"engines": {
"node": ">=18"
},
"optionalDependencies": {
"@esbuild/aix-ppc64": "0.25.0",
"@esbuild/android-arm": "0.25.0",
"@esbuild/android-arm64": "0.25.0",
"@esbuild/android-x64": "0.25.0",
"@esbuild/darwin-arm64": "0.25.0",
"@esbuild/darwin-x64": "0.25.0",
"@esbuild/freebsd-arm64": "0.25.0",
"@esbuild/freebsd-x64": "0.25.0",
"@esbuild/linux-arm": "0.25.0",
"@esbuild/linux-arm64": "0.25.0",
"@esbuild/linux-ia32": "0.25.0",
"@esbuild/linux-loong64": "0.25.0",
"@esbuild/linux-mips64el": "0.25.0",
"@esbuild/linux-ppc64": "0.25.0",
"@esbuild/linux-riscv64": "0.25.0",
"@esbuild/linux-s390x": "0.25.0",
"@esbuild/linux-x64": "0.25.0",
"@esbuild/netbsd-arm64": "0.25.0",
"@esbuild/netbsd-x64": "0.25.0",
"@esbuild/openbsd-arm64": "0.25.0",
"@esbuild/openbsd-x64": "0.25.0",
"@esbuild/sunos-x64": "0.25.0",
"@esbuild/win32-arm64": "0.25.0",
"@esbuild/win32-ia32": "0.25.0",
"@esbuild/win32-x64": "0.25.0"
}
},
"node_modules/esbuild-register": {
"version": "3.6.0",
"resolved": "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz",
"integrity": "sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==",
"devOptional": true,
"license": "MIT",
"dependencies": {
"debug": "^4.3.4"
},
"peerDependencies": {
"esbuild": ">=0.12 <1"
}
},
"node_modules/escalade": { "node_modules/escalade": {
"version": "3.2.0", "version": "3.2.0",
"resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz", "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz",
@ -2847,6 +3502,20 @@
"node": "^8.16.0 || ^10.6.0 || >=11.0.0" "node": "^8.16.0 || ^10.6.0 || >=11.0.0"
} }
}, },
"node_modules/fullcalendar": {
"version": "6.1.15",
"resolved": "https://registry.npmjs.org/fullcalendar/-/fullcalendar-6.1.15.tgz",
"integrity": "sha512-CFnh1yswjRh9puJVDk8VGwTlyZ6eXxr4qLI7QCA0+bozyAm+BluP1US5mOtgk0gEq23nQxGSNDoBvAraz++saQ==",
"license": "MIT",
"dependencies": {
"@fullcalendar/core": "~6.1.15",
"@fullcalendar/daygrid": "~6.1.15",
"@fullcalendar/interaction": "~6.1.15",
"@fullcalendar/list": "~6.1.15",
"@fullcalendar/multimonth": "~6.1.15",
"@fullcalendar/timegrid": "~6.1.15"
}
},
"node_modules/function-bind": { "node_modules/function-bind": {
"version": "1.1.2", "version": "1.1.2",
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz",
@ -3114,6 +3783,13 @@
"node": ">=16 || 14 >=14.17" "node": ">=16 || 14 >=14.17"
} }
}, },
"node_modules/ms": {
"version": "2.1.3",
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==",
"devOptional": true,
"license": "MIT"
},
"node_modules/mz": { "node_modules/mz": {
"version": "2.7.0", "version": "2.7.0",
"resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz", "resolved": "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz",
@ -3576,6 +4252,36 @@
"integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==", "integrity": "sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==",
"license": "MIT" "license": "MIT"
}, },
"node_modules/prisma": {
"version": "6.4.1",
"resolved": "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz",
"integrity": "sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==",
"devOptional": true,
"hasInstallScript": true,
"license": "Apache-2.0",
"dependencies": {
"@prisma/engines": "6.4.1",
"esbuild": ">=0.12 <1",
"esbuild-register": "3.6.0"
},
"bin": {
"prisma": "build/index.js"
},
"engines": {
"node": ">=18.18"
},
"optionalDependencies": {
"fsevents": "2.3.3"
},
"peerDependencies": {
"typescript": ">=5.1.0"
},
"peerDependenciesMeta": {
"typescript": {
"optional": true
}
}
},
"node_modules/prop-types": { "node_modules/prop-types": {
"version": "15.8.1", "version": "15.8.1",
"resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz", "resolved": "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz",
@ -4189,7 +4895,7 @@
"version": "5.7.3", "version": "5.7.3",
"resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz", "resolved": "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz",
"integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==", "integrity": "sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==",
"dev": true, "devOptional": true,
"bin": { "bin": {
"tsc": "bin/tsc", "tsc": "bin/tsc",
"tsserver": "bin/tsserver" "tsserver": "bin/tsserver"

View File

@ -9,7 +9,9 @@
"lint": "next lint" "lint": "next lint"
}, },
"dependencies": { "dependencies": {
"@fullcalendar/react": "^6.1.15",
"@hookform/resolvers": "^3.9.1", "@hookform/resolvers": "^3.9.1",
"@prisma/client": "^6.4.1",
"@radix-ui/react-accordion": "^1.2.2", "@radix-ui/react-accordion": "^1.2.2",
"@radix-ui/react-alert-dialog": "^1.1.4", "@radix-ui/react-alert-dialog": "^1.1.4",
"@radix-ui/react-aspect-ratio": "^1.1.1", "@radix-ui/react-aspect-ratio": "^1.1.1",
@ -41,8 +43,9 @@
"class-variance-authority": "^0.7.1", "class-variance-authority": "^0.7.1",
"clsx": "^2.1.1", "clsx": "^2.1.1",
"cmdk": "1.0.4", "cmdk": "1.0.4",
"date-fns": "3.0.0", "date-fns": "^3.0.0",
"embla-carousel-react": "8.5.1", "embla-carousel-react": "8.5.1",
"fullcalendar": "^6.1.15",
"input-otp": "1.4.1", "input-otp": "1.4.1",
"lucide-react": "^0.454.0", "lucide-react": "^0.454.0",
"next": "14.2.24", "next": "14.2.24",
@ -66,6 +69,7 @@
"@types/react": "^18", "@types/react": "^18",
"@types/react-dom": "^18", "@types/react-dom": "^18",
"postcss": "^8", "postcss": "^8",
"prisma": "^6.4.1",
"tailwindcss": "^3.4.17", "tailwindcss": "^3.4.17",
"typescript": "^5" "typescript": "^5"
} }

View File

@ -0,0 +1,39 @@
generator client {
provider = "prisma-client-js"
}
datasource db {
provider = "postgresql"
//TODO: Modifier l'url
url = "postgresql://enkun:183d9ad665c9257703c2e0703f111d240266a56b33e10df04fb8c565e55e0b94@172.16.32.141:5432/enkun?schema=public"
}
model Calendar {
id String @id @default(cuid())
name String
color String @default("#0082c9")
description String?
userId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
events Event[]
@@index([userId])
}
model Event {
id String @id @default(cuid())
title String
description String?
start DateTime
end DateTime
location String?
isAllDay Boolean @default(false)
calendar Calendar @relation(fields: [calendarId], references: [id], onDelete: Cascade)
calendarId String
createdAt DateTime @default(now())
updatedAt DateTime @updatedAt
@@index([calendarId])
@@index([start, end])
}

23
front/types/calendar.d.ts vendored Normal file
View File

@ -0,0 +1,23 @@
// front/types/calendar.d.ts
export interface Calendar {
id: string;
name: string;
color: string;
description?: string;
userId: string;
createdAt: string;
updatedAt: string;
}
export interface CalendarEvent {
id: string;
title: string;
description?: string;
start: string;
end: string;
location?: string;
isAllDay: boolean;
calendarId: string;
createdAt: string;
updatedAt: string;
}

View File

@ -14,6 +14,11 @@
dependencies: dependencies:
regenerator-runtime "^0.14.0" regenerator-runtime "^0.14.0"
"@esbuild/darwin-x64@0.25.0":
version "0.25.0"
resolved "https://registry.npmjs.org/@esbuild/darwin-x64/-/darwin-x64-0.25.0.tgz"
integrity sha512-DgDaYsPWFTS4S3nWpFcMn/33ZZwAAeAFKNHNa1QN0rI4pUjgqf0f7ONmXf6d22tqTY+H9FNdgeaAa+YIFUn2Rg==
"@floating-ui/core@^1.6.0": "@floating-ui/core@^1.6.0":
version "1.6.9" version "1.6.9"
resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz" resolved "https://registry.npmjs.org/@floating-ui/core/-/core-1.6.9.tgz"
@ -41,6 +46,47 @@
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"
integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg== integrity sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==
"@fullcalendar/core@~6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/core/-/core-6.1.15.tgz"
integrity sha512-BuX7o6ALpLb84cMw1FCB9/cSgF4JbVO894cjJZ6kP74jzbUZNjtwffwRdA+Id8rrLjT30d/7TrkW90k4zbXB5Q==
dependencies:
preact "~10.12.1"
"@fullcalendar/daygrid@~6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/daygrid/-/daygrid-6.1.15.tgz"
integrity sha512-j8tL0HhfiVsdtOCLfzK2J0RtSkiad3BYYemwQKq512cx6btz6ZZ2RNc/hVnIxluuWFyvx5sXZwoeTJsFSFTEFA==
"@fullcalendar/interaction@~6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/interaction/-/interaction-6.1.15.tgz"
integrity sha512-DOTSkofizM7QItjgu7W68TvKKvN9PSEEvDJceyMbQDvlXHa7pm/WAVtAc6xSDZ9xmB1QramYoWGLHkCYbTW1rQ==
"@fullcalendar/list@~6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/list/-/list-6.1.15.tgz"
integrity sha512-U1bce04tYDwkFnuVImJSy2XalYIIQr6YusOWRPM/5ivHcJh67Gm8CIMSWpi3KdRSNKFkqBxLPkfZGBMaOcJYug==
"@fullcalendar/multimonth@~6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/multimonth/-/multimonth-6.1.15.tgz"
integrity sha512-sEZY6jbOYkeF9TwhUldG+UUVv+hiPlGkS8zZEgPR7ypcjhipyA03c5rPjx7N6huOHqh6lCMH59zlohLooQRlaw==
dependencies:
"@fullcalendar/daygrid" "~6.1.15"
"@fullcalendar/react@^6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/react/-/react-6.1.15.tgz"
integrity sha512-L0b9hybS2J4e7lq6G2CD4nqriyLEqOH1tE8iI6JQjAMTVh5JicOo5Mqw+fhU5bJ7hLfMw2K3fksxX3Ul1ssw5w==
"@fullcalendar/timegrid@~6.1.15":
version "6.1.15"
resolved "https://registry.npmjs.org/@fullcalendar/timegrid/-/timegrid-6.1.15.tgz"
integrity sha512-61ORr3A148RtxQ2FNG7JKvacyA/TEVZ7z6I+3E9Oeu3dqTf6M928bFcpehRTIK6zIA6Yifs7BeWHgOE9dFnpbw==
dependencies:
"@fullcalendar/daygrid" "~6.1.15"
"@hookform/resolvers@^3.9.1": "@hookform/resolvers@^3.9.1":
version "3.10.0" version "3.10.0"
resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz" resolved "https://registry.npmjs.org/@hookform/resolvers/-/resolvers-3.10.0.tgz"
@ -131,6 +177,47 @@
resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz" resolved "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz"
integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg== integrity sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==
"@prisma/client@^6.4.1":
version "6.4.1"
resolved "https://registry.npmjs.org/@prisma/client/-/client-6.4.1.tgz"
integrity sha512-A7Mwx44+GVZVexT5e2GF/WcKkEkNNKbgr059xpr5mn+oUm2ZW1svhe+0TRNBwCdzhfIZ+q23jEgsNPvKD9u+6g==
"@prisma/debug@6.4.1":
version "6.4.1"
resolved "https://registry.npmjs.org/@prisma/debug/-/debug-6.4.1.tgz"
integrity sha512-Q9xk6yjEGIThjSD8zZegxd5tBRNHYd13GOIG0nLsanbTXATiPXCLyvlYEfvbR2ft6dlRsziQXfQGxAgv7zcMUA==
"@prisma/engines-version@6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d":
version "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d"
resolved "https://registry.npmjs.org/@prisma/engines-version/-/engines-version-6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d.tgz"
integrity sha512-Xq54qw55vaCGrGgIJqyDwOq0TtjZPJEWsbQAHugk99hpDf2jcEeQhUcF+yzEsSqegBaDNLA4IC8Nn34sXmkiTQ==
"@prisma/engines@6.4.1":
version "6.4.1"
resolved "https://registry.npmjs.org/@prisma/engines/-/engines-6.4.1.tgz"
integrity sha512-KldENzMHtKYwsOSLThghOIdXOBEsfDuGSrxAZjMnimBiDKd3AE4JQ+Kv+gBD/x77WoV9xIPf25GXMWffXZ17BA==
dependencies:
"@prisma/debug" "6.4.1"
"@prisma/engines-version" "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d"
"@prisma/fetch-engine" "6.4.1"
"@prisma/get-platform" "6.4.1"
"@prisma/fetch-engine@6.4.1":
version "6.4.1"
resolved "https://registry.npmjs.org/@prisma/fetch-engine/-/fetch-engine-6.4.1.tgz"
integrity sha512-uZ5hVeTmDspx7KcaRCNoXmcReOD+84nwlO2oFvQPRQh9xiFYnnUKDz7l9bLxp8t4+25CsaNlgrgilXKSQwrIGQ==
dependencies:
"@prisma/debug" "6.4.1"
"@prisma/engines-version" "6.4.0-29.a9055b89e58b4b5bfb59600785423b1db3d0e75d"
"@prisma/get-platform" "6.4.1"
"@prisma/get-platform@6.4.1":
version "6.4.1"
resolved "https://registry.npmjs.org/@prisma/get-platform/-/get-platform-6.4.1.tgz"
integrity sha512-gXqZaDI5scDkBF8oza7fOD3Q3QMD0e0rBynlzDDZdTWbWmzjuW58PRZtj+jkvKje2+ZigCWkH8SsWZAsH6q1Yw==
dependencies:
"@prisma/debug" "6.4.1"
"@radix-ui/number@1.1.0": "@radix-ui/number@1.1.0":
version "1.1.0" version "1.1.0"
resolved "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz" resolved "https://registry.npmjs.org/@radix-ui/number/-/number-1.1.0.tgz"
@ -1183,11 +1270,18 @@ d3-timer@^3.0.1:
resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz" resolved "https://registry.npmjs.org/d3-timer/-/d3-timer-3.0.1.tgz"
integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA== integrity sha512-ndfJ/JxxMd3nw31uyKoY2naivF+r29V+Lc0svZxe1JvvIRmi8hUsrMvdOwgS1o6uBHmiz91geQ0ylPP0aj1VUA==
"date-fns@^2.28.0 || ^3.0.0", date-fns@3.0.0: "date-fns@^2.28.0 || ^3.0.0", date-fns@^3.0.0:
version "3.0.0" version "3.0.0"
resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.0.0.tgz" resolved "https://registry.npmjs.org/date-fns/-/date-fns-3.0.0.tgz"
integrity sha512-xjDz3rNN9jp+Lh3P/4MeY4E5HkaRnEnrJCcrdRZnKdn42gJlIe6hwrrwVXePRwVR2kh1UcMnz00erYBnHF8PFA== integrity sha512-xjDz3rNN9jp+Lh3P/4MeY4E5HkaRnEnrJCcrdRZnKdn42gJlIe6hwrrwVXePRwVR2kh1UcMnz00erYBnHF8PFA==
debug@^4.3.4:
version "4.4.0"
resolved "https://registry.npmjs.org/debug/-/debug-4.4.0.tgz"
integrity sha512-6WTZ/IxCY/T6BALoZHaE4ctp9xm+Z5kY/pzYaCHRFeyVhojxlrm+46y68HA6hr0TcwEssoxNiDEUJQjfPZ/RYA==
dependencies:
ms "^2.1.3"
decimal.js-light@^2.4.1: decimal.js-light@^2.4.1:
version "2.5.1" version "2.5.1"
resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz" resolved "https://registry.npmjs.org/decimal.js-light/-/decimal.js-light-2.5.1.tgz"
@ -1254,6 +1348,44 @@ emoji-regex@^9.2.2:
resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz" resolved "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz"
integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg== integrity sha512-L18DaJsXSUk2+42pv8mLs5jJT2hqFkFE4j21wOmgbUqsZ2hL72NsUU785g9RXgo3s0ZNgVl42TiHp3ZtOv/Vyg==
esbuild-register@3.6.0:
version "3.6.0"
resolved "https://registry.npmjs.org/esbuild-register/-/esbuild-register-3.6.0.tgz"
integrity sha512-H2/S7Pm8a9CL1uhp9OvjwrBh5Pvx0H8qVOxNu8Wed9Y7qv56MPtq+GGM8RJpq6glYJn9Wspr8uw7l55uyinNeg==
dependencies:
debug "^4.3.4"
"esbuild@>=0.12 <1":
version "0.25.0"
resolved "https://registry.npmjs.org/esbuild/-/esbuild-0.25.0.tgz"
integrity sha512-BXq5mqc8ltbaN34cDqWuYKyNhX8D/Z0J1xdtdQ8UcIIIyJyz+ZMKUt58tF3SrZ85jcfN/PZYhjR5uDQAYNVbuw==
optionalDependencies:
"@esbuild/aix-ppc64" "0.25.0"
"@esbuild/android-arm" "0.25.0"
"@esbuild/android-arm64" "0.25.0"
"@esbuild/android-x64" "0.25.0"
"@esbuild/darwin-arm64" "0.25.0"
"@esbuild/darwin-x64" "0.25.0"
"@esbuild/freebsd-arm64" "0.25.0"
"@esbuild/freebsd-x64" "0.25.0"
"@esbuild/linux-arm" "0.25.0"
"@esbuild/linux-arm64" "0.25.0"
"@esbuild/linux-ia32" "0.25.0"
"@esbuild/linux-loong64" "0.25.0"
"@esbuild/linux-mips64el" "0.25.0"
"@esbuild/linux-ppc64" "0.25.0"
"@esbuild/linux-riscv64" "0.25.0"
"@esbuild/linux-s390x" "0.25.0"
"@esbuild/linux-x64" "0.25.0"
"@esbuild/netbsd-arm64" "0.25.0"
"@esbuild/netbsd-x64" "0.25.0"
"@esbuild/openbsd-arm64" "0.25.0"
"@esbuild/openbsd-x64" "0.25.0"
"@esbuild/sunos-x64" "0.25.0"
"@esbuild/win32-arm64" "0.25.0"
"@esbuild/win32-ia32" "0.25.0"
"@esbuild/win32-x64" "0.25.0"
escalade@^3.2.0: escalade@^3.2.0:
version "3.2.0" version "3.2.0"
resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz" resolved "https://registry.npmjs.org/escalade/-/escalade-3.2.0.tgz"
@ -1307,11 +1439,23 @@ fraction.js@^4.3.7:
resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz" resolved "https://registry.npmjs.org/fraction.js/-/fraction.js-4.3.7.tgz"
integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew== integrity sha512-ZsDfxO51wGAXREY55a7la9LScWpwv9RxIrYABrlvOFBlH/ShPnrtsXeuUIfXKKOVicNxQ+o8JTbJvjS4M89yew==
fsevents@~2.3.2: fsevents@~2.3.2, fsevents@2.3.3:
version "2.3.3" version "2.3.3"
resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz" resolved "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz"
integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw== integrity sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==
fullcalendar@^6.1.15:
version "6.1.15"
resolved "https://registry.npmjs.org/fullcalendar/-/fullcalendar-6.1.15.tgz"
integrity sha512-CFnh1yswjRh9puJVDk8VGwTlyZ6eXxr4qLI7QCA0+bozyAm+BluP1US5mOtgk0gEq23nQxGSNDoBvAraz++saQ==
dependencies:
"@fullcalendar/core" "~6.1.15"
"@fullcalendar/daygrid" "~6.1.15"
"@fullcalendar/interaction" "~6.1.15"
"@fullcalendar/list" "~6.1.15"
"@fullcalendar/multimonth" "~6.1.15"
"@fullcalendar/timegrid" "~6.1.15"
function-bind@^1.1.2: function-bind@^1.1.2:
version "1.1.2" version "1.1.2"
resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz" resolved "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz"
@ -1506,6 +1650,11 @@ minimatch@^9.0.4:
resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz" resolved "https://registry.npmjs.org/minipass/-/minipass-7.1.2.tgz"
integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw== integrity sha512-qOOzS1cBTWYF4BH8fVePDBOO9iptMnGUEZwNc/cMWnTV2nVLZ7VoNWEPHkYczZA0pdoA7dl6e7FL659nX9S2aw==
ms@^2.1.3:
version "2.1.3"
resolved "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz"
integrity sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==
mz@^2.7.0: mz@^2.7.0:
version "2.7.0" version "2.7.0"
resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz" resolved "https://registry.npmjs.org/mz/-/mz-2.7.0.tgz"
@ -1730,11 +1879,27 @@ preact@^10.6.3, preact@>=10:
resolved "https://registry.npmjs.org/preact/-/preact-10.26.2.tgz" resolved "https://registry.npmjs.org/preact/-/preact-10.26.2.tgz"
integrity sha512-0gNmv4qpS9HaN3+40CLBAnKe0ZfyE4ZWo5xKlC1rVrr0ckkEvJvAQqKaHANdFKsGstoxrY4AItZ7kZSGVoVjgg== integrity sha512-0gNmv4qpS9HaN3+40CLBAnKe0ZfyE4ZWo5xKlC1rVrr0ckkEvJvAQqKaHANdFKsGstoxrY4AItZ7kZSGVoVjgg==
preact@~10.12.1:
version "10.12.1"
resolved "https://registry.npmjs.org/preact/-/preact-10.12.1.tgz"
integrity sha512-l8386ixSsBdbreOAkqtrwqHwdvR35ID8c3rKPa8lCWuO86dBi32QWHV4vfsZK1utLLFMvw+Z5Ad4XLkZzchscg==
pretty-format@^3.8.0: pretty-format@^3.8.0:
version "3.8.0" version "3.8.0"
resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz" resolved "https://registry.npmjs.org/pretty-format/-/pretty-format-3.8.0.tgz"
integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew== integrity sha512-WuxUnVtlWL1OfZFQFuqvnvs6MiAGk9UNsBostyBOB0Is9wb5uRESevA6rnl/rkksXaGX3GzZhPup5d6Vp1nFew==
prisma@*, prisma@^6.4.1:
version "6.4.1"
resolved "https://registry.npmjs.org/prisma/-/prisma-6.4.1.tgz"
integrity sha512-q2uJkgXnua/jj66mk6P9bX/zgYJFI/jn4Yp0aS6SPRrjH/n6VyOV7RDe1vHD0DX8Aanx4MvgmUPPoYnR6MJnPg==
dependencies:
"@prisma/engines" "6.4.1"
esbuild ">=0.12 <1"
esbuild-register "3.6.0"
optionalDependencies:
fsevents "2.3.3"
prop-types@^15.6.2, prop-types@^15.8.1: prop-types@^15.6.2, prop-types@^15.8.1:
version "15.8.1" version "15.8.1"
resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz" resolved "https://registry.npmjs.org/prop-types/-/prop-types-15.8.1.tgz"
@ -1754,7 +1919,7 @@ react-day-picker@8.10.1:
resolved "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz" resolved "https://registry.npmjs.org/react-day-picker/-/react-day-picker-8.10.1.tgz"
integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA== integrity sha512-TMx7fNbhLk15eqcMt+7Z7S2KF7mfTId/XJDjKE8f+IUcFn0l08/kI4FiYTL/0yuOLmEcbR4Fwe3GJf/NiiMnPA==
"react-dom@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^17.0.2 || ^18 || ^19", react-dom@^18, "react-dom@^18 || ^19 || ^19.0.0-rc", "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8.0: "react-dom@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.7.0 || ^17 || ^18 || ^19", "react-dom@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react-dom@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react-dom@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react-dom@^17.0.2 || ^18 || ^19", react-dom@^18, "react-dom@^18 || ^19 || ^19.0.0-rc", "react-dom@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react-dom@^18.2.0, react-dom@>=16.6.0, react-dom@>=16.8.0:
version "18.3.1" version "18.3.1"
resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz" resolved "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz"
integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw== integrity sha512-5m4nQKp+rZRb09LNH59GM4BxTh9251/ylbKIbpe7TpGxfJ+9kv6BLkLBXIjjspbgbnIBNqlI23tRnTWT0snUIw==
@ -1833,7 +1998,7 @@ react-transition-group@^4.4.5:
loose-envify "^1.4.0" loose-envify "^1.4.0"
prop-types "^15.6.2" prop-types "^15.6.2"
react@*, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^17.0.2 || ^18 || ^19", react@^18, "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18.2.0, react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.6.0, react@>=16.8.0: react@*, "react@^16.0.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.14.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.5.1 || ^17.0.0 || ^18.0.0 || ^19.0.0-rc", "react@^16.7.0 || ^17 || ^18 || ^19", "react@^16.8 || ^17 || ^18 || ^19 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0", "react@^16.8 || ^17.0 || ^18.0 || ^19.0 || ^19.0.0-rc", "react@^16.8 || ^17.0 || ^18.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17 || ^18 || ^19", "react@^16.8.0 || ^17.0.0 || ^18.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0", "react@^16.8.0 || ^17.0.0 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^16.8.0 || ^17.0.1 || ^18.0.0 || ^19.0.0 || ^19.0.0-rc", "react@^17.0.2 || ^18 || ^19", react@^18, "react@^18 || ^19 || ^19.0.0-rc", "react@^18.0.0 || ^19.0.0 || ^19.0.0-rc", react@^18.2.0, react@^18.3.1, "react@>= 16.8.0 || 17.x.x || ^18.0.0-0", react@>=16.6.0, react@>=16.8.0:
version "18.3.1" version "18.3.1"
resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz" resolved "https://registry.npmjs.org/react/-/react-18.3.1.tgz"
integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ== integrity sha512-wS+hAgJShR0KhEvPJArfuPVN1+Hz1t0Y6n5jLrGQbkb4urgPE/0Rve+1kMB1v/oWgHgm4WIcV+i7F2pTVj+2iQ==
@ -2087,7 +2252,7 @@ tslib@^2.0.0, tslib@^2.1.0, tslib@^2.4.0:
resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz" resolved "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz"
integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w== integrity sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==
typescript@^5: typescript@^5, typescript@>=5.1.0:
version "5.7.3" version "5.7.3"
resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz" resolved "https://registry.npmjs.org/typescript/-/typescript-5.7.3.tgz"
integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw== integrity sha512-84MVSjMEHP+FQRPy3pX9sTVV/INIex71s9TL2Gm5FG/WG1SqXeKyZ0k7/blY/4FdOzI12CBy1vGc4og/eus0fw==