From 1c851a14e89829bc13fb94fc7d0bc2ba1c1db7a7 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Tue, 21 May 2024 16:42:19 +0100 Subject: [PATCH 01/79] auth using token/jwt login + logout --- src/app/auth/login/page.jsx | 40 +--------------- src/app/auth/verif/page.jsx | 6 +-- src/app/layout.js | 6 ++- src/app/lib/dal.js | 12 +++-- src/app/lib/fetchRequest.js | 8 ++-- src/app/lib/isAuthenticated.js | 14 ++++-- src/app/lib/isAuthenticatedSSR.js | 21 +++++++++ src/app/lib/session.js | 3 +- src/app/ui/Header.jsx | 76 +++++++++++++++++++++++++++++++ src/app/ui/LogoutButton.js | 19 ++++++++ src/middleware.js | 23 ++++------ 11 files changed, 157 insertions(+), 71 deletions(-) create mode 100644 src/app/lib/isAuthenticatedSSR.js create mode 100644 src/app/ui/Header.jsx create mode 100644 src/app/ui/LogoutButton.js diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index 33b3f09..29be228 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -18,10 +18,6 @@ const LoginPage = () => { // Add your form submission logic here }; - const secretKey = process.env.NEXT_PUBLIC_SESSION_SECRET; - console.log(secretKey); - - // send login request to the server const login = async (event) => { event.preventDefault(); try { @@ -37,43 +33,9 @@ const LoginPage = () => { setMessages(data.error); } else { setMessages('Login successful'); - // Set the cookie - const expiresAt = new Date(new Date().getTime() + 60 * 60 * 1000); // Set cookie to expire in 1 hour - // const session = json strigify it - Cookies.set('session', data.token, { - expires: expiresAt, - secure: true, - sameSite: 'Lax', - }); - // Redirect to the dashboard - // window.location.href = '/auth/verif'; - } - } catch (error) { - setMessages('An error occurred'); - } - }; - const loginn = async (event) => { - event.preventDefault(); - try { - const response = await fetch('http://localhost:8000/login/', { - method: 'POST', - headers: { - 'Content-Type': 'application/json', - }, - body: JSON.stringify({ username, password }), - }); - const data = await response.json(); - if (data.error) { - setMessages(data.error); - } else { - setMessages('Login successful'); - // Set the cookie - const expiresAt = new Date(new Date().getTime() + 60 * 60 * 1000); // Set cookie to expire in 1 hour - // const session = json strigify it await createSession(data); - // Redirect to the dashboard - // window.location.href = '/auth/verif'; + window.location.href = '/auth/verif'; } } catch (error) { setMessages('An error occurred'); diff --git a/src/app/auth/verif/page.jsx b/src/app/auth/verif/page.jsx index cfea4df..79eec0f 100644 --- a/src/app/auth/verif/page.jsx +++ b/src/app/auth/verif/page.jsx @@ -20,18 +20,18 @@ const Verif = () => { } }; const isAuth = async () => { - isAuthenticated(); + await isAuthenticated(); } return ( - <> +
user is redirected to this page after successful login
{/*test fetchExampleData */} - +
); } export default Verif; \ No newline at end of file diff --git a/src/app/layout.js b/src/app/layout.js index 9aef1df..3cf1c50 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -1,5 +1,6 @@ import { Inter } from "next/font/google"; import "./globals.css"; +import Header from "@/app/ui/Header"; const inter = Inter({ subsets: ["latin"] }); @@ -11,7 +12,10 @@ export const metadata = { export default function RootLayout({ children }) { return ( - {children} + +
+ {children} + ); } diff --git a/src/app/lib/dal.js b/src/app/lib/dal.js index 74c6293..67dfeaf 100644 --- a/src/app/lib/dal.js +++ b/src/app/lib/dal.js @@ -1,16 +1,18 @@ -import 'server-only' +// import 'server-only' +"use client"; import Cookies from 'js-cookie'; import { decrypt } from '@/app/lib/session' import {redirect} from "next/navigation"; -export const verifySession = cache(async () => { +export const verifySession = async () => { const cookie = Cookies.get('session').value const session = await decrypt(cookie) + console.log('session from dal', session) if (!session.token) { - redirect('/login') + redirect('/auth/login') } - return { isAuth: true, userName: session.username } -}) \ No newline at end of file + return { isAuth: true, sessionData: session.sessionData } +} \ No newline at end of file diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index 3afb1c3..b04097d 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -4,13 +4,13 @@ import Cookies from 'js-cookie'; import {decrypt} from "@/app/lib/session"; const fetchRequest = async (url, options = {}) => { - const token = Cookies.get('session'); - console.log('token', token) - const jwtDecrypted = decrypt(token); + const jwtCookie = Cookies.get('session'); + console.log('jwtCookie', jwtCookie) + const jwtDecrypted = await decrypt(jwtCookie); console.log('jwtDecrypted', jwtDecrypted) const headers = { ...options.headers, - 'Authorization': `Token ${token}`, + 'Authorization': `Token ${jwtDecrypted.sessionData.token}`, 'Content-Type': 'application/json', }; diff --git a/src/app/lib/isAuthenticated.js b/src/app/lib/isAuthenticated.js index 0a880ac..7c77a5e 100644 --- a/src/app/lib/isAuthenticated.js +++ b/src/app/lib/isAuthenticated.js @@ -1,16 +1,20 @@ //verify if the user is authenticated import Cookies from "js-cookie"; +import {decrypt} from "@/app/lib/session"; -export default function isAuthenticated() { +export default async function isAuthenticated() { if (!Cookies.get('session')) { console.log('user is not authenticated'); // redirect to auth/login page - window.location.href = '/auth/login'; - + // window.location.href = '/auth/login'; return false; } - else console.log('user is authenticated'); - return true; + + console.log('user is authenticated'); + const session = Cookies.get('session') + const cookieDecoded = await decrypt(session) + return { isAuth: true, sessionData: cookieDecoded.sessionData } + } diff --git a/src/app/lib/isAuthenticatedSSR.js b/src/app/lib/isAuthenticatedSSR.js new file mode 100644 index 0000000..09a8fd7 --- /dev/null +++ b/src/app/lib/isAuthenticatedSSR.js @@ -0,0 +1,21 @@ +//verify if the user is authenticated + +import { cookies } from 'next/headers' + +import {decrypt} from "@/app/lib/session"; + +export default async function isAuthenticatedSSR() { + if (!cookies().get('session')) { + console.log('user is not authenticated'); + // redirect to auth/login page + // window.location.href = '/auth/login'; + return false; + } + + console.log('user is authenticated'); + const cookie = cookies().get('session')?.value + const session = await decrypt(cookie) + return { isAuth: true, sessionData: session.sessionData } + +} + diff --git a/src/app/lib/session.js b/src/app/lib/session.js index 850ed74..1eb509a 100644 --- a/src/app/lib/session.js +++ b/src/app/lib/session.js @@ -2,7 +2,8 @@ import Cookies from 'js-cookie'; import { SignJWT, jwtVerify } from 'jose' -const secretKey = "password1234" +const secretKey = process.env.NEXT_PUBLIC_SESSION_SECRET; +console.log(secretKey); const encodedKey = new TextEncoder().encode(secretKey) export async function encrypt(payload) { diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx new file mode 100644 index 0000000..e85b42e --- /dev/null +++ b/src/app/ui/Header.jsx @@ -0,0 +1,76 @@ +// 'use client'; + +// import { useState, useEffect } from 'react'; +import Link from 'next/link'; +// import isAuthenticated from "@/app/lib/isAuthenticated"; +import isAuthenticatedSSR from "@/app/lib/isAuthenticatedSSR"; +import LogoutButton from "@/app/ui/LogoutButton"; +// import fetchRequest from "@/app/lib/fetchRequest"; +// import Cookies from "js-cookie"; + +const Header = async () => { + + const {isAuth, sessionData} = await isAuthenticatedSSR() + console.log('isAuth', isAuth) + console.log('sessionData', sessionData) + // const [loggedInData, setLoggedInData] = useState(); + // + // + // useEffect(() => { + // isAuthenticated().then((data) => { + // console.log('data from headerv1', data) + // if (data.isAuth) { + // setLoggedInData(data.sessionData) + // console.log('data from header', loggedInData) + // } + // }) + // }, []); + + + // const logout = async () => { + // console.log('logout') + // await fetchRequest('http://localhost:8000/logout', {method: 'GET' }); + // Cookies.remove('session'); + // setLoggedInData(null); + // // reload the page + // window.location.href = '/'; + // } + return ( +
+
+ +

TeamBook

+ +
+ +
+ ); +}; + +export default Header; \ No newline at end of file diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.js new file mode 100644 index 0000000..b0aa3fd --- /dev/null +++ b/src/app/ui/LogoutButton.js @@ -0,0 +1,19 @@ +'use client'; + +import Cookies from 'js-cookie'; + +const LogoutButton = () => { + const logout = async () => { + await fetch('http://localhost:8000/logout', { method: 'GET' }); + Cookies.remove('session'); + window.location.href = '/'; + }; + + return ( + + ); +}; + +export default LogoutButton; diff --git a/src/middleware.js b/src/middleware.js index 1308f82..62024a0 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,12 +1,10 @@ -"use client"; - import { NextResponse } from 'next/server' import { decrypt } from '@/app/lib/session' -import Cookies from 'js-cookie'; +import { cookies } from 'next/headers' // 1. Specify protected and public routes -const protectedRoutes = ['/dashboard'] -const publicRoutes = ['/login', '/signup', '/'] +const protectedRoutes = ['/dashboard', '/auth/verif'] +const publicRoutes = ['/auth/login', '/auth/signup'] export default async function middleware(req) { // 2. Check if the current route is protected or public @@ -15,22 +13,21 @@ export default async function middleware(req) { const isPublicRoute = publicRoutes.includes(path) // 3. Decrypt the session from the cookie - const cookie = Cookies.get('session'); - console.log('cookie', cookie) + const cookie = cookies().get('session')?.value const session = await decrypt(cookie) - + console.log('session dfdf', session) // 5. Redirect to /login if the user is not authenticated - if (isProtectedRoute && !session?.token) { - return NextResponse.redirect(new URL('/login', req.nextUrl)) + if (isProtectedRoute && !session?.sessionData.token) { + return NextResponse.redirect(new URL('/auth/login', req.nextUrl)) } // 6. Redirect to /dashboard if the user is authenticated if ( isPublicRoute && - session?.token && - !req.nextUrl.pathname.startsWith('/auth') + session?.sessionData.token && + !req.nextUrl.pathname.startsWith('/auth/verif') ) { - return NextResponse.redirect(new URL('/auth', req.nextUrl)) + return NextResponse.redirect(new URL('/auth/verif', req.nextUrl)) } return NextResponse.next() -- GitLab From 7ff7ae2ec30b73a0aac738a935f93f3b8ae82817 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Wed, 22 May 2024 13:20:13 +0100 Subject: [PATCH 02/79] add forgot password & change password UI --- next.config.mjs | 6 +- src/app/auth/change-password/page.jsx | 120 +++++++++++++++++++++ src/app/auth/forgot-password/page.jsx | 98 ++++++++++++++++++ src/app/auth/login/page.jsx | 12 +-- src/app/globals.css | 40 ++----- src/app/lib/constants.js | 1 + src/app/lib/fetchRequest.js | 17 +-- src/app/page.js | 143 +------------------------- src/components/Loader/Loader.css | 46 +++++++++ src/components/Loader/Loader.jsx | 13 +++ 10 files changed, 312 insertions(+), 184 deletions(-) create mode 100644 src/app/auth/change-password/page.jsx create mode 100644 src/app/auth/forgot-password/page.jsx create mode 100644 src/app/lib/constants.js create mode 100644 src/components/Loader/Loader.css create mode 100644 src/components/Loader/Loader.jsx diff --git a/next.config.mjs b/next.config.mjs index 4678774..c9a3c0c 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -1,4 +1,8 @@ /** @type {import('next').NextConfig} */ -const nextConfig = {}; +const nextConfig = { + images: { + domains: ['https://res.cloudinary.com'], + } +}; export default nextConfig; diff --git a/src/app/auth/change-password/page.jsx b/src/app/auth/change-password/page.jsx new file mode 100644 index 0000000..be4882e --- /dev/null +++ b/src/app/auth/change-password/page.jsx @@ -0,0 +1,120 @@ +"use client" +import fetchRequest from '@/app/lib/fetchRequest' +import Image from 'next/image' +import Link from 'next/link' +import React, { useEffect, useState } from 'react' +import { useSearchParams } from 'next/navigation' +import Loader from '@/components/Loader/Loader' +import { passwordRegex } from '@/app/lib/constants' + +const ChangePassword = () => { + const [isSuccess, setIsSuccess] = useState(false) + const [formErrors, setFormErrors] = useState([]) + const [password, setPassword] = useState("") + const [confirmPassword, setConfirmPassword] = useState("") + const [isLoading, setIsLoading] = useState(false) + const params = useSearchParams(); + const handleChangePassword = async (event) => { + event.preventDefault() + setIsLoading(true) + const { isSuccess, data, errors } = await fetchRequest(`/password_reset/confirm/?token=${params.get("token")}`, { + method: "POST", + body: JSON.stringify({ + password, + token: params.get("token") + }) + }) + if (isSuccess) { + setIsSuccess(true) + } + else { + console.log(errors) + setIsLoading(false) + if (errors.type === "Validation Error") { + if (errors.detail.token) { + setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est invalide"]) + } + else { + setFormErrors(["Le Mot de passe est invalide"]) + } + } else if (errors?.detail?.startsWith("The OTP password entered is not valid")) { + setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est déja utilisé"]) + } + else { + console.log("Internal Server Error") + } + } + } + const isEmptyFields = !password && !confirmPassword + useEffect(() => { + var currentErrors = [] + if (password !== confirmPassword) { + currentErrors.push("Les mots de passe ne sont pas identiques.") + } + if (password.length < 8) { + currentErrors.push("Le mot de passe doit comporter au moins 8 caractères.") + } + if (!passwordRegex.test(password)) { + currentErrors.push("Le mot de passe doit contenir au moins une lettre, un chiffre et un caractère spécial.") + } + setFormErrors(currentErrors) + }, [password, confirmPassword]) + return ( +
+
+
+
+ teamwill +
+ {(!isSuccess) &&
+

Change your password. +

+
+
    +
  • Le mot de passe doit contenir au moins 8 caractères
  • +
  • Le mot de passe doit contenir au moins un chiffre
  • +
  • Le mot de passe doit contenir au moins un caractère spécial
  • +
+
+
+
+ + setPassword(event.target.value)} type="password" name="new_password1" id="new_password1" + className="rounded-md px-3 w-full duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" /> +
+
+ + setConfirmPassword(event.target.value)} type="password" name="new_password2" id="new_password2" + className="rounded-md px-3 duration-150 delay-75 w-full focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" /> +
+
    0 && !isEmptyFields ? "bg-red-100 border border-red-300" : ""} min-h-10 px-3 text-xs py-3 rounded relative mt-9 mb-6 list-inside list-disc`} role="alert"> + {!isEmptyFields && formErrors.map((error, index) => { + return
  • {error}
  • + })} +
+
+ +
+
+
} + {(isSuccess) && ( +
+

The password has been changed!

+ log in again? +
+ )} +
+
+
+ ) +} + +export default ChangePassword \ No newline at end of file diff --git a/src/app/auth/forgot-password/page.jsx b/src/app/auth/forgot-password/page.jsx new file mode 100644 index 0000000..3895740 --- /dev/null +++ b/src/app/auth/forgot-password/page.jsx @@ -0,0 +1,98 @@ +'use client' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import Image from 'next/image' +import Link from 'next/link' +import React, { useState } from 'react' + +const ForgotPassword = () => { + const [email, setEmail] = useState("") + const [isSuccess, setIsSuccess] = useState(false) + const [isLoading, setIsLoading] = useState(false) + const [requestErrors, setRequestErrors] = useState([]) + const handleForgetPassword = async (event) => { + event.preventDefault() + setIsLoading(true) + const { isSuccess, errors, data } = await fetchRequest("/password_reset/", { + body: JSON.stringify({ email }), + method: "POST" + }) + if (isSuccess) { + setIsSuccess(true) + } + else { + setIsLoading(false) + if (errors.type === "Validation Error") { + // setRequestErrors(Object.keys(errors.detail).reduce((prev, current) => { + // return [...prev, ...errors.detail[current]] + // }, [])) + setRequestErrors(["Nous n'avons pas pu trouver de compte associé à cet e-mail. Veuillez essayer une autre adresse e-mail."]) + } else { + console.log("Internal server error") + } + } + } + return ( +
+
+
+ teamwill + {(!isSuccess) &&
+
+

Forgot your password!! No Problem + Reset it here +

+
+
+
+
+
+ setEmail(e.target.value)} autocomplete="off" id="email" name="email" type="text" + className="peer placeholder-transparent h-10 w-full border-b-2 border-gray-300 text-gray-900 focus:outline-none focus:borer-rose-600" + placeholder="Email address" /> + +
+ + + {(requestErrors.length !== 0) && } +
+ Back to log + in +
+ +
+
+
+
} + {(isSuccess) &&
+ + + +
+

Email Sent!

+

Check your email and open the link we sent to continue

+
+
} +
+
+
+ ) +} + +export default ForgotPassword \ No newline at end of file diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index 33b3f09..d9dd76c 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -3,7 +3,7 @@ import { useState } from 'react'; import Link from 'next/link'; import Cookies from 'js-cookie'; -import {createSession} from "@/app/lib/session"; +import { createSession } from "@/app/lib/session"; const LoginPage = () => { const [username, setUsername] = useState(''); @@ -91,7 +91,7 @@ const LoginPage = () => {
{
{ />
- -

Mot de passe oublié?

+ +

Mot de passe oublié?

{messages && ( @@ -124,7 +124,7 @@ const LoginPage = () => { )} diff --git a/src/app/globals.css b/src/app/globals.css index 6c139df..b6ba7d3 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -2,29 +2,6 @@ @tailwind components; @tailwind utilities; -:root { - --foreground-rgb: 0, 0, 0; - --background-start-rgb: 214, 219, 220; - --background-end-rgb: 255, 255, 255; -} - -@media (prefers-color-scheme: dark) { - :root { - --foreground-rgb: 255, 255, 255; - --background-start-rgb: 0, 0, 0; - --background-end-rgb: 0, 0, 0; - } -} - -body { - color: rgb(var(--foreground-rgb)); - background: linear-gradient( - to bottom, - transparent, - rgb(var(--background-end-rgb)) - ) - rgb(var(--background-start-rgb)); -} @layer utilities { .text-balance { @@ -34,18 +11,23 @@ body { /* Customize the scrollbar */ ::-webkit-scrollbar { - width: 6px; /* Set the width of the scrollbar */ + width: 6px; + /* Set the width of the scrollbar */ } ::-webkit-scrollbar-thumb { - background-color: rgba(0, 0, 0, 0.3); /* Color of the thumb */ - border-radius: 3px; /* Rounded corners of the thumb */ + background-color: rgba(0, 0, 0, 0.3); + /* Color of the thumb */ + border-radius: 3px; + /* Rounded corners of the thumb */ } ::-webkit-scrollbar-thumb:hover { - background-color: rgba(0, 0, 0, 0.5); /* Color of the thumb on hover */ + background-color: rgba(0, 0, 0, 0.5); + /* Color of the thumb on hover */ } ::-webkit-scrollbar-track { - background-color: rgba(0, 0, 0, 0.1); /* Color of the track */ -} + background-color: rgba(0, 0, 0, 0.1); + /* Color of the track */ +} \ No newline at end of file diff --git a/src/app/lib/constants.js b/src/app/lib/constants.js new file mode 100644 index 0000000..01b3d8f --- /dev/null +++ b/src/app/lib/constants.js @@ -0,0 +1 @@ +export const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; \ No newline at end of file diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index 3afb1c3..722c921 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -1,8 +1,8 @@ //custom fetch tha have the token in header import Cookies from 'js-cookie'; -import {decrypt} from "@/app/lib/session"; - +import { decrypt } from "@/app/lib/session"; +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" const fetchRequest = async (url, options = {}) => { const token = Cookies.get('session'); console.log('token', token) @@ -14,16 +14,21 @@ const fetchRequest = async (url, options = {}) => { 'Content-Type': 'application/json', }; - const response = await fetch(url, { + const response = await fetch(`${BASE_URL}${url}`, { ...options, headers, }); if (!response.ok) { - throw new Error('Network response was not ok'); + try { + const errorData = await response.json(); + return { isSuccess: false, errors: errorData, data: null } + } catch (error) { + return { isSuccess: false, errors: error, data: null } + } } - - return response.json(); + const data = await response.json() + return { isSuccess: true, errors: null, data: data }; }; export default fetchRequest; diff --git a/src/app/page.js b/src/app/page.js index ecfca6f..87ce7aa 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,149 +1,8 @@ -import Image from "next/image"; export default function Home() { return (
-
-

- Get started by editing  - src/app/page.js -

- -
- -
- Next.js Logo -
- -
- -

- Docs{" "} - - -> - -

-

- Find in-depth information about Next.js features and API. -

-
- - -

- Learn{" "} - - -> - -

-

- Learn about Next.js in an interactive course with quizzes! -

-
- - -

- Templates{" "} - - -> - -

-

- Explore starter templates for Next.js. -

-
- - -

- Deploy{" "} - - -> - -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-

- Instantly deploy your Next.js site to a shareable URL with Vercel. -

-
-
+ Home Page
); } diff --git a/src/components/Loader/Loader.css b/src/components/Loader/Loader.css new file mode 100644 index 0000000..1e1ca65 --- /dev/null +++ b/src/components/Loader/Loader.css @@ -0,0 +1,46 @@ +@keyframes ldio-gcsicpsikdq { + 0% { + transform: rotate(0deg); + } + + 100% { + transform: rotate(360deg); + } +} + +.ldio-gcsicpsikdq div { + width: 50px; + height: 50px; + border-top-color: transparent; + border: 5px solid transparent; + border-radius: 50%; +} + +.ldio-gcsicpsikdq div { + animation: ldio-gcsicpsikdq 1s linear infinite; +} + +.loadingio-spinner-rolling-daoiuzlm498 { + width: min-content; + height: 70px; + display: flex; + flex-direction: column; + justify-content: center; + align-items: center; + background: transparent; +} + +.ldio-gcsicpsikdq { + display: flex; + flex-direction: column; + align-items: center; + justify-content: center; + transform: translateZ(0) scale(1); + backface-visibility: hidden; + transform-origin: 0 0; + /* see note above */ +} + +.ldio-gcsicpsikdq div { + box-sizing: content-box; +} \ No newline at end of file diff --git a/src/components/Loader/Loader.jsx b/src/components/Loader/Loader.jsx new file mode 100644 index 0000000..2430f19 --- /dev/null +++ b/src/components/Loader/Loader.jsx @@ -0,0 +1,13 @@ +import React from 'react' +import "./Loader.css" +const Loader = ({ size, border, className, color }) => { + return ( +
+
+
+
+
+ ) +} + +export default Loader \ No newline at end of file -- GitLab From ab38e63869b479441512628b9dec581559d05059 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Wed, 22 May 2024 14:30:04 +0100 Subject: [PATCH 03/79] add forgot password & change password features --- src/app/auth/change-password/page.jsx | 4 ++-- src/app/lib/constants.js | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/app/auth/change-password/page.jsx b/src/app/auth/change-password/page.jsx index be4882e..a89f484 100644 --- a/src/app/auth/change-password/page.jsx +++ b/src/app/auth/change-password/page.jsx @@ -5,7 +5,7 @@ import Link from 'next/link' import React, { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import Loader from '@/components/Loader/Loader' -import { passwordRegex } from '@/app/lib/constants' +import { PASSWORD_REGEX } from '@/app/lib/constants' const ChangePassword = () => { const [isSuccess, setIsSuccess] = useState(false) @@ -54,7 +54,7 @@ const ChangePassword = () => { if (password.length < 8) { currentErrors.push("Le mot de passe doit comporter au moins 8 caractères.") } - if (!passwordRegex.test(password)) { + if (!PASSWORD_REGEX.test(password)) { currentErrors.push("Le mot de passe doit contenir au moins une lettre, un chiffre et un caractère spécial.") } setFormErrors(currentErrors) diff --git a/src/app/lib/constants.js b/src/app/lib/constants.js index 01b3d8f..f4da663 100644 --- a/src/app/lib/constants.js +++ b/src/app/lib/constants.js @@ -1 +1 @@ -export const passwordRegex = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; \ No newline at end of file +export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; \ No newline at end of file -- GitLab From abb29479bf6bfad33f77c4934329a2f8940dec69 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 22 May 2024 14:38:01 +0100 Subject: [PATCH 04/79] fixed login with jwt --- src/app/auth/login/page.jsx | 6 +++--- src/app/auth/verif/page.jsx | 2 +- src/app/ui/Header.jsx | 22 ---------------------- src/app/ui/LogoutButton.js | 13 ++++++++++--- 4 files changed, 14 insertions(+), 29 deletions(-) diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index 29be228..2d6459e 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -21,7 +21,7 @@ const LoginPage = () => { const login = async (event) => { event.preventDefault(); try { - const response = await fetch('http://localhost:8000/login/', { + const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login/`, { method: 'POST', headers: { 'Content-Type': 'application/json', @@ -29,8 +29,8 @@ const LoginPage = () => { body: JSON.stringify({ username, password }), }); const data = await response.json(); - if (data.error) { - setMessages(data.error); + if (data.non_field_errors) { + setMessages(data.non_field_errors[0]); } else { setMessages('Login successful'); diff --git a/src/app/auth/verif/page.jsx b/src/app/auth/verif/page.jsx index 79eec0f..b49360b 100644 --- a/src/app/auth/verif/page.jsx +++ b/src/app/auth/verif/page.jsx @@ -5,7 +5,7 @@ const Verif = () => { // fetch data from the server /example const fetchExampleData = async () => { try { - const data = await fetchRequest('http://localhost:8000/example/', + const data = await fetchRequest(`${process.env.NEXT_PUBLIC_API_URL}/example/`, {method: 'POST', body: JSON.stringify({ "name": "jhon", diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx index e85b42e..b9d5df2 100644 --- a/src/app/ui/Header.jsx +++ b/src/app/ui/Header.jsx @@ -13,28 +13,6 @@ const Header = async () => { const {isAuth, sessionData} = await isAuthenticatedSSR() console.log('isAuth', isAuth) console.log('sessionData', sessionData) - // const [loggedInData, setLoggedInData] = useState(); - // - // - // useEffect(() => { - // isAuthenticated().then((data) => { - // console.log('data from headerv1', data) - // if (data.isAuth) { - // setLoggedInData(data.sessionData) - // console.log('data from header', loggedInData) - // } - // }) - // }, []); - - - // const logout = async () => { - // console.log('logout') - // await fetchRequest('http://localhost:8000/logout', {method: 'GET' }); - // Cookies.remove('session'); - // setLoggedInData(null); - // // reload the page - // window.location.href = '/'; - // } return (
diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.js index b0aa3fd..d3a137c 100644 --- a/src/app/ui/LogoutButton.js +++ b/src/app/ui/LogoutButton.js @@ -1,12 +1,19 @@ 'use client'; import Cookies from 'js-cookie'; +import fetchRequest from "@/app/lib/fetchRequest"; const LogoutButton = () => { const logout = async () => { - await fetch('http://localhost:8000/logout', { method: 'GET' }); - Cookies.remove('session'); - window.location.href = '/'; + const response = await fetchRequest(`${process.env.NEXT_PUBLIC_API_URL}/logout`, { + method: 'GET'}); + console.log(response); + if (response.statusCode === 200) { + console.log('logout successful'); + Cookies.remove('session'); + window.location.href = '/'; + } + }; return ( -- GitLab From 4608a92a3849b3a3d1e264d233297f2fed0ab819 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 22 May 2024 15:18:14 +0100 Subject: [PATCH 05/79] fixed fetch request /login/logout --- src/app/auth/verif/page.jsx | 2 +- src/app/lib/dal.js | 18 ------------------ src/app/lib/fetchRequest.js | 3 ++- src/app/ui/LogoutButton.js | 4 ++-- 4 files changed, 5 insertions(+), 22 deletions(-) delete mode 100644 src/app/lib/dal.js diff --git a/src/app/auth/verif/page.jsx b/src/app/auth/verif/page.jsx index b49360b..15d3d40 100644 --- a/src/app/auth/verif/page.jsx +++ b/src/app/auth/verif/page.jsx @@ -5,7 +5,7 @@ const Verif = () => { // fetch data from the server /example const fetchExampleData = async () => { try { - const data = await fetchRequest(`${process.env.NEXT_PUBLIC_API_URL}/example/`, + const data = await fetchRequest(`/example/`, {method: 'POST', body: JSON.stringify({ "name": "jhon", diff --git a/src/app/lib/dal.js b/src/app/lib/dal.js deleted file mode 100644 index 67dfeaf..0000000 --- a/src/app/lib/dal.js +++ /dev/null @@ -1,18 +0,0 @@ -// import 'server-only' -"use client"; - -import Cookies from 'js-cookie'; -import { decrypt } from '@/app/lib/session' -import {redirect} from "next/navigation"; - -export const verifySession = async () => { - const cookie = Cookies.get('session').value - const session = await decrypt(cookie) - console.log('session from dal', session) - - if (!session.token) { - redirect('/auth/login') - } - - return { isAuth: true, sessionData: session.sessionData } -} \ No newline at end of file diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index b6f73c2..9effc1e 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -10,7 +10,8 @@ const fetchRequest = async (url, options = {}) => { console.log('jwtDecrypted', jwtDecrypted) const headers = { ...options.headers, - 'Authorization': `Token ${jwtDecrypted.sessionData.token}`, + // add authorization header with token if there is jwtDecrypted.sessionData.token + Authorization: jwtDecrypted?.sessionData.token ? `Bearer ${jwtDecrypted.sessionData.token}` : undefined, 'Content-Type': 'application/json', }; diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.js index d3a137c..b219fc4 100644 --- a/src/app/ui/LogoutButton.js +++ b/src/app/ui/LogoutButton.js @@ -5,10 +5,10 @@ import fetchRequest from "@/app/lib/fetchRequest"; const LogoutButton = () => { const logout = async () => { - const response = await fetchRequest(`${process.env.NEXT_PUBLIC_API_URL}/logout`, { + const response = await fetchRequest(`/logout`, { method: 'GET'}); console.log(response); - if (response.statusCode === 200) { + if (response.isSuccess) { console.log('logout successful'); Cookies.remove('session'); window.location.href = '/'; -- GitLab From c738045759e2c636b6bc531a3c5f6d65537e9753 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Thu, 23 May 2024 09:43:38 +0100 Subject: [PATCH 06/79] added projects front only --- src/app/auth/login/page.jsx | 2 - src/app/lib/fetchRequest.js | 2 +- src/app/lib/fetchRequestServer.js | 41 ++++ src/app/lib/isAuthenticatedSSR.js | 1 - src/app/projects/ProjectForm.jsx | 99 +++++++++ src/app/projects/ProjectList.jsx | 109 ++++++++++ src/app/projects/SideBar.jsx | 334 ++++++++++++++++++++++++++++++ src/app/projects/page.jsx | 59 ++++++ src/middleware.js | 2 +- 9 files changed, 644 insertions(+), 5 deletions(-) create mode 100644 src/app/lib/fetchRequestServer.js create mode 100644 src/app/projects/ProjectForm.jsx create mode 100644 src/app/projects/ProjectList.jsx create mode 100644 src/app/projects/SideBar.jsx create mode 100644 src/app/projects/page.jsx diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index a657afd..e94f196 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -32,8 +32,6 @@ const LoginPage = () => { if (data.non_field_errors) { setMessages(data.non_field_errors[0]); } else { - setMessages('Login successful'); - await createSession(data); window.location.href = '/auth/verif'; } diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index 9effc1e..0a029d7 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -11,7 +11,7 @@ const fetchRequest = async (url, options = {}) => { const headers = { ...options.headers, // add authorization header with token if there is jwtDecrypted.sessionData.token - Authorization: jwtDecrypted?.sessionData.token ? `Bearer ${jwtDecrypted.sessionData.token}` : undefined, + Authorization: jwtDecrypted?.sessionData.token ? `Token ${jwtDecrypted.sessionData.token}` : undefined, 'Content-Type': 'application/json', }; diff --git a/src/app/lib/fetchRequestServer.js b/src/app/lib/fetchRequestServer.js new file mode 100644 index 0000000..ee50e36 --- /dev/null +++ b/src/app/lib/fetchRequestServer.js @@ -0,0 +1,41 @@ +// src/app/lib/fetchRequestServer.js +import { cookies as nextCookies } from 'next/headers'; +import { decrypt } from "@/app/lib/session"; + +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + +const fetchRequestServer = async (url, options = {}) => { + const cookieStore = nextCookies(); + const jwtCookie = cookieStore.get('session')?.value; + + if (!jwtCookie) { + return { isSuccess: false, errors: "No session cookie found", data: null }; + } + + const jwtDecrypted = await decrypt(jwtCookie); + + const headers = { + ...options.headers, + Authorization: jwtDecrypted?.sessionData.token ? `Token ${jwtDecrypted.sessionData.token}` : undefined, + 'Content-Type': 'application/json', + }; + + const response = await fetch(`${BASE_URL}${url}`, { + ...options, + headers, + }); + + if (!response.ok) { + try { + const errorData = await response.json(); + return { isSuccess: false, errors: errorData, data: null }; + } catch (error) { + return { isSuccess: false, errors: error, data: null }; + } + } + + const data = await response.json(); + return { isSuccess: true, errors: null, data: data }; +}; + +export default fetchRequestServer; diff --git a/src/app/lib/isAuthenticatedSSR.js b/src/app/lib/isAuthenticatedSSR.js index 09a8fd7..b068815 100644 --- a/src/app/lib/isAuthenticatedSSR.js +++ b/src/app/lib/isAuthenticatedSSR.js @@ -1,5 +1,4 @@ //verify if the user is authenticated - import { cookies } from 'next/headers' import {decrypt} from "@/app/lib/session"; diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx new file mode 100644 index 0000000..418e9b4 --- /dev/null +++ b/src/app/projects/ProjectForm.jsx @@ -0,0 +1,99 @@ +import { useState, useEffect } from 'react'; + +const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { + const [projectName, setProjectName] = useState(''); + const [clientName, setClientName] = useState(''); + const [emails, setEmails] = useState(['']); + + useEffect(() => { + if (editingProject) { + setProjectName(editingProject.projectName); + setClientName(editingProject.clientName); + setEmails(editingProject.emails); + } else { + setProjectName(''); + setClientName(''); + setEmails(['']); + } + }, [editingProject]); + + const handleEmailChange = (index, value) => { + const newEmails = [...emails]; + newEmails[index] = value; + setEmails(newEmails); + }; + + const handleAddEmail = () => { + setEmails([...emails, '']); + }; + + const handleRemoveEmail = (index) => { + setEmails(emails.filter((_, i) => i !== index)); + }; + + const handleSubmit = (e) => { + e.preventDefault(); + const project = { projectName, clientName, emails }; + + if (editingProject) { + onEditProject(editingProject.id, project); + } else { + onAddProject(project); + } + }; + + return ( +
+
+ + setProjectName(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" + /> +
+
+ + setClientName(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" + /> +
+
+ + {emails.map((email, index) => ( +
+ handleEmailChange(index, e.target.value)} + className="flex-1 px-3 py-2 bg-white border border-gray-300 rounded-md" + /> + +
+ ))} + +
+ +
+ ); +}; + +export default ProjectForm; diff --git a/src/app/projects/ProjectList.jsx b/src/app/projects/ProjectList.jsx new file mode 100644 index 0000000..a939286 --- /dev/null +++ b/src/app/projects/ProjectList.jsx @@ -0,0 +1,109 @@ +const ProjectList = ({ projects, onEdit, onDelete }) => { + return ( + <> + + + + + + + + + + {projects.map((project, index) => ( + + + + + + ))} + +
ProjetMembres de léquipeActions
{project.projectName}{project.emails.length} + + +
+
+
    +
  1. + + Prev Page + + + + +
  2. + +
  3. + + 1 + +
  4. + +
  5. + 2 +
  6. + +
  7. + + 3 + +
  8. + +
  9. + + 4 + +
  10. + +
  11. + + Next Page + + + + +
  12. +
+
+ + ); +}; + +export default ProjectList; diff --git a/src/app/projects/SideBar.jsx b/src/app/projects/SideBar.jsx new file mode 100644 index 0000000..c182af2 --- /dev/null +++ b/src/app/projects/SideBar.jsx @@ -0,0 +1,334 @@ +const SideBar = () => { + return ( +
+
+ + +
+
+ +
+
+
+ +
+
+ +
+
+
+ + ); +} +export default SideBar; \ No newline at end of file diff --git a/src/app/projects/page.jsx b/src/app/projects/page.jsx new file mode 100644 index 0000000..1db5d0a --- /dev/null +++ b/src/app/projects/page.jsx @@ -0,0 +1,59 @@ +'use client'; + +import SideBar from "@/app/projects/SideBar"; +import { useState } from 'react'; +import ProjectForm from "@/app/projects/ProjectForm"; +import ProjectList from "@/app/projects/ProjectList"; +const Projects = () => { + const [projects, setProjects] = useState([]); + const [editingProject, setEditingProject] = useState(null); + + const handleAddProject = (project) => { + project.id = projects.length ? projects[projects.length - 1].id + 1 : 1; + setProjects([...projects, project]); + setEditingProject(null); + }; + + const handleEditProject = (id, updatedProject) => { + const updatedProjects = projects.map((project) => + project.id === id ? { ...project, ...updatedProject } : project + ); + setProjects(updatedProjects); + setEditingProject(null); + }; + + const handleEditClick = (project) => { + setEditingProject(project); + }; + + const handleDeleteProject = (project) => { + setProjects(projects.filter((p) => p.id !== project.id)); + setEditingProject(null); + }; + + return ( +
+
+ +
+ +
+
+

Projets

+ + +
+
+
+ ); +} +export default Projects; + diff --git a/src/middleware.js b/src/middleware.js index 62024a0..9c10ccd 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -15,7 +15,7 @@ export default async function middleware(req) { // 3. Decrypt the session from the cookie const cookie = cookies().get('session')?.value const session = await decrypt(cookie) - console.log('session dfdf', session) + // console.log('session dfdf', session) // 5. Redirect to /login if the user is not authenticated if (isProtectedRoute && !session?.sessionData.token) { return NextResponse.redirect(new URL('/auth/login', req.nextUrl)) -- GitLab From 739d18abc69b4f544ca36280e800b315e4afcb37 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Thu, 23 May 2024 16:58:26 +0100 Subject: [PATCH 07/79] feature: add privilege management UI --- jsconfig.json | 6 +- next.config.mjs | 25 +- package-lock.json | 3050 ++++++++++++++++++++- package.json | 1 + src/app/lib/TypesHelper.js | 3 + src/app/lib/fetchRequest.js | 3 +- src/app/privilege/CreatePrivilegeForm.jsx | 54 + src/app/privilege/PrivilegeTableRow.jsx | 112 + src/app/privilege/page.jsx | 55 + src/static/image/svg/cancel.svg | 1 + src/static/image/svg/check.svg | 1 + src/static/image/svg/delete.svg | 3 + src/static/image/svg/edit.svg | 1 + 13 files changed, 3230 insertions(+), 85 deletions(-) create mode 100644 src/app/lib/TypesHelper.js create mode 100644 src/app/privilege/CreatePrivilegeForm.jsx create mode 100644 src/app/privilege/PrivilegeTableRow.jsx create mode 100644 src/app/privilege/page.jsx create mode 100644 src/static/image/svg/cancel.svg create mode 100644 src/static/image/svg/check.svg create mode 100644 src/static/image/svg/delete.svg create mode 100644 src/static/image/svg/edit.svg diff --git a/jsconfig.json b/jsconfig.json index b8d6842..879f8fc 100644 --- a/jsconfig.json +++ b/jsconfig.json @@ -1,7 +1,9 @@ { "compilerOptions": { "paths": { - "@/*": ["./src/*"] + "@/*": [ + "./src/*" + ] } } -} +} \ No newline at end of file diff --git a/next.config.mjs b/next.config.mjs index c9a3c0c..1a2f07f 100644 --- a/next.config.mjs +++ b/next.config.mjs @@ -2,7 +2,30 @@ const nextConfig = { images: { domains: ['https://res.cloudinary.com'], - } + }, + webpack(config) { + const fileLoaderRule = config.module.rules.find((rule) => + rule.test?.test?.('.svg'), + ) + + config.module.rules.push( + { + ...fileLoaderRule, + test: /\.svg$/i, + resourceQuery: /url/, // *.svg?url + }, + { + test: /\.svg$/i, + issuer: fileLoaderRule.issuer, + resourceQuery: { not: [...fileLoaderRule.resourceQuery.not, /url/] }, // exclude if *.svg?url + use: ['@svgr/webpack'], + }, + ) + + fileLoaderRule.exclude = /\.svg$/i + + return config + }, }; export default nextConfig; diff --git a/package-lock.json b/package-lock.json index d4a9662..617d640 100644 --- a/package-lock.json +++ b/package-lock.json @@ -16,6 +16,7 @@ "react-dom": "^18" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", @@ -33,6 +34,1942 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/@ampproject/remapping": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/@ampproject/remapping/-/remapping-2.3.0.tgz", + "integrity": "sha512-30iZtAPgz+LTIYoeivqYo853f02jBYSd5uGnGpkFV0M3xOt9aN73erkgYAmZU43x4VfqcnLxW9Kpg3R5LC4YYw==", + "dev": true, + "dependencies": { + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.24" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/code-frame": { + "version": "7.24.2", + "resolved": "https://registry.npmjs.org/@babel/code-frame/-/code-frame-7.24.2.tgz", + "integrity": "sha512-y5+tLQyV8pg3fsiln67BVLD1P13Eg4lh5RW9mF0zUuvLrv9uIQ4MCL+CRT+FTsBlBjcIan6PGsLcBN0m3ClUyQ==", + "dev": true, + "dependencies": { + "@babel/highlight": "^7.24.2", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/compat-data": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/compat-data/-/compat-data-7.24.4.tgz", + "integrity": "sha512-vg8Gih2MLK+kOkHJp4gBEIkyaIi00jgWot2D9QOmmfLC8jINSOzmCLta6Bvz/JSBCqnegV0L80jhxkol5GWNfQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/core": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/core/-/core-7.24.5.tgz", + "integrity": "sha512-tVQRucExLQ02Boi4vdPp49svNGcfL2GhdTCT9aldhXgCJVAI21EtRfBettiuLUwce/7r6bFdgs6JFkcdTiFttA==", + "dev": true, + "dependencies": { + "@ampproject/remapping": "^2.2.0", + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-module-transforms": "^7.24.5", + "@babel/helpers": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5", + "convert-source-map": "^2.0.0", + "debug": "^4.1.0", + "gensync": "^1.0.0-beta.2", + "json5": "^2.2.3", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/babel" + } + }, + "node_modules/@babel/core/node_modules/json5": { + "version": "2.2.3", + "resolved": "https://registry.npmjs.org/json5/-/json5-2.2.3.tgz", + "integrity": "sha512-XmOWe7eyHYH14cLdVPoyg+GOH3rYX++KpzrylJwSW98t3Nk+U8XOl8FWKOgwtzdb8lXGf6zYwDUzeHMWfxasyg==", + "dev": true, + "bin": { + "json5": "lib/cli.js" + }, + "engines": { + "node": ">=6" + } + }, + "node_modules/@babel/core/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/generator": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/generator/-/generator-7.24.5.tgz", + "integrity": "sha512-x32i4hEXvr+iI0NEoEfDKzlemF8AmtOP8CcrRaEcpzysWuoEb1KknpcvMsHKPONoKZiDuItklgWhB18xEhr9PA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5", + "@jridgewell/gen-mapping": "^0.3.5", + "@jridgewell/trace-mapping": "^0.3.25", + "jsesc": "^2.5.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-annotate-as-pure": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-annotate-as-pure/-/helper-annotate-as-pure-7.22.5.tgz", + "integrity": "sha512-LvBTxu8bQSQkcyKOU+a1btnNFQ1dMAd0R6PyW3arXes06F6QLWLIrd681bxRPIXlrMGR3XYnW9JyML7dP3qgxg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-builder-binary-assignment-operator-visitor": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-builder-binary-assignment-operator-visitor/-/helper-builder-binary-assignment-operator-visitor-7.22.15.tgz", + "integrity": "sha512-QkBXwGgaoC2GtGZRoma6kv7Szfv06khvhFav67ZExau2RaXzy8MpHSMO2PNoP2XtmQphJQRHFfg77Bq731Yizw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.15" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets": { + "version": "7.23.6", + "resolved": "https://registry.npmjs.org/@babel/helper-compilation-targets/-/helper-compilation-targets-7.23.6.tgz", + "integrity": "sha512-9JB548GZoQVmzrFgp8o7KxdgkTGm6xs9DW0o/Pim72UDjzr5ObUQ6ZzYPqA+g9OTS2bBQoctLJrky0RDCAWRgQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.23.5", + "@babel/helper-validator-option": "^7.23.5", + "browserslist": "^4.22.2", + "lru-cache": "^5.1.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/lru-cache": { + "version": "5.1.1", + "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-5.1.1.tgz", + "integrity": "sha512-KpNARQA3Iwv+jTA0utUVVbrh+Jlrr1Fv0e56GGzAFOXN7dk/FviaDW8LHmK52DlcH4WP2n6gI8vN1aesBFgo9w==", + "dev": true, + "dependencies": { + "yallist": "^3.0.2" + } + }, + "node_modules/@babel/helper-compilation-targets/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-class-features-plugin": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-create-class-features-plugin/-/helper-create-class-features-plugin-7.24.5.tgz", + "integrity": "sha512-uRc4Cv8UQWnE4NXlYTIIdM7wfFkOqlFztcC/gVXDKohKoVB3OyonfelUBaJzSwpBntZ2KYGF/9S7asCHsXwW6g==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-member-expression-to-functions": "^7.24.5", + "@babel/helper-optimise-call-expression": "^7.22.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-class-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin": { + "version": "7.22.15", + "resolved": "https://registry.npmjs.org/@babel/helper-create-regexp-features-plugin/-/helper-create-regexp-features-plugin-7.22.15.tgz", + "integrity": "sha512-29FkPLFjn4TPEa3RE7GpW+qbE8tlsu3jntNYNfcGsc49LphF1PQIiD+vMZ1z1xVOKt+93khA9tc2JBs3kBjA7w==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "regexpu-core": "^5.3.1", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-create-regexp-features-plugin/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/helper-define-polyfill-provider": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/@babel/helper-define-polyfill-provider/-/helper-define-polyfill-provider-0.6.2.tgz", + "integrity": "sha512-LV76g+C502biUK6AyZ3LK10vDpDyCzZnhZFXkH1L75zHPj68+qc8Zfpx2th+gzwA2MzyK+1g/3EPl62yFnVttQ==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.22.6", + "@babel/helper-plugin-utils": "^7.22.5", + "debug": "^4.1.1", + "lodash.debounce": "^4.0.8", + "resolve": "^1.14.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/helper-environment-visitor": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-environment-visitor/-/helper-environment-visitor-7.22.20.tgz", + "integrity": "sha512-zfedSIzFhat/gFhWfHtgWvlec0nqB9YEIVrpuwjruLlXfUSnA8cJB0miHKwqDnQ7d32aKo2xt88/xZptwxbfhA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-function-name": { + "version": "7.23.0", + "resolved": "https://registry.npmjs.org/@babel/helper-function-name/-/helper-function-name-7.23.0.tgz", + "integrity": "sha512-OErEqsrxjZTJciZ4Oo+eoZqeW9UIiOcuYKRJA4ZAgV9myA+pOXhhmpfNCKjEH/auVfEYVFJ6y1Tc4r0eIApqiw==", + "dev": true, + "dependencies": { + "@babel/template": "^7.22.15", + "@babel/types": "^7.23.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-hoist-variables": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-hoist-variables/-/helper-hoist-variables-7.22.5.tgz", + "integrity": "sha512-wGjk9QZVzvknA6yKIUURb8zY3grXCcOZt+/7Wcy8O2uctxhplmUPkOdlgoNhmdVee2c92JXbf1xpMtVNbfoxRw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-member-expression-to-functions": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-member-expression-to-functions/-/helper-member-expression-to-functions-7.24.5.tgz", + "integrity": "sha512-4owRteeihKWKamtqg4JmWSsEZU445xpFRXPEwp44HbgbxdWlUV1b4Agg4lkA806Lil5XM/e+FJyS0vj5T6vmcA==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-imports": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/helper-module-imports/-/helper-module-imports-7.24.3.tgz", + "integrity": "sha512-viKb0F9f2s0BCS22QSF308z/+1YWKV/76mwt61NBzS5izMzDPwdq1pTrzf+Li3npBWX9KdQbkeCt1jSAM7lZqg==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-module-transforms": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-module-transforms/-/helper-module-transforms-7.24.5.tgz", + "integrity": "sha512-9GxeY8c2d2mdQUP1Dye0ks3VDyIMS98kt/llQ2nUId8IsWqTF0l1LkSX0/uP7l7MCDrzXS009Hyhe2gzTiGW8A==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-module-imports": "^7.24.3", + "@babel/helper-simple-access": "^7.24.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/helper-validator-identifier": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-optimise-call-expression": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-optimise-call-expression/-/helper-optimise-call-expression-7.22.5.tgz", + "integrity": "sha512-HBwaojN0xFRx4yIvpwGqxiV2tUfl7401jlok564NgB9EHS1y6QT17FmKWm4ztqjeVdXLuC4fSvHc5ePpQjoTbw==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-plugin-utils": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-plugin-utils/-/helper-plugin-utils-7.24.5.tgz", + "integrity": "sha512-xjNLDopRzW2o6ba0gKbkZq5YWEBaK3PCyTOY1K2P/O07LGMhMqlMXPxwN4S5/RhWuCobT8z0jrlKGlYmeR1OhQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-remap-async-to-generator": { + "version": "7.22.20", + "resolved": "https://registry.npmjs.org/@babel/helper-remap-async-to-generator/-/helper-remap-async-to-generator-7.22.20.tgz", + "integrity": "sha512-pBGyV4uBqOns+0UvhsTO8qgl8hO89PmiDYv+/COyp1aeMcmfrfruz+/nCMFiYyFF/Knn0yfrC85ZzNFjembFTw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-wrap-function": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-replace-supers": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-replace-supers/-/helper-replace-supers-7.24.1.tgz", + "integrity": "sha512-QCR1UqC9BzG5vZl8BMicmZ28RuUBnHhAMddD8yHFHDRH9lLTZ9uUPehX8ctVPT8l0TKblJidqcgUUKGVrePleQ==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-member-expression-to-functions": "^7.23.0", + "@babel/helper-optimise-call-expression": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/helper-simple-access": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-simple-access/-/helper-simple-access-7.24.5.tgz", + "integrity": "sha512-uH3Hmf5q5n7n8mz7arjUlDOCbttY/DW4DYhE6FUsjKJ/oYC1kQQUvwEQWxRwUpX9qQKRXeqLwWxrqilMrf32sQ==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-skip-transparent-expression-wrappers": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/helper-skip-transparent-expression-wrappers/-/helper-skip-transparent-expression-wrappers-7.22.5.tgz", + "integrity": "sha512-tK14r66JZKiC43p8Ki33yLBVJKlQDFoA8GYN67lWCDCqoL6EMMSuM9b+Iff2jHaM/RRFYl7K+iiru7hbRqNx8Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-split-export-declaration": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-split-export-declaration/-/helper-split-export-declaration-7.24.5.tgz", + "integrity": "sha512-5CHncttXohrHk8GWOFCcCl4oRD9fKosWlIRgWm4ql9VYioKm52Mk2xsmoohvm7f3JoiLSM5ZgJuRaf5QZZYd3Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-string-parser": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/helper-string-parser/-/helper-string-parser-7.24.1.tgz", + "integrity": "sha512-2ofRCjnnA9y+wk8b9IAREroeUP02KHp431N2mhKniy2yKIDKpbrHv9eXwm8cBeWQYcJmzv5qKCu65P47eCF7CQ==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-identifier": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-identifier/-/helper-validator-identifier-7.24.5.tgz", + "integrity": "sha512-3q93SSKX2TWCG30M2G2kwaKeTYgEUp5Snjuj8qm729SObL6nbtUldAi37qbxkD5gg3xnBio+f9nqpSepGZMvxA==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-validator-option": { + "version": "7.23.5", + "resolved": "https://registry.npmjs.org/@babel/helper-validator-option/-/helper-validator-option-7.23.5.tgz", + "integrity": "sha512-85ttAOMLsr53VgXkTbkx8oA6YTfT4q7/HzXSLEYmjcSTJPMPQtvq1BD79Byep5xMUYbGRzEpDsjUf3dyp54IKw==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helper-wrap-function": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helper-wrap-function/-/helper-wrap-function-7.24.5.tgz", + "integrity": "sha512-/xxzuNvgRl4/HLNKvnFwdhdgN3cpLxgLROeLDl83Yx0AJ1SGvq1ak0OszTOjDfiB8Vx03eJbeDWh9r+jCCWttw==", + "dev": true, + "dependencies": { + "@babel/helper-function-name": "^7.23.0", + "@babel/template": "^7.24.0", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/helpers": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/helpers/-/helpers-7.24.5.tgz", + "integrity": "sha512-CiQmBMMpMQHwM5m01YnrM6imUG1ebgYJ+fAIW4FZe6m4qHTPaRHti+R8cggAwkdz4oXhtO4/K9JWlh+8hIfR2Q==", + "dev": true, + "dependencies": { + "@babel/template": "^7.24.0", + "@babel/traverse": "^7.24.5", + "@babel/types": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/highlight/-/highlight-7.24.5.tgz", + "integrity": "sha512-8lLmua6AVh/8SLJRRVD6V8p73Hir9w5mJrhE+IPpILG31KKlI9iz5zmBYKcWPS59qSfgP9RaSBQSHHE81WKuEw==", + "dev": true, + "dependencies": { + "@babel/helper-validator-identifier": "^7.24.5", + "chalk": "^2.4.2", + "js-tokens": "^4.0.0", + "picocolors": "^1.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/highlight/node_modules/ansi-styles": { + "version": "3.2.1", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-3.2.1.tgz", + "integrity": "sha512-VT0ZI6kZRdTh8YyJw3SMbYm/u+NqfsAxEpWO0Pf9sq8/e94WxxOpPKx9FR1FlyCtOVDNOQ+8ntlqFxiRc+r5qA==", + "dev": true, + "dependencies": { + "color-convert": "^1.9.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/chalk": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-2.4.2.tgz", + "integrity": "sha512-Mti+f9lpJNcwF4tWV8/OrTTtF1gZi+f8FqlyAdouralcFWFQWF2+NgCHShjkCb+IFBLq9buZwE1xckQU4peSuQ==", + "dev": true, + "dependencies": { + "ansi-styles": "^3.2.1", + "escape-string-regexp": "^1.0.5", + "supports-color": "^5.3.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/color-convert": { + "version": "1.9.3", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-1.9.3.tgz", + "integrity": "sha512-QfAUtd+vFdAtFQcC8CCyYt1fYWxSqAiK2cSD6zDB8N3cpsEBAvRxp9zOGg6G/SHHJYAT88/az/IuDGALsNVbGg==", + "dev": true, + "dependencies": { + "color-name": "1.1.3" + } + }, + "node_modules/@babel/highlight/node_modules/color-name": { + "version": "1.1.3", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.3.tgz", + "integrity": "sha512-72fSenhMw2HZMTVHeCA9KCmpEIbzWiQsjN+BHcBbS9vr1mtt+vJjPdksIBNUmKAW8TFUDPJK5SUU3QhE9NEXDw==", + "dev": true + }, + "node_modules/@babel/highlight/node_modules/escape-string-regexp": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz", + "integrity": "sha512-vbRorB5FUQWvla16U8R/qgaFIya2qGzwDrNmCZuYKrbdSUMG6I1ZCGQRefkRVhuOkIGVne7BQ35DSfo1qvJqFg==", + "dev": true, + "engines": { + "node": ">=0.8.0" + } + }, + "node_modules/@babel/highlight/node_modules/has-flag": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-3.0.0.tgz", + "integrity": "sha512-sKJf1+ceQBr4SMkvQnBDNDtf4TXpVhVGateu0t918bl30FnbE2m4vNLX+VWe/dpjlb+HugGYzW7uQXH98HPEYw==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/highlight/node_modules/supports-color": { + "version": "5.5.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-5.5.0.tgz", + "integrity": "sha512-QjVjwdXIt408MIiAqCX4oUKsgU2EqAGzs2Ppkm4aQYbjm+ZEWEcW4SfFNTr4uMNZma0ey4f5lgLrkB0aX0QMow==", + "dev": true, + "dependencies": { + "has-flag": "^3.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/parser": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/parser/-/parser-7.24.5.tgz", + "integrity": "sha512-EOv5IK8arwh3LI47dz1b0tKUb/1uhHAnHJOrjgtQMIpu1uXd9mlFrJg9IUgGUgZ41Ch0K8REPTYpO7B76b4vJg==", + "dev": true, + "bin": { + "parser": "bin/babel-parser.js" + }, + "engines": { + "node": ">=6.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-firefox-class-in-computed-class-key": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-firefox-class-in-computed-class-key/-/plugin-bugfix-firefox-class-in-computed-class-key-7.24.5.tgz", + "integrity": "sha512-LdXRi1wEMTrHVR4Zc9F8OewC3vdm5h4QB6L71zy6StmYeqGi1b3ttIO8UC+BfZKcH9jdr4aI249rBkm+3+YvHw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression/-/plugin-bugfix-safari-id-destructuring-collision-in-function-expression-7.24.1.tgz", + "integrity": "sha512-y4HqEnkelJIOQGd+3g1bTeKsA5c6qM7eOn7VggGVbBc0y8MLSKHacwcIE2PplNlQSj0PqS9rrXL/nkPVK+kUNg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining/-/plugin-bugfix-v8-spread-parameters-in-optional-chaining-7.24.1.tgz", + "integrity": "sha512-Hj791Ii4ci8HqnaKHAlLNs+zaLXb0EzSDhiAWp5VNlyvCNymYfacs64pxTxbH1znW/NcArSmwpmG9IKE/TUVVQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-transform-optional-chaining": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.13.0" + } + }, + "node_modules/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly/-/plugin-bugfix-v8-static-class-fields-redefine-readonly-7.24.1.tgz", + "integrity": "sha512-m9m/fXsXLiHfwdgydIFnpk+7jlVbnvlK5B2EKiPdLUb6WX654ZaaEWJUjk8TftRbZpK0XibovlLWX4KIZhV6jw==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-proposal-private-property-in-object": { + "version": "7.21.0-placeholder-for-preset-env.2", + "resolved": "https://registry.npmjs.org/@babel/plugin-proposal-private-property-in-object/-/plugin-proposal-private-property-in-object-7.21.0-placeholder-for-preset-env.2.tgz", + "integrity": "sha512-SOSkfJDddaM7mak6cPEpswyTRnuRltl429hMraQEglW+OkovnCzsiszTmsrlY//qLFjCpQDFRvjdm2wA5pPm9w==", + "dev": true, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-async-generators": { + "version": "7.8.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-async-generators/-/plugin-syntax-async-generators-7.8.4.tgz", + "integrity": "sha512-tycmZxkGfZaxhMRbXlPXuVFpdWlXpir2W4AMhSJgRKzk/eDlIXOhb2LHWoLpDF7TEHylV5zNhykX6KAgHJmTNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-properties": { + "version": "7.12.13", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-properties/-/plugin-syntax-class-properties-7.12.13.tgz", + "integrity": "sha512-fm4idjKla0YahUNgFNLCB0qySdsoPiZP3iQE3rky0mBUtMZ23yDJ9SJdg6dXTSDnulOVqiF3Hgr9nbXvXTQZYA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.12.13" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-class-static-block": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-class-static-block/-/plugin-syntax-class-static-block-7.14.5.tgz", + "integrity": "sha512-b+YyPmr6ldyNnM6sqYeMWE+bgJcJpO6yS4QD7ymxgH34GBPNDM/THBh8iunyvKIZztiwLH4CJZ0RxTk9emgpjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-dynamic-import": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-dynamic-import/-/plugin-syntax-dynamic-import-7.8.3.tgz", + "integrity": "sha512-5gdGbFon+PszYzqs83S3E5mpi7/y/8M9eC90MRTZfduQOYW76ig6SOSPNe41IG5LoP3FGBn2N0RjVDSQiS94kQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-export-namespace-from": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-export-namespace-from/-/plugin-syntax-export-namespace-from-7.8.3.tgz", + "integrity": "sha512-MXf5laXo6c1IbEbegDmzGPwGNTsHZmEy6QGznu5Sh2UCWvueywb2ee+CCE4zQiZstxU9BMoQO9i6zUFSY0Kj0Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.3" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-assertions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-assertions/-/plugin-syntax-import-assertions-7.24.1.tgz", + "integrity": "sha512-IuwnI5XnuF189t91XbxmXeCDz3qs6iDRO7GJ++wcfgeXNs/8FmIlKcpDSXNVyuLQxlwvskmI3Ct73wUODkJBlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-attributes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-attributes/-/plugin-syntax-import-attributes-7.24.1.tgz", + "integrity": "sha512-zhQTMH0X2nVLnb04tz+s7AMuasX8U0FnpE+nHTOhSOINjWMnopoZTxtIKsd45n4GQ/HIZLyfIpoul8e2m0DnRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-import-meta": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-import-meta/-/plugin-syntax-import-meta-7.10.4.tgz", + "integrity": "sha512-Yqfm+XDx0+Prh3VSeEQCPU81yC+JWZ2pDPFSS4ZdpfZhp4MkFMaDC1UqseovEKwSUpnIL7+vK+Clp7bfh0iD7g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-json-strings": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-json-strings/-/plugin-syntax-json-strings-7.8.3.tgz", + "integrity": "sha512-lY6kdGpWHvjoe2vk4WrAapEuBR69EMxZl+RoGRhrFGNYVK8mOPAW8VfbT/ZgrFbXlDNiiaxQnAtgVCZ6jv30EA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-jsx": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-jsx/-/plugin-syntax-jsx-7.24.1.tgz", + "integrity": "sha512-2eCtxZXf+kbkMIsXS4poTvT4Yu5rXiRa+9xGVT56raghjmBTKMpFNc9R4IDiB4emao9eO22Ox7CxuJG7BgExqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-logical-assignment-operators": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-logical-assignment-operators/-/plugin-syntax-logical-assignment-operators-7.10.4.tgz", + "integrity": "sha512-d8waShlpFDinQ5MtvGU9xDAOzKH47+FFoney2baFIoMr952hKOLp1HR7VszoZvOsV/4+RRszNY7D17ba0te0ig==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-nullish-coalescing-operator": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-nullish-coalescing-operator/-/plugin-syntax-nullish-coalescing-operator-7.8.3.tgz", + "integrity": "sha512-aSff4zPII1u2QD7y+F8oDsz19ew4IGEJg9SVW+bqwpwtfFleiQDMdzA/R+UlWDzfnHFCxxleFT0PMIrR36XLNQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-numeric-separator": { + "version": "7.10.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-numeric-separator/-/plugin-syntax-numeric-separator-7.10.4.tgz", + "integrity": "sha512-9H6YdfkcK/uOnY/K7/aA2xpzaAgkQn37yzWUMRK7OaPOqOpGS1+n0H5hxT9AUw9EsSjPW8SVyMJwYRtWs3X3ug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.10.4" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-object-rest-spread": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-object-rest-spread/-/plugin-syntax-object-rest-spread-7.8.3.tgz", + "integrity": "sha512-XoqMijGZb9y3y2XskN+P1wUGiVwWZ5JmoDRwx5+3GmEplNyVM2s2Dg8ILFQm8rWM48orGy5YpI5Bl8U1y7ydlA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-catch-binding": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-catch-binding/-/plugin-syntax-optional-catch-binding-7.8.3.tgz", + "integrity": "sha512-6VPD0Pc1lpTqw0aKoeRTMiB+kWhAoT24PA+ksWSBrFtl5SIRVpZlwN3NNPQjehA2E/91FV3RjLWoVTglWcSV3Q==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-optional-chaining": { + "version": "7.8.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-optional-chaining/-/plugin-syntax-optional-chaining-7.8.3.tgz", + "integrity": "sha512-KoK9ErH1MBlCPxV0VANkXW2/dw4vlbGDrFgz8bmUsBGYkFRcbRwMh6cIJubdPrkxRwuGdtCk0v/wPTKbQgBjkg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.8.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-private-property-in-object": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-private-property-in-object/-/plugin-syntax-private-property-in-object-7.14.5.tgz", + "integrity": "sha512-0wVnp9dxJ72ZUJDV27ZfbSj6iHLoytYZmh3rFcxNnvsJF3ktkzLDZPy/mA17HGsaQT3/DQsWYX1f1QGWkCoVUg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-top-level-await": { + "version": "7.14.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-top-level-await/-/plugin-syntax-top-level-await-7.14.5.tgz", + "integrity": "sha512-hx++upLv5U1rgYfwe1xBQUhRmU41NEvpUvrp8jkrSCdvGSnM5/qdRMtylJ6PG5OFkBaHkbTAKTnd3/YyESRHFw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-typescript/-/plugin-syntax-typescript-7.24.1.tgz", + "integrity": "sha512-Yhnmvy5HZEnHUty6i++gcfH1/l68AHnItFHnaCv6hn9dNh0hQvvQJsxpi4BMBFN5DLeHBuucT/0DgzXif/OyRw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-syntax-unicode-sets-regex": { + "version": "7.18.6", + "resolved": "https://registry.npmjs.org/@babel/plugin-syntax-unicode-sets-regex/-/plugin-syntax-unicode-sets-regex-7.18.6.tgz", + "integrity": "sha512-727YkEAPwSIQTv5im8QHz3upqp92JTWhidIC81Tdx4VJYIte/VndKf1qKrfnnhPLiPghStWfvC/iFaMCQu7Nqg==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.18.6", + "@babel/helper-plugin-utils": "^7.18.6" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-arrow-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-arrow-functions/-/plugin-transform-arrow-functions-7.24.1.tgz", + "integrity": "sha512-ngT/3NkRhsaep9ck9uj2Xhv9+xB1zShY3tM3g6om4xxCELwCDN4g4Aq5dRn48+0hasAql7s2hdBOysCfNpr4fw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-generator-functions": { + "version": "7.24.3", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-generator-functions/-/plugin-transform-async-generator-functions-7.24.3.tgz", + "integrity": "sha512-Qe26CMYVjpQxJ8zxM1340JFNjZaF+ISWpr1Kt/jGo+ZTUzKkfw/pphEWbRCb+lmSM6k/TOgfYLvmbHkUQ0asIg==", + "dev": true, + "dependencies": { + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-remap-async-to-generator": "^7.22.20", + "@babel/plugin-syntax-async-generators": "^7.8.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-async-to-generator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-async-to-generator/-/plugin-transform-async-to-generator-7.24.1.tgz", + "integrity": "sha512-AawPptitRXp1y0n4ilKcGbRYWfbbzFWz2NqNu7dacYDtFtz0CMjG64b3LQsb3KIgnf4/obcUL78hfaOS7iCUfw==", + "dev": true, + "dependencies": { + "@babel/helper-module-imports": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-remap-async-to-generator": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoped-functions": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoped-functions/-/plugin-transform-block-scoped-functions-7.24.1.tgz", + "integrity": "sha512-TWWC18OShZutrv9C6mye1xwtam+uNi2bnTOCBUd5sZxyHOiWbU6ztSROofIMrK84uweEZC219POICK/sTYwfgg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-block-scoping": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-block-scoping/-/plugin-transform-block-scoping-7.24.5.tgz", + "integrity": "sha512-sMfBc3OxghjC95BkYrYocHL3NaOplrcaunblzwXhGmlPwpmfsxr4vK+mBBt49r+S240vahmv+kUxkeKgs+haCw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-properties/-/plugin-transform-class-properties-7.24.1.tgz", + "integrity": "sha512-OMLCXi0NqvJfORTaPQBwqLXHhb93wkBKZ4aNwMl6WtehO7ar+cmp+89iPEQPqxAnxsOKTaMcs3POz3rKayJ72g==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-class-static-block": { + "version": "7.24.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-class-static-block/-/plugin-transform-class-static-block-7.24.4.tgz", + "integrity": "sha512-B8q7Pz870Hz/q9UgP8InNpY01CSLDSCyqX7zcRuv3FcPl87A2G17lASroHWaCtbdIcbYzOZ7kWmXFKbijMSmFg==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.4", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-class-static-block": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.12.0" + } + }, + "node_modules/@babel/plugin-transform-classes": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-classes/-/plugin-transform-classes-7.24.5.tgz", + "integrity": "sha512-gWkLP25DFj2dwe9Ck8uwMOpko4YsqyfZJrOmqqcegeDYEbp7rmn4U6UQZNj08UF6MaX39XenSpKRCvpDRBtZ7Q==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-replace-supers": "^7.24.1", + "@babel/helper-split-export-declaration": "^7.24.5", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-classes/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/plugin-transform-computed-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-computed-properties/-/plugin-transform-computed-properties-7.24.1.tgz", + "integrity": "sha512-5pJGVIUfJpOS+pAqBQd+QMaTD2vCL/HcePooON6pDpHgRp4gNRmzyHTPIkXntwKsq3ayUFVfJaIKPw2pOkOcTw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/template": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-destructuring": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-destructuring/-/plugin-transform-destructuring-7.24.5.tgz", + "integrity": "sha512-SZuuLyfxvsm+Ah57I/i1HVjveBENYK9ue8MJ7qkc7ndoNjqquJiElzA7f5yaAXjyW2hKojosOTAQQRX50bPSVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dotall-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dotall-regex/-/plugin-transform-dotall-regex-7.24.1.tgz", + "integrity": "sha512-p7uUxgSoZwZ2lPNMzUkqCts3xlp8n+o05ikjy7gbtFJSt9gdU88jAmtfmOxHM14noQXBxfgzf2yRWECiNVhTCw==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-duplicate-keys": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-duplicate-keys/-/plugin-transform-duplicate-keys-7.24.1.tgz", + "integrity": "sha512-msyzuUnvsjsaSaocV6L7ErfNsa5nDWL1XKNnDePLgmz+WdU4w/J8+AxBMrWfi9m4IxfL5sZQKUPQKDQeeAT6lA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-dynamic-import": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-dynamic-import/-/plugin-transform-dynamic-import-7.24.1.tgz", + "integrity": "sha512-av2gdSTyXcJVdI+8aFZsCAtR29xJt0S5tas+Ef8NvBNmD1a+N/3ecMLeMBgfcK+xzsjdLDT6oHt+DFPyeqUbDA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-dynamic-import": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-exponentiation-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-exponentiation-operator/-/plugin-transform-exponentiation-operator-7.24.1.tgz", + "integrity": "sha512-U1yX13dVBSwS23DEAqU+Z/PkwE9/m7QQy8Y9/+Tdb8UWYaGNDYwTLi19wqIAiROr8sXVum9A/rtiH5H0boUcTw==", + "dev": true, + "dependencies": { + "@babel/helper-builder-binary-assignment-operator-visitor": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-export-namespace-from": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-export-namespace-from/-/plugin-transform-export-namespace-from-7.24.1.tgz", + "integrity": "sha512-Ft38m/KFOyzKw2UaJFkWG9QnHPG/Q/2SkOrRk4pNBPg5IPZ+dOxcmkK5IyuBcxiNPyyYowPGUReyBvrvZs7IlQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-for-of": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-for-of/-/plugin-transform-for-of-7.24.1.tgz", + "integrity": "sha512-OxBdcnF04bpdQdR3i4giHZNZQn7cm8RQKcSwA17wAAqEELo1ZOwp5FFgeptWUQXFyT9kwHo10aqqauYkRZPCAg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-function-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-function-name/-/plugin-transform-function-name-7.24.1.tgz", + "integrity": "sha512-BXmDZpPlh7jwicKArQASrj8n22/w6iymRnvHYYd2zO30DbE277JO20/7yXJT3QxDPtiQiOxQBbZH4TpivNXIxA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-json-strings": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-json-strings/-/plugin-transform-json-strings-7.24.1.tgz", + "integrity": "sha512-U7RMFmRvoasscrIFy5xA4gIp8iWnWubnKkKuUGJjsuOH7GfbMkB+XZzeslx2kLdEGdOJDamEmCqOks6e8nv8DQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-json-strings": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-literals/-/plugin-transform-literals-7.24.1.tgz", + "integrity": "sha512-zn9pwz8U7nCqOYIiBaOxoQOtYmMODXTJnkxG4AtX8fPmnCRYWBOHD0qcpwS9e2VDSp1zNJYpdnFMIKb8jmwu6g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-logical-assignment-operators": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-logical-assignment-operators/-/plugin-transform-logical-assignment-operators-7.24.1.tgz", + "integrity": "sha512-OhN6J4Bpz+hIBqItTeWJujDOfNP+unqv/NJgyhlpSqgBTPm37KkMmZV6SYcOj+pnDbdcl1qRGV/ZiIjX9Iy34w==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-member-expression-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-member-expression-literals/-/plugin-transform-member-expression-literals-7.24.1.tgz", + "integrity": "sha512-4ojai0KysTWXzHseJKa1XPNXKRbuUrhkOPY4rEGeR+7ChlJVKxFa3H3Bz+7tWaGKgJAXUWKOGmltN+u9B3+CVg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-amd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-amd/-/plugin-transform-modules-amd-7.24.1.tgz", + "integrity": "sha512-lAxNHi4HVtjnHd5Rxg3D5t99Xm6H7b04hUS7EHIXcUl2EV4yl1gWdqZrNzXnSrHveL9qMdbODlLF55mvgjAfaQ==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-commonjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-commonjs/-/plugin-transform-modules-commonjs-7.24.1.tgz", + "integrity": "sha512-szog8fFTUxBfw0b98gEWPaEqF42ZUD/T3bkynW/wtgx2p/XCP55WEsb+VosKceRSd6njipdZvNogqdtI4Q0chw==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-simple-access": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-systemjs": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-systemjs/-/plugin-transform-modules-systemjs-7.24.1.tgz", + "integrity": "sha512-mqQ3Zh9vFO1Tpmlt8QPnbwGHzNz3lpNEMxQb1kAemn/erstyqw1r9KeOlOfo3y6xAnFEcOv2tSyrXfmMk+/YZA==", + "dev": true, + "dependencies": { + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-identifier": "^7.22.20" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-modules-umd": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-modules-umd/-/plugin-transform-modules-umd-7.24.1.tgz", + "integrity": "sha512-tuA3lpPj+5ITfcCluy6nWonSL7RvaG0AOTeAuvXqEKS34lnLzXpDb0dcP6K8jD0zWZFNDVly90AGFJPnm4fOYg==", + "dev": true, + "dependencies": { + "@babel/helper-module-transforms": "^7.23.3", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-named-capturing-groups-regex": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-named-capturing-groups-regex/-/plugin-transform-named-capturing-groups-regex-7.22.5.tgz", + "integrity": "sha512-YgLLKmS3aUBhHaxp5hi1WJTgOUb/NCuDHzGT9z9WTt3YG+CPRhJs6nprbStx6DnWM4dh6gt7SU3sZodbZ08adQ==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.5", + "@babel/helper-plugin-utils": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/plugin-transform-new-target": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-new-target/-/plugin-transform-new-target-7.24.1.tgz", + "integrity": "sha512-/rurytBM34hYy0HKZQyA0nHbQgQNFm4Q/BOc9Hflxi2X3twRof7NaE5W46j4kQitm7SvACVRXsa6N/tSZxvPug==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-nullish-coalescing-operator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-nullish-coalescing-operator/-/plugin-transform-nullish-coalescing-operator-7.24.1.tgz", + "integrity": "sha512-iQ+caew8wRrhCikO5DrUYx0mrmdhkaELgFa+7baMcVuhxIkN7oxt06CZ51D65ugIb1UWRQ8oQe+HXAVM6qHFjw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-numeric-separator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-numeric-separator/-/plugin-transform-numeric-separator-7.24.1.tgz", + "integrity": "sha512-7GAsGlK4cNL2OExJH1DzmDeKnRv/LXq0eLUSvudrehVA5Rgg4bIrqEUW29FbKMBRT0ztSqisv7kjP+XIC4ZMNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-numeric-separator": "^7.10.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-rest-spread": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-rest-spread/-/plugin-transform-object-rest-spread-7.24.5.tgz", + "integrity": "sha512-7EauQHszLGM3ay7a161tTQH7fj+3vVM/gThlz5HpFtnygTxjrlvoeq7MPVA1Vy9Q555OB8SnAOsMkLShNkkrHA==", + "dev": true, + "dependencies": { + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-transform-parameters": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-object-super": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-object-super/-/plugin-transform-object-super-7.24.1.tgz", + "integrity": "sha512-oKJqR3TeI5hSLRxudMjFQ9re9fBVUU0GICqM3J1mi8MqlhVr6hC/ZN4ttAyMuQR6EZZIY6h/exe5swqGNNIkWQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-replace-supers": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-catch-binding": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-catch-binding/-/plugin-transform-optional-catch-binding-7.24.1.tgz", + "integrity": "sha512-oBTH7oURV4Y+3EUrf6cWn1OHio3qG/PVwO5J03iSJmBg6m2EhKjkAu/xuaXaYwWW9miYtvbWv4LNf0AmR43LUA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-optional-chaining": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-optional-chaining/-/plugin-transform-optional-chaining-7.24.5.tgz", + "integrity": "sha512-xWCkmwKT+ihmA6l7SSTpk8e4qQl/274iNbSKRRS8mpqFR32ksy36+a+LWY8OXCCEefF8WFlnOHVsaDI2231wBg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5", + "@babel/plugin-syntax-optional-chaining": "^7.8.3" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-parameters": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-parameters/-/plugin-transform-parameters-7.24.5.tgz", + "integrity": "sha512-9Co00MqZ2aoky+4j2jhofErthm6QVLKbpQrvz20c3CH9KQCLHyNB+t2ya4/UrRpQGR+Wrwjg9foopoeSdnHOkA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-methods": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-methods/-/plugin-transform-private-methods-7.24.1.tgz", + "integrity": "sha512-tGvisebwBO5em4PaYNqt4fkw56K2VALsAbAakY0FjTYqJp7gfdrgr7YX76Or8/cpik0W6+tj3rZ0uHU9Oil4tw==", + "dev": true, + "dependencies": { + "@babel/helper-create-class-features-plugin": "^7.24.1", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-private-property-in-object": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-private-property-in-object/-/plugin-transform-private-property-in-object-7.24.5.tgz", + "integrity": "sha512-JM4MHZqnWR04jPMujQDTBVRnqxpLLpx2tkn7iPn+Hmsc0Gnb79yvRWOkvqFOx3Z7P7VxiRIR22c4eGSNj87OBQ==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-property-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-property-literals/-/plugin-transform-property-literals-7.24.1.tgz", + "integrity": "sha512-LetvD7CrHmEx0G442gOomRr66d7q8HzzGGr4PMHGr+5YIm6++Yke+jxj246rpvsbyhJwCLxcTn6zW1P1BSenqA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-constant-elements": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-constant-elements/-/plugin-transform-react-constant-elements-7.24.1.tgz", + "integrity": "sha512-QXp1U9x0R7tkiGB0FOk8o74jhnap0FlZ5gNkRIWdG3eP+SvMFg118e1zaWewDzgABb106QSKpVsD3Wgd8t6ifA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-display-name": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-display-name/-/plugin-transform-react-display-name-7.24.1.tgz", + "integrity": "sha512-mvoQg2f9p2qlpDQRBC7M3c3XTr0k7cp/0+kFKKO/7Gtu0LSw16eKB+Fabe2bDT/UpsyasTBBkAnbdsLrkD5XMw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx": { + "version": "7.23.4", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx/-/plugin-transform-react-jsx-7.23.4.tgz", + "integrity": "sha512-5xOpoPguCZCRbo/JeHlloSkTA8Bld1J/E1/kLfD1nsuiW1m8tduTA1ERCgIZokDflX/IBzKcqR3l7VlRgiIfHA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-module-imports": "^7.22.15", + "@babel/helper-plugin-utils": "^7.22.5", + "@babel/plugin-syntax-jsx": "^7.23.3", + "@babel/types": "^7.23.4" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-jsx-development": { + "version": "7.22.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-jsx-development/-/plugin-transform-react-jsx-development-7.22.5.tgz", + "integrity": "sha512-bDhuzwWMuInwCYeDeMzyi7TaBgRQei6DqxhbyniL7/VG4RSS7HtSL2QbY4eESy1KJqlWt8g3xeEBGPuo+XqC8A==", + "dev": true, + "dependencies": { + "@babel/plugin-transform-react-jsx": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-react-pure-annotations": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-react-pure-annotations/-/plugin-transform-react-pure-annotations-7.24.1.tgz", + "integrity": "sha512-+pWEAaDJvSm9aFvJNpLiM2+ktl2Sn2U5DdyiWdZBxmLc6+xGt88dvFqsHiAiDS+8WqUwbDfkKz9jRxK3M0k+kA==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-regenerator": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-regenerator/-/plugin-transform-regenerator-7.24.1.tgz", + "integrity": "sha512-sJwZBCzIBE4t+5Q4IGLaaun5ExVMRY0lYwos/jNecjMrVCygCdph3IKv0tkP5Fc87e/1+bebAmEAGBfnRD+cnw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "regenerator-transform": "^0.15.2" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-reserved-words": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-reserved-words/-/plugin-transform-reserved-words-7.24.1.tgz", + "integrity": "sha512-JAclqStUfIwKN15HrsQADFgeZt+wexNQ0uLhuqvqAUFoqPMjEcFCYZBhq0LUdz6dZK/mD+rErhW71fbx8RYElg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-shorthand-properties": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-shorthand-properties/-/plugin-transform-shorthand-properties-7.24.1.tgz", + "integrity": "sha512-LyjVB1nsJ6gTTUKRjRWx9C1s9hE7dLfP/knKdrfeH9UPtAGjYGgxIbFfx7xyLIEWs7Xe1Gnf8EWiUqfjLhInZA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-spread": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-spread/-/plugin-transform-spread-7.24.1.tgz", + "integrity": "sha512-KjmcIM+fxgY+KxPVbjelJC6hrH1CgtPmTvdXAfn3/a9CnWGSTY7nH4zm5+cjmWJybdcPSsD0++QssDsjcpe47g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-skip-transparent-expression-wrappers": "^7.22.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-sticky-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-sticky-regex/-/plugin-transform-sticky-regex-7.24.1.tgz", + "integrity": "sha512-9v0f1bRXgPVcPrngOQvLXeGNNVLc8UjMVfebo9ka0WF3/7+aVUHmaJVT3sa0XCzEFioPfPHZiOcYG9qOsH63cw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-template-literals": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-template-literals/-/plugin-transform-template-literals-7.24.1.tgz", + "integrity": "sha512-WRkhROsNzriarqECASCNu/nojeXCDTE/F2HmRgOzi7NGvyfYGq1NEjKBK3ckLfRgGc6/lPAqP0vDOSw3YtG34g==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typeof-symbol": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typeof-symbol/-/plugin-transform-typeof-symbol-7.24.5.tgz", + "integrity": "sha512-UTGnhYVZtTAjdwOTzT+sCyXmTn8AhaxOS/MjG9REclZ6ULHWF9KoCZur0HSGU7hk8PdBFKKbYe6+gqdXWz84Jg==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.5" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-typescript": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-typescript/-/plugin-transform-typescript-7.24.5.tgz", + "integrity": "sha512-E0VWu/hk83BIFUWnsKZ4D81KXjN5L3MobvevOHErASk9IPwKHOkTgvqzvNo1yP/ePJWqqK2SpUR5z+KQbl6NVw==", + "dev": true, + "dependencies": { + "@babel/helper-annotate-as-pure": "^7.22.5", + "@babel/helper-create-class-features-plugin": "^7.24.5", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/plugin-syntax-typescript": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-escapes": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-escapes/-/plugin-transform-unicode-escapes-7.24.1.tgz", + "integrity": "sha512-RlkVIcWT4TLI96zM660S877E7beKlQw7Ig+wqkKBiWfj0zH5Q4h50q6er4wzZKRNSYpfo6ILJ+hrJAGSX2qcNw==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-property-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-property-regex/-/plugin-transform-unicode-property-regex-7.24.1.tgz", + "integrity": "sha512-Ss4VvlfYV5huWApFsF8/Sq0oXnGO+jB+rijFEFugTd3cwSObUSnUi88djgR5528Csl0uKlrI331kRqe56Ov2Ng==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-regex/-/plugin-transform-unicode-regex-7.24.1.tgz", + "integrity": "sha512-2A/94wgZgxfTsiLaQ2E36XAOdcZmGAaEEgVmxQWwZXWkGhvoHbaqXcKnU8zny4ycpu3vNqg0L/PcCiYtHtA13g==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/plugin-transform-unicode-sets-regex": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/plugin-transform-unicode-sets-regex/-/plugin-transform-unicode-sets-regex-7.24.1.tgz", + "integrity": "sha512-fqj4WuzzS+ukpgerpAoOnMfQXwUHFxXUZUE84oL2Kao2N8uSlvcpnAidKASgsNgzZHBsHWvcm8s9FPWUhAb8fA==", + "dev": true, + "dependencies": { + "@babel/helper-create-regexp-features-plugin": "^7.22.15", + "@babel/helper-plugin-utils": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0" + } + }, + "node_modules/@babel/preset-env": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/preset-env/-/preset-env-7.24.5.tgz", + "integrity": "sha512-UGK2ifKtcC8i5AI4cH+sbLLuLc2ktYSFJgBAXorKAsHUZmrQ1q6aQ6i3BvU24wWs2AAKqQB6kq3N9V9Gw1HiMQ==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.24.4", + "@babel/helper-compilation-targets": "^7.23.6", + "@babel/helper-plugin-utils": "^7.24.5", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-bugfix-firefox-class-in-computed-class-key": "^7.24.5", + "@babel/plugin-bugfix-safari-id-destructuring-collision-in-function-expression": "^7.24.1", + "@babel/plugin-bugfix-v8-spread-parameters-in-optional-chaining": "^7.24.1", + "@babel/plugin-bugfix-v8-static-class-fields-redefine-readonly": "^7.24.1", + "@babel/plugin-proposal-private-property-in-object": "7.21.0-placeholder-for-preset-env.2", + "@babel/plugin-syntax-async-generators": "^7.8.4", + "@babel/plugin-syntax-class-properties": "^7.12.13", + "@babel/plugin-syntax-class-static-block": "^7.14.5", + "@babel/plugin-syntax-dynamic-import": "^7.8.3", + "@babel/plugin-syntax-export-namespace-from": "^7.8.3", + "@babel/plugin-syntax-import-assertions": "^7.24.1", + "@babel/plugin-syntax-import-attributes": "^7.24.1", + "@babel/plugin-syntax-import-meta": "^7.10.4", + "@babel/plugin-syntax-json-strings": "^7.8.3", + "@babel/plugin-syntax-logical-assignment-operators": "^7.10.4", + "@babel/plugin-syntax-nullish-coalescing-operator": "^7.8.3", + "@babel/plugin-syntax-numeric-separator": "^7.10.4", + "@babel/plugin-syntax-object-rest-spread": "^7.8.3", + "@babel/plugin-syntax-optional-catch-binding": "^7.8.3", + "@babel/plugin-syntax-optional-chaining": "^7.8.3", + "@babel/plugin-syntax-private-property-in-object": "^7.14.5", + "@babel/plugin-syntax-top-level-await": "^7.14.5", + "@babel/plugin-syntax-unicode-sets-regex": "^7.18.6", + "@babel/plugin-transform-arrow-functions": "^7.24.1", + "@babel/plugin-transform-async-generator-functions": "^7.24.3", + "@babel/plugin-transform-async-to-generator": "^7.24.1", + "@babel/plugin-transform-block-scoped-functions": "^7.24.1", + "@babel/plugin-transform-block-scoping": "^7.24.5", + "@babel/plugin-transform-class-properties": "^7.24.1", + "@babel/plugin-transform-class-static-block": "^7.24.4", + "@babel/plugin-transform-classes": "^7.24.5", + "@babel/plugin-transform-computed-properties": "^7.24.1", + "@babel/plugin-transform-destructuring": "^7.24.5", + "@babel/plugin-transform-dotall-regex": "^7.24.1", + "@babel/plugin-transform-duplicate-keys": "^7.24.1", + "@babel/plugin-transform-dynamic-import": "^7.24.1", + "@babel/plugin-transform-exponentiation-operator": "^7.24.1", + "@babel/plugin-transform-export-namespace-from": "^7.24.1", + "@babel/plugin-transform-for-of": "^7.24.1", + "@babel/plugin-transform-function-name": "^7.24.1", + "@babel/plugin-transform-json-strings": "^7.24.1", + "@babel/plugin-transform-literals": "^7.24.1", + "@babel/plugin-transform-logical-assignment-operators": "^7.24.1", + "@babel/plugin-transform-member-expression-literals": "^7.24.1", + "@babel/plugin-transform-modules-amd": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-modules-systemjs": "^7.24.1", + "@babel/plugin-transform-modules-umd": "^7.24.1", + "@babel/plugin-transform-named-capturing-groups-regex": "^7.22.5", + "@babel/plugin-transform-new-target": "^7.24.1", + "@babel/plugin-transform-nullish-coalescing-operator": "^7.24.1", + "@babel/plugin-transform-numeric-separator": "^7.24.1", + "@babel/plugin-transform-object-rest-spread": "^7.24.5", + "@babel/plugin-transform-object-super": "^7.24.1", + "@babel/plugin-transform-optional-catch-binding": "^7.24.1", + "@babel/plugin-transform-optional-chaining": "^7.24.5", + "@babel/plugin-transform-parameters": "^7.24.5", + "@babel/plugin-transform-private-methods": "^7.24.1", + "@babel/plugin-transform-private-property-in-object": "^7.24.5", + "@babel/plugin-transform-property-literals": "^7.24.1", + "@babel/plugin-transform-regenerator": "^7.24.1", + "@babel/plugin-transform-reserved-words": "^7.24.1", + "@babel/plugin-transform-shorthand-properties": "^7.24.1", + "@babel/plugin-transform-spread": "^7.24.1", + "@babel/plugin-transform-sticky-regex": "^7.24.1", + "@babel/plugin-transform-template-literals": "^7.24.1", + "@babel/plugin-transform-typeof-symbol": "^7.24.5", + "@babel/plugin-transform-unicode-escapes": "^7.24.1", + "@babel/plugin-transform-unicode-property-regex": "^7.24.1", + "@babel/plugin-transform-unicode-regex": "^7.24.1", + "@babel/plugin-transform-unicode-sets-regex": "^7.24.1", + "@babel/preset-modules": "0.1.6-no-external-plugins", + "babel-plugin-polyfill-corejs2": "^0.4.10", + "babel-plugin-polyfill-corejs3": "^0.10.4", + "babel-plugin-polyfill-regenerator": "^0.6.1", + "core-js-compat": "^3.31.0", + "semver": "^6.3.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-env/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/@babel/preset-modules": { + "version": "0.1.6-no-external-plugins", + "resolved": "https://registry.npmjs.org/@babel/preset-modules/-/preset-modules-0.1.6-no-external-plugins.tgz", + "integrity": "sha512-HrcgcIESLm9aIR842yhJ5RWan/gebQUJ6E/E5+rf0y9o6oj7w0Br+sWuL6kEQ/o/AdfvR1Je9jG18/gnpwjEyA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.0.0", + "@babel/types": "^7.4.4", + "esutils": "^2.0.2" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/@babel/preset-react": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-react/-/preset-react-7.24.1.tgz", + "integrity": "sha512-eFa8up2/8cZXLIpkafhaADTXSnl7IsUFCYenRWrARBz0/qZwcT0RBXpys0LJU4+WfPoF2ZG6ew6s2V6izMCwRA==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-transform-react-display-name": "^7.24.1", + "@babel/plugin-transform-react-jsx": "^7.23.4", + "@babel/plugin-transform-react-jsx-development": "^7.22.5", + "@babel/plugin-transform-react-pure-annotations": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/preset-typescript": { + "version": "7.24.1", + "resolved": "https://registry.npmjs.org/@babel/preset-typescript/-/preset-typescript-7.24.1.tgz", + "integrity": "sha512-1DBaMmRDpuYQBPWD8Pf/WEwCrtgRHxsZnP4mIy9G/X+hFfbI47Q2G4t1Paakld84+qsk2fSsUPMKg71jkoOOaQ==", + "dev": true, + "dependencies": { + "@babel/helper-plugin-utils": "^7.24.0", + "@babel/helper-validator-option": "^7.23.5", + "@babel/plugin-syntax-jsx": "^7.24.1", + "@babel/plugin-transform-modules-commonjs": "^7.24.1", + "@babel/plugin-transform-typescript": "^7.24.1" + }, + "engines": { + "node": ">=6.9.0" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@babel/regjsgen": { + "version": "0.8.0", + "resolved": "https://registry.npmjs.org/@babel/regjsgen/-/regjsgen-0.8.0.tgz", + "integrity": "sha512-x/rqGMdzj+fWZvCOYForTghzbtqPDZ5gPwaoNGHdgDfF2QA/XZbCBp4Moo5scrkAMPhB7z26XM/AaHuIJdgauA==", + "dev": true + }, "node_modules/@babel/runtime": { "version": "7.24.5", "resolved": "https://registry.npmjs.org/@babel/runtime/-/runtime-7.24.5.tgz", @@ -45,6 +1982,64 @@ "node": ">=6.9.0" } }, + "node_modules/@babel/template": { + "version": "7.24.0", + "resolved": "https://registry.npmjs.org/@babel/template/-/template-7.24.0.tgz", + "integrity": "sha512-Bkf2q8lMB0AFpX0NFEqSbx1OkTHf0f+0j82mkw+ZpzBnkk7e9Ql0891vlfgi+kHwOk8tQjiQHpqh4LaSa0fKEA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.23.5", + "@babel/parser": "^7.24.0", + "@babel/types": "^7.24.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/traverse/-/traverse-7.24.5.tgz", + "integrity": "sha512-7aaBLeDQ4zYcUFDUD41lJc1fG8+5IU9DaNSJAgal866FGvmD5EbWQgnEC6kO1gGLsX0esNkfnJSndbTXA3r7UA==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.24.2", + "@babel/generator": "^7.24.5", + "@babel/helper-environment-visitor": "^7.22.20", + "@babel/helper-function-name": "^7.23.0", + "@babel/helper-hoist-variables": "^7.22.5", + "@babel/helper-split-export-declaration": "^7.24.5", + "@babel/parser": "^7.24.5", + "@babel/types": "^7.24.5", + "debug": "^4.3.1", + "globals": "^11.1.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, + "node_modules/@babel/traverse/node_modules/globals": { + "version": "11.12.0", + "resolved": "https://registry.npmjs.org/globals/-/globals-11.12.0.tgz", + "integrity": "sha512-WOBp/EEGUiIsJSp7wcv/y6MO+lV9UoncWqxuFfm8eBwzWNgyfBd6Gz+IeKQ9jCmyhoH99g15M3T+QaVHFjizVA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/@babel/types": { + "version": "7.24.5", + "resolved": "https://registry.npmjs.org/@babel/types/-/types-7.24.5.tgz", + "integrity": "sha512-6mQNsaLeXTw0nxYUYu+NSa4Hx4BlF1x1x8/PMFbiR+GBSr+2DkECc69b8hgy2frEodNcvPffeH8YfWd3LI6jhQ==", + "dev": true, + "dependencies": { + "@babel/helper-string-parser": "^7.24.1", + "@babel/helper-validator-identifier": "^7.24.5", + "to-fast-properties": "^2.0.0" + }, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/@eslint-community/eslint-utils": { "version": "4.4.0", "resolved": "https://registry.npmjs.org/@eslint-community/eslint-utils/-/eslint-utils-4.4.0.tgz", @@ -307,112 +2302,369 @@ "node": ">= 10" } }, - "node_modules/@next/swc-linux-x64-musl": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", - "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "linux" - ], + "node_modules/@next/swc-linux-x64-musl": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-linux-x64-musl/-/swc-linux-x64-musl-14.2.3.tgz", + "integrity": "sha512-BTAbq0LnCbF5MtoM7I/9UeUu/8ZBY0i8SFjUMCbPDOLv+un67e2JgyN4pmgfXBwy/I+RHu8q+k+MCkDN6P9ViQ==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "linux" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-arm64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", + "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", + "cpu": [ + "arm64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-ia32-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", + "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", + "cpu": [ + "ia32" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@next/swc-win32-x64-msvc": { + "version": "14.2.3", + "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", + "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", + "cpu": [ + "x64" + ], + "optional": true, + "os": [ + "win32" + ], + "engines": { + "node": ">= 10" + } + }, + "node_modules/@nodelib/fs.scandir": { + "version": "2.1.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", + "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "dependencies": { + "@nodelib/fs.stat": "2.0.5", + "run-parallel": "^1.1.9" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.stat": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", + "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "engines": { + "node": ">= 8" + } + }, + "node_modules/@nodelib/fs.walk": { + "version": "1.2.8", + "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", + "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "dependencies": { + "@nodelib/fs.scandir": "2.1.5", + "fastq": "^1.6.0" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/@pkgjs/parseargs": { + "version": "0.11.0", + "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", + "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", + "optional": true, + "engines": { + "node": ">=14" + } + }, + "node_modules/@rushstack/eslint-patch": { + "version": "1.10.3", + "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", + "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", + "dev": true + }, + "node_modules/@svgr/babel-plugin-add-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-add-jsx-attribute/-/babel-plugin-add-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-b9MIk7yhdS1pMCZM8VeNfUlSKVRhsHZNMl5O9SfaX0l0t5wjdgu4IDzGB8bpnGBBOjGST3rRFVsaaEtI4W6f7g==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-attribute": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-attribute/-/babel-plugin-remove-jsx-attribute-8.0.0.tgz", + "integrity": "sha512-BcCkm/STipKvbCl6b7QFrMh/vx00vIP63k2eM66MfHJzPr6O2U0jYEViXkHJWqXqQYjdeA9cuCl5KWmlwjDvbA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-remove-jsx-empty-expression": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-remove-jsx-empty-expression/-/babel-plugin-remove-jsx-empty-expression-8.0.0.tgz", + "integrity": "sha512-5BcGCBfBxB5+XSDSWnhTThfI9jcO5f0Ai2V24gZpG+wXF14BzwxxdDb4g6trdOux0rhibGs385BeFMSmxtS3uA==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-replace-jsx-attribute-value": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-replace-jsx-attribute-value/-/babel-plugin-replace-jsx-attribute-value-8.0.0.tgz", + "integrity": "sha512-KVQ+PtIjb1BuYT3ht8M5KbzWBhdAjjUPdlMtpuw/VjT8coTrItWX6Qafl9+ji831JaJcu6PJNKCV0bp01lBNzQ==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-dynamic-title": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-dynamic-title/-/babel-plugin-svg-dynamic-title-8.0.0.tgz", + "integrity": "sha512-omNiKqwjNmOQJ2v6ge4SErBbkooV2aAWwaPFs2vUY7p7GhVkzRkJ00kILXQvRhA6miHnNpXv7MRnnSjdRjK8og==", + "dev": true, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" + } + }, + "node_modules/@svgr/babel-plugin-svg-em-dimensions": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-svg-em-dimensions/-/babel-plugin-svg-em-dimensions-8.0.0.tgz", + "integrity": "sha512-mURHYnu6Iw3UBTbhGwE/vsngtCIbHE43xCRK7kCw4t01xyGqb2Pd+WXekRRoFOBIY29ZoOhUCTEweDMdrjfi9g==", + "dev": true, "engines": { - "node": ">= 10" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@next/swc-win32-arm64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-arm64-msvc/-/swc-win32-arm64-msvc-14.2.3.tgz", - "integrity": "sha512-AEHIw/dhAMLNFJFJIJIyOFDzrzI5bAjI9J26gbO5xhAKHYTZ9Or04BesFPXiAYXDNdrwTP2dQceYA4dL1geu8A==", - "cpu": [ - "arm64" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@svgr/babel-plugin-transform-react-native-svg": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-react-native-svg/-/babel-plugin-transform-react-native-svg-8.1.0.tgz", + "integrity": "sha512-Tx8T58CHo+7nwJ+EhUwx3LfdNSG9R2OKfaIXXs5soiy5HtgoAEkDay9LIimLOcG8dJQH1wPZp/cnAv6S9CrR1Q==", + "dev": true, "engines": { - "node": ">= 10" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@next/swc-win32-ia32-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-ia32-msvc/-/swc-win32-ia32-msvc-14.2.3.tgz", - "integrity": "sha512-vga40n1q6aYb0CLrM+eEmisfKCR45ixQYXuBXxOOmmoV8sYST9k7E3US32FsY+CkkF7NtzdcebiFT4CHuMSyZw==", - "cpu": [ - "ia32" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@svgr/babel-plugin-transform-svg-component": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-plugin-transform-svg-component/-/babel-plugin-transform-svg-component-8.0.0.tgz", + "integrity": "sha512-DFx8xa3cZXTdb/k3kfPeaixecQLgKh5NVBMwD0AQxOzcZawK4oo1Jh9LbrcACUivsCA7TLG8eeWgrDXjTMhRmw==", + "dev": true, "engines": { - "node": ">= 10" + "node": ">=12" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@next/swc-win32-x64-msvc": { - "version": "14.2.3", - "resolved": "https://registry.npmjs.org/@next/swc-win32-x64-msvc/-/swc-win32-x64-msvc-14.2.3.tgz", - "integrity": "sha512-Q1/zm43RWynxrO7lW4ehciQVj+5ePBhOK+/K2P7pLFX3JaJ/IZVC69SHidrmZSOkqz7ECIOhhy7XhAFG4JYyHA==", - "cpu": [ - "x64" - ], - "optional": true, - "os": [ - "win32" - ], + "node_modules/@svgr/babel-preset": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/babel-preset/-/babel-preset-8.1.0.tgz", + "integrity": "sha512-7EYDbHE7MxHpv4sxvnVPngw5fuR6pw79SkcrILHJ/iMpuKySNCl5W1qcwPEpU+LgyRXOaAFgH0KhwD18wwg6ug==", + "dev": true, + "dependencies": { + "@svgr/babel-plugin-add-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-attribute": "8.0.0", + "@svgr/babel-plugin-remove-jsx-empty-expression": "8.0.0", + "@svgr/babel-plugin-replace-jsx-attribute-value": "8.0.0", + "@svgr/babel-plugin-svg-dynamic-title": "8.0.0", + "@svgr/babel-plugin-svg-em-dimensions": "8.0.0", + "@svgr/babel-plugin-transform-react-native-svg": "8.1.0", + "@svgr/babel-plugin-transform-svg-component": "8.0.0" + }, "engines": { - "node": ">= 10" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@babel/core": "^7.0.0-0" } }, - "node_modules/@nodelib/fs.scandir": { - "version": "2.1.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.scandir/-/fs.scandir-2.1.5.tgz", - "integrity": "sha512-vq24Bq3ym5HEQm2NKCr3yXDwjc7vTsEThRDnkp2DK9p1uqLR+DHurm/NOTo0KG7HYHU7eppKZj3MyqYuMBf62g==", + "node_modules/@svgr/core": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/core/-/core-8.1.0.tgz", + "integrity": "sha512-8QqtOQT5ACVlmsvKOJNEaWmRPmcojMOzCz4Hs2BGG/toAp/K38LcsMRyLp349glq5AzJbCEeimEoxaX6v/fLrA==", + "dev": true, "dependencies": { - "@nodelib/fs.stat": "2.0.5", - "run-parallel": "^1.1.9" + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "camelcase": "^6.2.0", + "cosmiconfig": "^8.1.3", + "snake-case": "^3.0.4" }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@nodelib/fs.stat": { - "version": "2.0.5", - "resolved": "https://registry.npmjs.org/@nodelib/fs.stat/-/fs.stat-2.0.5.tgz", - "integrity": "sha512-RkhPPp2zrqDAQA/2jNhnztcPAlv64XdhIp7a7454A5ovI7Bukxgt7MX7udwAu3zg1DcpPU0rz3VV1SeaqvY4+A==", + "node_modules/@svgr/hast-util-to-babel-ast": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/@svgr/hast-util-to-babel-ast/-/hast-util-to-babel-ast-8.0.0.tgz", + "integrity": "sha512-EbDKwO9GpfWP4jN9sGdYwPBU0kdomaPIL2Eu4YwmgP+sJeXT+L7bMwJUBnhzfH8Q2qMBqZ4fJwpCyYsAN3mt2Q==", + "dev": true, + "dependencies": { + "@babel/types": "^7.21.3", + "entities": "^4.4.0" + }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" } }, - "node_modules/@nodelib/fs.walk": { - "version": "1.2.8", - "resolved": "https://registry.npmjs.org/@nodelib/fs.walk/-/fs.walk-1.2.8.tgz", - "integrity": "sha512-oGB+UxlgWcgQkgwo8GcEGwemoTFt3FIO9ababBmaGwXIoBKZ+GTy0pP185beGg7Llih/NSHSV2XAs1lnznocSg==", + "node_modules/@svgr/plugin-jsx": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-jsx/-/plugin-jsx-8.1.0.tgz", + "integrity": "sha512-0xiIyBsLlr8quN+WyuxooNW9RJ0Dpr8uOnH/xrCVO8GLUcwHISwj1AG0k+LFzteTkAA0GbX0kj9q6Dk70PTiPA==", + "dev": true, "dependencies": { - "@nodelib/fs.scandir": "2.1.5", - "fastq": "^1.6.0" + "@babel/core": "^7.21.3", + "@svgr/babel-preset": "8.1.0", + "@svgr/hast-util-to-babel-ast": "8.0.0", + "svg-parser": "^2.0.4" }, "engines": { - "node": ">= 8" + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "node_modules/@pkgjs/parseargs": { - "version": "0.11.0", - "resolved": "https://registry.npmjs.org/@pkgjs/parseargs/-/parseargs-0.11.0.tgz", - "integrity": "sha512-+1VkjdD0QBLPodGrJUeqarH8VAIvQODIbwh9XpP5Syisf7YoQgsJKPNFoqqLQlu+VQ/tVSshMR6loPMn8U+dPg==", - "optional": true, + "node_modules/@svgr/plugin-svgo": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/plugin-svgo/-/plugin-svgo-8.1.0.tgz", + "integrity": "sha512-Ywtl837OGO9pTLIN/onoWLmDQ4zFUycI1g76vuKGEz6evR/ZTJlJuz3G/fIkb6OVBJ2g0o6CGJzaEjfmEo3AHA==", + "dev": true, + "dependencies": { + "cosmiconfig": "^8.1.3", + "deepmerge": "^4.3.1", + "svgo": "^3.0.2" + }, "engines": { "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + }, + "peerDependencies": { + "@svgr/core": "*" } }, - "node_modules/@rushstack/eslint-patch": { - "version": "1.10.3", - "resolved": "https://registry.npmjs.org/@rushstack/eslint-patch/-/eslint-patch-1.10.3.tgz", - "integrity": "sha512-qC/xYId4NMebE6w/V33Fh9gWxLgURiNYgVNObbJl2LZv0GUUItCcCqC5axQSwRaAgaxl2mELq1rMzlswaQ0Zxg==", - "dev": true + "node_modules/@svgr/webpack": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/@svgr/webpack/-/webpack-8.1.0.tgz", + "integrity": "sha512-LnhVjMWyMQV9ZmeEy26maJk+8HTIbd59cH4F2MJ439k9DqejRisfFNGAPvRYlKETuh9LrImlS8aKsBgKjMA8WA==", + "dev": true, + "dependencies": { + "@babel/core": "^7.21.3", + "@babel/plugin-transform-react-constant-elements": "^7.21.3", + "@babel/preset-env": "^7.20.2", + "@babel/preset-react": "^7.18.6", + "@babel/preset-typescript": "^7.21.0", + "@svgr/core": "8.1.0", + "@svgr/plugin-jsx": "8.1.0", + "@svgr/plugin-svgo": "8.1.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "type": "github", + "url": "https://github.com/sponsors/gregberge" + } }, "node_modules/@swc/counter": { "version": "0.1.3", @@ -436,6 +2688,15 @@ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" } }, + "node_modules/@trysound/sax": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/@trysound/sax/-/sax-0.2.0.tgz", + "integrity": "sha512-L7z9BgrNEcYyUYtF+HaEfiS5ebkh9jXqbszz7pC0hRBPaatV0XjSD3+eHrpqFemQfgwiFF0QPIarnIihIDn7OA==", + "dev": true, + "engines": { + "node": ">=10.13.0" + } + }, "node_modules/@types/json5": { "version": "0.0.29", "resolved": "https://registry.npmjs.org/@types/json5/-/json5-0.0.29.tgz", @@ -878,6 +3139,54 @@ "dequal": "^2.0.3" } }, + "node_modules/babel-plugin-polyfill-corejs2": { + "version": "0.4.11", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs2/-/babel-plugin-polyfill-corejs2-0.4.11.tgz", + "integrity": "sha512-sMEJ27L0gRHShOh5G54uAAPaiCOygY/5ratXuiyb2G46FmlSpc9eFCzYVyDiPxfNbwzA7mYahmjQc5q+CZQ09Q==", + "dev": true, + "dependencies": { + "@babel/compat-data": "^7.22.6", + "@babel/helper-define-polyfill-provider": "^0.6.2", + "semver": "^6.3.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-corejs2/node_modules/semver": { + "version": "6.3.1", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.1.tgz", + "integrity": "sha512-BR7VvDCVHO+q2xBEWskxS6DJE1qRnb7DxzUrogb71CWoSficBxYsiAGd+Kl0mmq/MprG9yArRkyrQxTO6XjMzA==", + "dev": true, + "bin": { + "semver": "bin/semver.js" + } + }, + "node_modules/babel-plugin-polyfill-corejs3": { + "version": "0.10.4", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-corejs3/-/babel-plugin-polyfill-corejs3-0.10.4.tgz", + "integrity": "sha512-25J6I8NGfa5YkCDogHRID3fVCadIR8/pGl1/spvCkzb6lVn6SR3ojpx9nOn9iEBcUsjY24AmdKm5khcfKdylcg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.1", + "core-js-compat": "^3.36.1" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, + "node_modules/babel-plugin-polyfill-regenerator": { + "version": "0.6.2", + "resolved": "https://registry.npmjs.org/babel-plugin-polyfill-regenerator/-/babel-plugin-polyfill-regenerator-0.6.2.tgz", + "integrity": "sha512-2R25rQZWP63nGwaAswvDazbPXfrM3HwVoBXK6HcqeKrSrL/JqcC/rDcf95l4r7LXLyxDXc8uQDa064GubtCABg==", + "dev": true, + "dependencies": { + "@babel/helper-define-polyfill-provider": "^0.6.2" + }, + "peerDependencies": { + "@babel/core": "^7.4.0 || ^8.0.0-0 <8.0.0" + } + }, "node_modules/balanced-match": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", @@ -894,6 +3203,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/boolbase": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/boolbase/-/boolbase-1.0.0.tgz", + "integrity": "sha512-JZOSA7Mo9sNGB8+UjSgzdLtokWAky1zbztM3WRLCbZ70/3cTANmQmOdR7y2g+J0e2WXywy1yS468tY+IruqEww==", + "dev": true + }, "node_modules/brace-expansion": { "version": "1.1.11", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.11.tgz", @@ -915,6 +3230,38 @@ "node": ">=8" } }, + "node_modules/browserslist": { + "version": "4.23.0", + "resolved": "https://registry.npmjs.org/browserslist/-/browserslist-4.23.0.tgz", + "integrity": "sha512-QW8HiM1shhT2GuzkvklfjcKDiWFXHOeFCIA/huJPwHsslwcydgk7X+z2zXpEijP98UCY7HbubZt5J2Zgvf0CaQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "caniuse-lite": "^1.0.30001587", + "electron-to-chromium": "^1.4.668", + "node-releases": "^2.0.14", + "update-browserslist-db": "^1.0.13" + }, + "bin": { + "browserslist": "cli.js" + }, + "engines": { + "node": "^6 || ^7 || ^8 || ^9 || ^10 || ^11 || ^12 || >=13.7" + } + }, "node_modules/busboy": { "version": "1.6.0", "resolved": "https://registry.npmjs.org/busboy/-/busboy-1.6.0.tgz", @@ -954,6 +3301,18 @@ "node": ">=6" } }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/camelcase-css": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/camelcase-css/-/camelcase-css-2.0.1.tgz", @@ -1066,17 +3425,103 @@ "integrity": "sha512-/Srv4dswyQNBfohGpz9o6Yb3Gz3SrUDqBH5rTuhGR7ahtlbYKnVxw2bCFMRljaA7EXHaXZ8wsHdodFvbkhKmqg==", "dev": true }, + "node_modules/convert-source-map": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/convert-source-map/-/convert-source-map-2.0.0.tgz", + "integrity": "sha512-Kvp459HrV2FEJ1CAsi1Ku+MY3kasH19TFykTz2xWmMeq6bk2NU3XXvfJ+Q61m0xktWwt+1HSYf3JZsTms3aRJg==", + "dev": true + }, + "node_modules/core-js-compat": { + "version": "3.37.1", + "resolved": "https://registry.npmjs.org/core-js-compat/-/core-js-compat-3.37.1.tgz", + "integrity": "sha512-9TNiImhKvQqSUkOvk/mMRZzOANTiEVC7WaBNhHcKM7x+/5E1l5NvsysR19zuDQScE8k+kfQXWRN3AtS/eOSHpg==", + "dev": true, + "dependencies": { + "browserslist": "^4.23.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/core-js" + } + }, + "node_modules/cosmiconfig": { + "version": "8.3.6", + "resolved": "https://registry.npmjs.org/cosmiconfig/-/cosmiconfig-8.3.6.tgz", + "integrity": "sha512-kcZ6+W5QzcJ3P1Mt+83OUv/oHFqZHIx8DuxG6eZ5RGMERoLqp4BuGjhHLYGK+Kf5XVkQvqBSmAy/nGWN3qDgEA==", + "dev": true, + "dependencies": { + "import-fresh": "^3.3.0", + "js-yaml": "^4.1.0", + "parse-json": "^5.2.0", + "path-type": "^4.0.0" + }, + "engines": { + "node": ">=14" + }, + "funding": { + "url": "https://github.com/sponsors/d-fischer" + }, + "peerDependencies": { + "typescript": ">=4.9.5" + }, + "peerDependenciesMeta": { + "typescript": { + "optional": true + } + } + }, "node_modules/cross-spawn": { "version": "7.0.3", "resolved": "https://registry.npmjs.org/cross-spawn/-/cross-spawn-7.0.3.tgz", "integrity": "sha512-iRDPJKUPVEND7dHPO8rkbOnPpyDygcDFtWjpeWNCgy8WP2rXcxXL8TskReQl6OrB2G7+UJrags1q15Fudc7G6w==", "dependencies": { - "path-key": "^3.1.0", - "shebang-command": "^2.0.0", - "which": "^2.0.1" + "path-key": "^3.1.0", + "shebang-command": "^2.0.0", + "which": "^2.0.1" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/css-select": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz", + "integrity": "sha512-nwoRF1rvRRnnCqqY7updORDsuqKzqYJ28+oSMaJMMgOauh3fvwHqMS7EZpIPqK8GL+g9mKxF1vP/ZjSeNjEVHg==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0", + "css-what": "^6.1.0", + "domhandler": "^5.0.2", + "domutils": "^3.0.1", + "nth-check": "^2.0.1" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" + } + }, + "node_modules/css-tree": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.3.1.tgz", + "integrity": "sha512-6Fv1DV/TYw//QF5IzQdqsNDjx/wc8TrMBZsqjL9eW01tWb7R7k/mq+/VXfJCl7SoD5emsJop9cOByJZfs8hYIw==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.30", + "source-map-js": "^1.0.1" }, "engines": { - "node": ">= 8" + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0" + } + }, + "node_modules/css-what": { + "version": "6.1.0", + "resolved": "https://registry.npmjs.org/css-what/-/css-what-6.1.0.tgz", + "integrity": "sha512-HTUrgRJ7r4dsZKU6GjmpfRK1O76h97Z8MfS1G0FozR+oF2kG6Vfe8JE6zwrkbxigziPHinCJ+gCPjA9EaBDtRw==", + "dev": true, + "engines": { + "node": ">= 6" + }, + "funding": { + "url": "https://github.com/sponsors/fb55" } }, "node_modules/cssesc": { @@ -1090,6 +3535,39 @@ "node": ">=4" } }, + "node_modules/csso": { + "version": "5.0.5", + "resolved": "https://registry.npmjs.org/csso/-/csso-5.0.5.tgz", + "integrity": "sha512-0LrrStPOdJj+SPCCrGhzryycLjwcgUSHBtxNA8aIDxf0GLsRh1cKYhB00Gd1lDOS4yGH69+SNn13+TWbVHETFQ==", + "dev": true, + "dependencies": { + "css-tree": "~2.2.0" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/css-tree": { + "version": "2.2.1", + "resolved": "https://registry.npmjs.org/css-tree/-/css-tree-2.2.1.tgz", + "integrity": "sha512-OA0mILzGc1kCOCSJerOeqDxDQ4HOh+G8NbOJFOTgOCzpw7fCBubk0fEyxp8AgOL/jvLgYA/uV0cMbe43ElF1JA==", + "dev": true, + "dependencies": { + "mdn-data": "2.0.28", + "source-map-js": "^1.0.1" + }, + "engines": { + "node": "^10 || ^12.20.0 || ^14.13.0 || >=15.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/csso/node_modules/mdn-data": { + "version": "2.0.28", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.28.tgz", + "integrity": "sha512-aylIc7Z9y4yzHYAJNuESG3hfhC+0Ibp/MAMiaOZgNv4pmEdFyfZhhhny4MNiAfWdBQ1RQ2mfDWmM1x8SvGyp8g==", + "dev": true + }, "node_modules/damerau-levenshtein": { "version": "1.0.8", "resolved": "https://registry.npmjs.org/damerau-levenshtein/-/damerau-levenshtein-1.0.8.tgz", @@ -1170,6 +3648,15 @@ "integrity": "sha512-oIPzksmTg4/MriiaYGO+okXDT7ztn/w3Eptv/+gSIdMdKsJo0u4CfYNFJPy+4SKMuCqGw2wxnA+URMg3t8a/bQ==", "dev": true }, + "node_modules/deepmerge": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/deepmerge/-/deepmerge-4.3.1.tgz", + "integrity": "sha512-3sUqbMEc77XqpdNO7FRyRog+eW3ph+GYCbj+rK+uYyRMuwsVy0rMiVtPn+QJlKFvWP/1PYpapqYn0Me2knFn+A==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, "node_modules/define-data-property": { "version": "1.1.4", "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.4.tgz", @@ -1247,11 +3734,82 @@ "node": ">=6.0.0" } }, + "node_modules/dom-serializer": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/dom-serializer/-/dom-serializer-2.0.0.tgz", + "integrity": "sha512-wIkAryiqt/nV5EQKqQpo3SToSOV9J0DnbJqwK7Wv/Trc92zIAYZ4FlMu+JPFW1DfGFt81ZTCGgDEabffXeLyJg==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0", + "domhandler": "^5.0.2", + "entities": "^4.2.0" + }, + "funding": { + "url": "https://github.com/cheeriojs/dom-serializer?sponsor=1" + } + }, + "node_modules/domelementtype": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/domelementtype/-/domelementtype-2.3.0.tgz", + "integrity": "sha512-OLETBj6w0OsagBwdXnPdN0cnMfF9opN69co+7ZrbfPGrdpPVNBUj02spi6B1N7wChLQiPn4CSH/zJvXw56gmHw==", + "dev": true, + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/fb55" + } + ] + }, + "node_modules/domhandler": { + "version": "5.0.3", + "resolved": "https://registry.npmjs.org/domhandler/-/domhandler-5.0.3.tgz", + "integrity": "sha512-cgwlv/1iFQiFnU96XXgROh8xTeetsnJiDsTc7TYCLFd9+/WNkIqPTxiM/8pSd8VIrhXGTf1Ny1q1hquVqDJB5w==", + "dev": true, + "dependencies": { + "domelementtype": "^2.3.0" + }, + "engines": { + "node": ">= 4" + }, + "funding": { + "url": "https://github.com/fb55/domhandler?sponsor=1" + } + }, + "node_modules/domutils": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/domutils/-/domutils-3.1.0.tgz", + "integrity": "sha512-H78uMmQtI2AhgDJjWeQmHwJJ2bLPD3GMmO7Zja/ZZh84wkm+4ut+IUnUdRa8uCGX88DiVx1j6FRe1XfxEgjEZA==", + "dev": true, + "dependencies": { + "dom-serializer": "^2.0.0", + "domelementtype": "^2.3.0", + "domhandler": "^5.0.3" + }, + "funding": { + "url": "https://github.com/fb55/domutils?sponsor=1" + } + }, + "node_modules/dot-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/dot-case/-/dot-case-3.0.4.tgz", + "integrity": "sha512-Kv5nKlh6yRrdrGvxeJ2e5y2eRUpkUosIW4A2AS38zwSz27zu7ufDwQPi5Jhs3XAlGNetl3bmnGhQsMtkKJnj3w==", + "dev": true, + "dependencies": { + "no-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/eastasianwidth": { "version": "0.2.0", "resolved": "https://registry.npmjs.org/eastasianwidth/-/eastasianwidth-0.2.0.tgz", "integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==" }, + "node_modules/electron-to-chromium": { + "version": "1.4.779", + "resolved": "https://registry.npmjs.org/electron-to-chromium/-/electron-to-chromium-1.4.779.tgz", + "integrity": "sha512-oaTiIcszNfySXVJzKcjxd2YjPxziAd+GmXyb2HbidCeFo6Z88ygOT7EimlrEQhM2U08VhSrbKhLOXP0kKUCZ6g==", + "dev": true + }, "node_modules/emoji-regex": { "version": "9.2.2", "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-9.2.2.tgz", @@ -1270,6 +3828,27 @@ "node": ">=10.13.0" } }, + "node_modules/entities": { + "version": "4.5.0", + "resolved": "https://registry.npmjs.org/entities/-/entities-4.5.0.tgz", + "integrity": "sha512-V0hjH4dGPh9Ao5p0MoRY6BVqtwCjhz6vI5LT8AJ55H+4g9/4vbHx1I54fS0XuclLhDHArPQCiMjDxjaL8fPxhw==", + "dev": true, + "engines": { + "node": ">=0.12" + }, + "funding": { + "url": "https://github.com/fb55/entities?sponsor=1" + } + }, + "node_modules/error-ex": { + "version": "1.3.2", + "resolved": "https://registry.npmjs.org/error-ex/-/error-ex-1.3.2.tgz", + "integrity": "sha512-7dFHNmqeFSEt2ZBsCriorKnn3Z2pj+fd9kmI6QoWw4//DL+icEBfc0U7qJCisqrTsKTjw4fNFy2pW9OqStD84g==", + "dev": true, + "dependencies": { + "is-arrayish": "^0.2.1" + } + }, "node_modules/es-abstract": { "version": "1.23.3", "resolved": "https://registry.npmjs.org/es-abstract/-/es-abstract-1.23.3.tgz", @@ -1428,6 +4007,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, "node_modules/escape-string-regexp": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", @@ -2041,6 +4629,15 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/gensync": { + "version": "1.0.0-beta.2", + "resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz", + "integrity": "sha512-3hN7NaskYvMDLQY55gnW3NQ+mesEAepTqlg+VEbj7zzqEMBVNhzcGYYeqFo/TlYz6eQiFcp1HcsCZO+nGgS8zg==", + "dev": true, + "engines": { + "node": ">=6.9.0" + } + }, "node_modules/get-intrinsic": { "version": "1.2.4", "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.4.tgz", @@ -2377,6 +4974,12 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/is-arrayish": { + "version": "0.2.1", + "resolved": "https://registry.npmjs.org/is-arrayish/-/is-arrayish-0.2.1.tgz", + "integrity": "sha512-zz06S8t0ozoDXMG+ube26zeCTNXcKIPJZJi8hBrF4idCLms4CG9QtK7qBl1boi5ODzFpjswb5JPmHCbMpjaYzg==", + "dev": true + }, "node_modules/is-async-function": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/is-async-function/-/is-async-function-2.0.0.tgz", @@ -2804,12 +5407,30 @@ "js-yaml": "bin/js-yaml.js" } }, + "node_modules/jsesc": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-2.5.2.tgz", + "integrity": "sha512-OYu7XEzjkCQ3C5Ps3QIZsQfNpqoJyZZA99wd9aWd05NCtC5pWOkShK2mkL6HXQR6/Cy2lbNdPlZBpuQHXE63gA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/json-buffer": { "version": "3.0.1", "resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz", "integrity": "sha512-4bV5BfR2mqfQTJm+V5tPPdf+ZpuhiIvTuAB5g8kcrXOZpTT/QwwVRWBywX1ozr6lEuPdbHxwaJlm9G6mI2sfSQ==", "dev": true }, + "node_modules/json-parse-even-better-errors": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/json-parse-even-better-errors/-/json-parse-even-better-errors-2.3.1.tgz", + "integrity": "sha512-xyFwyhro/JEof6Ghe2iz2NcXoj2sloNsWr/XsERDK/oiPCfaNhl5ONfp+jQdAZRQQ0IJWNzH9zIZF7li91kh2w==", + "dev": true + }, "node_modules/json-schema-traverse": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz", @@ -2917,6 +5538,12 @@ "url": "https://github.com/sponsors/sindresorhus" } }, + "node_modules/lodash.debounce": { + "version": "4.0.8", + "resolved": "https://registry.npmjs.org/lodash.debounce/-/lodash.debounce-4.0.8.tgz", + "integrity": "sha512-FT1yDzDYEoYWhnSGnpE/4Kj1fLZkDFyqRb7fNt6FdYOSxlUWAtp42Eh6Wb0rGIv/m9Bgo7x4GhQbm5Ys4SG5ow==", + "dev": true + }, "node_modules/lodash.merge": { "version": "4.6.2", "resolved": "https://registry.npmjs.org/lodash.merge/-/lodash.merge-4.6.2.tgz", @@ -2934,6 +5561,15 @@ "loose-envify": "cli.js" } }, + "node_modules/lower-case": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/lower-case/-/lower-case-2.0.2.tgz", + "integrity": "sha512-7fm3l3NAF9WfN6W3JOmf5drwpVqX78JtoGJ3A6W0a6ZnldM41w2fV5D490psKFTpMds8TJse/eHLFFsNHHjHgg==", + "dev": true, + "dependencies": { + "tslib": "^2.0.3" + } + }, "node_modules/lru-cache": { "version": "10.2.2", "resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-10.2.2.tgz", @@ -2942,6 +5578,12 @@ "node": "14 || >=16.14" } }, + "node_modules/mdn-data": { + "version": "2.0.30", + "resolved": "https://registry.npmjs.org/mdn-data/-/mdn-data-2.0.30.tgz", + "integrity": "sha512-GaqWWShW4kv/G9IEucWScBx9G1/vsFZZJUO+tD26M8J8z3Kw5RDQjaoZe03YAClgeS/SWPOcb4nkFBTEi5DUEA==", + "dev": true + }, "node_modules/merge2": { "version": "1.4.1", "resolved": "https://registry.npmjs.org/merge2/-/merge2-1.4.1.tgz", @@ -3106,6 +5748,22 @@ "node": "^10 || ^12 || >=14" } }, + "node_modules/no-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/no-case/-/no-case-3.0.4.tgz", + "integrity": "sha512-fgAN3jGAh+RoxUGZHTSOLJIqUc2wmoBwGR4tbpNAKmmovFoWq0OdRkb0VkldReO2a2iBT/OEulG9XSUc10r3zg==", + "dev": true, + "dependencies": { + "lower-case": "^2.0.2", + "tslib": "^2.0.3" + } + }, + "node_modules/node-releases": { + "version": "2.0.14", + "resolved": "https://registry.npmjs.org/node-releases/-/node-releases-2.0.14.tgz", + "integrity": "sha512-y10wOWt8yZpqXmOgRo77WaHEmhYQYGNA6y421PKsKYWEK8aW+cqAphborZDhqfyKrbZEN92CN1X2KbafY2s7Yw==", + "dev": true + }, "node_modules/normalize-path": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", @@ -3114,6 +5772,18 @@ "node": ">=0.10.0" } }, + "node_modules/nth-check": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/nth-check/-/nth-check-2.1.1.tgz", + "integrity": "sha512-lqjrjmaOoAnWfMmBPL+XNnynZh2+swxiX3WUE0s4yEHI6m+AwrK2UZOimIRl3X/4QctVqS8AiZjFqyOGrMXb/w==", + "dev": true, + "dependencies": { + "boolbase": "^1.0.0" + }, + "funding": { + "url": "https://github.com/fb55/nth-check?sponsor=1" + } + }, "node_modules/object-assign": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", @@ -3314,6 +5984,24 @@ "node": ">=6" } }, + "node_modules/parse-json": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/parse-json/-/parse-json-5.2.0.tgz", + "integrity": "sha512-ayCKvm/phCGxOkYRSCM82iDwct8/EonSEgCSxWxD7ve6jHggsFl4fZVQBPRNgQoKiuV/odhFrGzQXZwbifC8Rg==", + "dev": true, + "dependencies": { + "@babel/code-frame": "^7.0.0", + "error-ex": "^1.3.1", + "json-parse-even-better-errors": "^2.3.0", + "lines-and-columns": "^1.1.6" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, "node_modules/path-exists": { "version": "4.0.0", "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", @@ -3668,12 +6356,39 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regenerate": { + "version": "1.4.2", + "resolved": "https://registry.npmjs.org/regenerate/-/regenerate-1.4.2.tgz", + "integrity": "sha512-zrceR/XhGYU/d/opr2EKO7aRHUeiBI8qjtfHqADTwZd6Szfy16la6kqD0MIUs5z5hx6AaKa+PixpPrR289+I0A==", + "dev": true + }, + "node_modules/regenerate-unicode-properties": { + "version": "10.1.1", + "resolved": "https://registry.npmjs.org/regenerate-unicode-properties/-/regenerate-unicode-properties-10.1.1.tgz", + "integrity": "sha512-X007RyZLsCJVVrjgEFVpLUTZwyOZk3oiL75ZcuYjlIWd6rNJtOjkBwQc5AsRrpbKVkxN6sklw/k/9m2jJYOf8Q==", + "dev": true, + "dependencies": { + "regenerate": "^1.4.2" + }, + "engines": { + "node": ">=4" + } + }, "node_modules/regenerator-runtime": { "version": "0.14.1", "resolved": "https://registry.npmjs.org/regenerator-runtime/-/regenerator-runtime-0.14.1.tgz", "integrity": "sha512-dYnhHh0nJoMfnkZs6GmmhFknAGRrLznOu5nc9ML+EJxGvrx6H7teuevqVqCuPcPK//3eDrrjQhehXVx9cnkGdw==", "dev": true }, + "node_modules/regenerator-transform": { + "version": "0.15.2", + "resolved": "https://registry.npmjs.org/regenerator-transform/-/regenerator-transform-0.15.2.tgz", + "integrity": "sha512-hfMp2BoF0qOk3uc5V20ALGDS2ddjQaLrdl7xrGXvAIow7qeWRM2VA2HuCHkUKk9slq3VwEwLNK3DFBqDfPGYtg==", + "dev": true, + "dependencies": { + "@babel/runtime": "^7.8.4" + } + }, "node_modules/regexp.prototype.flags": { "version": "1.5.2", "resolved": "https://registry.npmjs.org/regexp.prototype.flags/-/regexp.prototype.flags-1.5.2.tgz", @@ -3692,6 +6407,44 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/regexpu-core": { + "version": "5.3.2", + "resolved": "https://registry.npmjs.org/regexpu-core/-/regexpu-core-5.3.2.tgz", + "integrity": "sha512-RAM5FlZz+Lhmo7db9L298p2vHP5ZywrVXmVXpmAD9GuL5MPH6t9ROw1iA/wfHkQ76Qe7AaPF0nGuim96/IrQMQ==", + "dev": true, + "dependencies": { + "@babel/regjsgen": "^0.8.0", + "regenerate": "^1.4.2", + "regenerate-unicode-properties": "^10.1.0", + "regjsparser": "^0.9.1", + "unicode-match-property-ecmascript": "^2.0.0", + "unicode-match-property-value-ecmascript": "^2.1.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/regjsparser": { + "version": "0.9.1", + "resolved": "https://registry.npmjs.org/regjsparser/-/regjsparser-0.9.1.tgz", + "integrity": "sha512-dQUtn90WanSNl+7mQKcXAgZxvUe7Z0SqXlgzv0za4LwiUhyzBC58yQO3liFoUgu8GiJVInAhJjkj1N0EtQ5nkQ==", + "dev": true, + "dependencies": { + "jsesc": "~0.5.0" + }, + "bin": { + "regjsparser": "bin/parser" + } + }, + "node_modules/regjsparser/node_modules/jsesc": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/jsesc/-/jsesc-0.5.0.tgz", + "integrity": "sha512-uZz5UnB7u4T9LvwmFqXii7pZSouaRPorGs5who1Ip7VO0wxanFvBL7GkM6dTHlgX+jhBApRetaWpnDabOeTcnA==", + "dev": true, + "bin": { + "jsesc": "bin/jsesc" + } + }, "node_modules/resolve": { "version": "1.22.8", "resolved": "https://registry.npmjs.org/resolve/-/resolve-1.22.8.tgz", @@ -3936,6 +6689,16 @@ "node": ">=8" } }, + "node_modules/snake-case": { + "version": "3.0.4", + "resolved": "https://registry.npmjs.org/snake-case/-/snake-case-3.0.4.tgz", + "integrity": "sha512-LAOh4z89bGQvl9pFfNF8V146i7o7/CqFPbqzYgP+yYzDIDeS9HaNFtXABamRW+AQzEVODcvE79ljJ+8a9YSdMg==", + "dev": true, + "dependencies": { + "dot-case": "^3.0.4", + "tslib": "^2.0.3" + } + }, "node_modules/source-map-js": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/source-map-js/-/source-map-js-1.2.0.tgz", @@ -4197,6 +6960,46 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/svg-parser": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/svg-parser/-/svg-parser-2.0.4.tgz", + "integrity": "sha512-e4hG1hRwoOdRb37cIMSgzNsxyzKfayW6VOflrwvR+/bzrkyxY/31WkbgnQpgtrNp1SdpJvpUAGTa/ZoiPNDuRQ==", + "dev": true + }, + "node_modules/svgo": { + "version": "3.3.2", + "resolved": "https://registry.npmjs.org/svgo/-/svgo-3.3.2.tgz", + "integrity": "sha512-OoohrmuUlBs8B8o6MB2Aevn+pRIH9zDALSR+6hhqVfa6fRwG/Qw9VUMSMW9VNg2CFc/MTIfabtdOVl9ODIJjpw==", + "dev": true, + "dependencies": { + "@trysound/sax": "0.2.0", + "commander": "^7.2.0", + "css-select": "^5.1.0", + "css-tree": "^2.3.1", + "css-what": "^6.1.0", + "csso": "^5.0.5", + "picocolors": "^1.0.0" + }, + "bin": { + "svgo": "bin/svgo" + }, + "engines": { + "node": ">=14.0.0" + }, + "funding": { + "type": "opencollective", + "url": "https://opencollective.com/svgo" + } + }, + "node_modules/svgo/node_modules/commander": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-7.2.0.tgz", + "integrity": "sha512-QrWXB+ZQSVPmIWIhtEO9H+gwHaMGYiF5ChvoJ+K9ZGHG/sVsa6yiesAD1GC/x46sET00Xlwo1u49RVVVzvcSkw==", + "dev": true, + "engines": { + "node": ">= 10" + } + }, "node_modules/tailwindcss": { "version": "3.4.3", "resolved": "https://registry.npmjs.org/tailwindcss/-/tailwindcss-3.4.3.tgz", @@ -4267,6 +7070,15 @@ "node": ">=0.8" } }, + "node_modules/to-fast-properties": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/to-fast-properties/-/to-fast-properties-2.0.0.tgz", + "integrity": "sha512-/OaKK0xYrs3DmxRYqL/yDc+FxFUVYhDlXMhRmv3z915w2HF1tnN1omB354j8VUGO/hbRzyD6Y3sA7v7GS/ceog==", + "dev": true, + "engines": { + "node": ">=4" + } + }, "node_modules/to-regex-range": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", @@ -4438,6 +7250,76 @@ "url": "https://github.com/sponsors/ljharb" } }, + "node_modules/unicode-canonical-property-names-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-canonical-property-names-ecmascript/-/unicode-canonical-property-names-ecmascript-2.0.0.tgz", + "integrity": "sha512-yY5PpDlfVIU5+y/BSCxAJRBIS1Zc2dDG3Ujq+sR0U+JjUevW2JhocOF+soROYDSaAezOzOKuyyixhD6mBknSmQ==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-ecmascript": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-ecmascript/-/unicode-match-property-ecmascript-2.0.0.tgz", + "integrity": "sha512-5kaZCrbp5mmbz5ulBkDkbY0SsPOjKqVS35VpL9ulMPfSl0J0Xsm+9Evphv9CoIZFwre7aJoa94AY6seMKGVN5Q==", + "dev": true, + "dependencies": { + "unicode-canonical-property-names-ecmascript": "^2.0.0", + "unicode-property-aliases-ecmascript": "^2.0.0" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-match-property-value-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-match-property-value-ecmascript/-/unicode-match-property-value-ecmascript-2.1.0.tgz", + "integrity": "sha512-qxkjQt6qjg/mYscYMC0XKRn3Rh0wFPlfxB0xkt9CfyTvpX1Ra0+rAmdX2QyAobptSEvuy4RtpPRui6XkV+8wjA==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/unicode-property-aliases-ecmascript": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/unicode-property-aliases-ecmascript/-/unicode-property-aliases-ecmascript-2.1.0.tgz", + "integrity": "sha512-6t3foTQI9qne+OZoVQB/8x8rk2k1eVy1gRXhV3oFQ5T6R1dqQ1xtin3XqSlx3+ATBkliTaR/hHyJBm+LVPNM8w==", + "dev": true, + "engines": { + "node": ">=4" + } + }, + "node_modules/update-browserslist-db": { + "version": "1.0.16", + "resolved": "https://registry.npmjs.org/update-browserslist-db/-/update-browserslist-db-1.0.16.tgz", + "integrity": "sha512-KVbTxlBYlckhF5wgfyZXTWnMn7MMZjMu9XG8bPlliUOP9ThaF4QnhP8qrjrH7DRzHfSk0oQv1wToW+iA5GajEQ==", + "dev": true, + "funding": [ + { + "type": "opencollective", + "url": "https://opencollective.com/browserslist" + }, + { + "type": "tidelift", + "url": "https://tidelift.com/funding/github/npm/browserslist" + }, + { + "type": "github", + "url": "https://github.com/sponsors/ai" + } + ], + "dependencies": { + "escalade": "^3.1.2", + "picocolors": "^1.0.1" + }, + "bin": { + "update-browserslist-db": "cli.js" + }, + "peerDependencies": { + "browserslist": ">= 4.21.0" + } + }, "node_modules/uri-js": { "version": "4.4.1", "resolved": "https://registry.npmjs.org/uri-js/-/uri-js-4.4.1.tgz", @@ -4647,6 +7529,12 @@ "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", "dev": true }, + "node_modules/yallist": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/yallist/-/yallist-3.1.1.tgz", + "integrity": "sha512-a4UGQaWPH59mOXUYnAG2ewncQS4i4F43Tv3JoAM+s2VDAmS9NsK8GpDMLrCHPksFT7h3K6TOoUNn2pb7RoXx4g==", + "dev": true + }, "node_modules/yaml": { "version": "2.4.2", "resolved": "https://registry.npmjs.org/yaml/-/yaml-2.4.2.tgz", diff --git a/package.json b/package.json index c2cd5f9..b38ca7e 100644 --- a/package.json +++ b/package.json @@ -17,6 +17,7 @@ "react-dom": "^18" }, "devDependencies": { + "@svgr/webpack": "^8.1.0", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", diff --git a/src/app/lib/TypesHelper.js b/src/app/lib/TypesHelper.js new file mode 100644 index 0000000..8077bc8 --- /dev/null +++ b/src/app/lib/TypesHelper.js @@ -0,0 +1,3 @@ +export const isArray = (arg) => { + return Array.isArray(arg) +} \ No newline at end of file diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index b6f73c2..1b0bc04 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -10,7 +10,7 @@ const fetchRequest = async (url, options = {}) => { console.log('jwtDecrypted', jwtDecrypted) const headers = { ...options.headers, - 'Authorization': `Token ${jwtDecrypted.sessionData.token}`, + 'Authorization': jwtDecrypted?.sessionData.token ? `Token ${jwtDecrypted.sessionData.token}` : undefined, 'Content-Type': 'application/json', }; @@ -28,6 +28,7 @@ const fetchRequest = async (url, options = {}) => { } } const data = await response.json() + return { isSuccess: true, errors: null, data: data }; }; diff --git a/src/app/privilege/CreatePrivilegeForm.jsx b/src/app/privilege/CreatePrivilegeForm.jsx new file mode 100644 index 0000000..38ae465 --- /dev/null +++ b/src/app/privilege/CreatePrivilegeForm.jsx @@ -0,0 +1,54 @@ +'use client' +import Loader from '@/components/Loader/Loader' +import React, { useRef, useState } from 'react' +import fetchRequest from '../lib/fetchRequest' + +const CreatePrivilegeForm = ({ appendPrivilege }) => { + const [isLoading, setIsLoading] = useState(false) + const [error, setError] = useState(null) + const [privilegeName, setPrivilegeName] = useState("") + const handlePrivilegeNameChange = (event) => { + setError("") + setPrivilegeName(event.target.value) + } + const inputRef = useRef(null) + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoading(true) + const { data, errors, isSuccess } = await fetchRequest("/privileges/", { + method: "POST", + body: JSON.stringify({ name: privilegeName }) + }) + if (isSuccess) { + setIsLoading(false) + appendPrivilege(data) + inputRef.current.value = "" + setPrivilegeName("") + } else { + setIsLoading(false) + if (errors.type === "ValidationError") { + if (errors.detail.name) { + setError("Le privilège existe déjà") + } + } + console.log(errors) + } + } + return ( +
+

Ajout d'habilitation

+
+ + +
+

{error}

+
+ +
+
+ ) +} + +export default CreatePrivilegeForm \ No newline at end of file diff --git a/src/app/privilege/PrivilegeTableRow.jsx b/src/app/privilege/PrivilegeTableRow.jsx new file mode 100644 index 0000000..13324e6 --- /dev/null +++ b/src/app/privilege/PrivilegeTableRow.jsx @@ -0,0 +1,112 @@ +'use client' +import React, { useEffect, useRef, useState } from 'react' +import fetchRequest from '../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +const PrivilegeTableRow = ({ id, name, setPrivileges }) => { + const [isUpdating, setIsUpdating] = useState(false) + const [privilegeName, setPrivilegeName] = useState(name) + const [loadingStatus, setLoadingStatus] = useState(false) + useEffect(() => { + setPrivilegeName(name) + inputRef.current.value = name + }, [name]) + const handleUpdatePrivilege = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data } = await fetchRequest(`/privileges/${id}/`, { + method: "PATCH", + body: JSON.stringify({ name: privilegeName }) + }) + setLoadingStatus(false) + if (isSuccess) { + setPrivileges((privileges) => privileges.map((element) => element.id === id ? data.data : element)) + setIsUpdating(false) + } else { + // TODO : display the success UI for the UPDATE + console.log(errors) + } + + } + const handleDelete = async () => { + const { isSuccess, errors } = await fetchRequest(`/privileges/${id}/`, { method: "DELETE" }) + if (isSuccess) { + setPrivileges((privileges) => privileges.filter((element) => element.id !== id)) + // TODO : display the success UI for the DELETE + } else { + console.log(errors) + // TODO : display the error alert UI with the error + } + } + const cancelUpdate = () => { + setIsUpdating(false) + setPrivilegeName(name) + inputRef.current.value = name + } + const inputRef = useRef(null) + const rowRef = useRef(null) + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("privilegeRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + useEffect(() => { + if (isUpdating && inputRef?.current) { + inputRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + return ( + + + setPrivilegeName(event.target.value)} defaultValue={name} type='text' className='disabled:bg-white w-full border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + + ) + +} + +export default PrivilegeTableRow \ No newline at end of file diff --git a/src/app/privilege/page.jsx b/src/app/privilege/page.jsx new file mode 100644 index 0000000..eb6376a --- /dev/null +++ b/src/app/privilege/page.jsx @@ -0,0 +1,55 @@ +'use client' +import React, { useEffect, useState } from 'react' +import CreatePrivilegeForm from './CreatePrivilegeForm' +import fetchRequest from '../lib/fetchRequest' +import { isArray } from '../lib/TypesHelper' +import PrivilegeTableRows from './PrivilegeTableRow' +import Loader from '@/components/Loader/Loader' +const Privilege = () => { + const [privileges, setPrivileges] = useState([]) + const [isLoading, setIsLoading] = useState(true) + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest("/privileges") + setIsLoading(false) + if (isSuccess) { + setPrivileges(data) + } else { + console.log(errors) + } + } + getPrivileges() + }, []) + const appendPrivilege = (privilege) => { + setPrivileges((data) => [privilege, ...data]) + } + return ( +
+
+
+ +

List des habilitations

+ {isLoading &&
} + {!isLoading && <> {(!isArray(privileges) || privileges?.length === 0) + ?
+

Pas encore des habilitations

+
+ :
+ + + + + + {privileges.map((element) => { + return + })} +
ProjetAction
+
+ }} +
+
+
+ ) +} + +export default Privilege \ No newline at end of file diff --git a/src/static/image/svg/cancel.svg b/src/static/image/svg/cancel.svg new file mode 100644 index 0000000..1a52a76 --- /dev/null +++ b/src/static/image/svg/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/check.svg b/src/static/image/svg/check.svg new file mode 100644 index 0000000..f169344 --- /dev/null +++ b/src/static/image/svg/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/delete.svg b/src/static/image/svg/delete.svg new file mode 100644 index 0000000..37a3374 --- /dev/null +++ b/src/static/image/svg/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/edit.svg b/src/static/image/svg/edit.svg new file mode 100644 index 0000000..9f6730f --- /dev/null +++ b/src/static/image/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From 10a324dd9342fd35c838b23a42fb3475f9694890 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Thu, 23 May 2024 17:04:11 +0100 Subject: [PATCH 08/79] finished projects front workflow --- src/app/lib/fetchRequest.js | 19 +- src/app/lib/fetchRequestServer.js | 21 +- src/app/projects/ProjectForm.jsx | 117 +++++++--- src/app/projects/ProjectList.jsx | 186 +++++++--------- src/app/projects/SideBar.jsx | 359 +++++++++--------------------- src/app/projects/page.jsx | 119 ++++++++-- src/app/ui/Header.jsx | 2 +- 7 files changed, 407 insertions(+), 416 deletions(-) diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index 0a029d7..a52f45d 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -1,7 +1,8 @@ //custom fetch tha have the token in header import Cookies from 'js-cookie'; -import { decrypt } from "@/app/lib/session"; +import {decrypt} from "@/app/lib/session"; + const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" const fetchRequest = async (url, options = {}) => { const jwtCookie = Cookies.get('session'); @@ -23,13 +24,21 @@ const fetchRequest = async (url, options = {}) => { if (!response.ok) { try { const errorData = await response.json(); - return { isSuccess: false, errors: errorData, data: null } + return {isSuccess: false, errors: errorData, data: null} } catch (error) { - return { isSuccess: false, errors: error, data: null } + return {isSuccess: false, errors: error, data: null} } } - const data = await response.json() - return { isSuccess: true, errors: null, data: data }; + console.log('response', response) + // Check if the response has content before parsing it as JSON + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + return {isSuccess: true, errors: null, data: data}; + } + // If no JSON content, return null for data + return {isSuccess: true, errors: null, data: null}; + }; export default fetchRequest; diff --git a/src/app/lib/fetchRequestServer.js b/src/app/lib/fetchRequestServer.js index ee50e36..16aafe9 100644 --- a/src/app/lib/fetchRequestServer.js +++ b/src/app/lib/fetchRequestServer.js @@ -1,6 +1,6 @@ // src/app/lib/fetchRequestServer.js -import { cookies as nextCookies } from 'next/headers'; -import { decrypt } from "@/app/lib/session"; +import {cookies as nextCookies} from 'next/headers'; +import {decrypt} from "@/app/lib/session"; const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; @@ -9,7 +9,7 @@ const fetchRequestServer = async (url, options = {}) => { const jwtCookie = cookieStore.get('session')?.value; if (!jwtCookie) { - return { isSuccess: false, errors: "No session cookie found", data: null }; + return {isSuccess: false, errors: "No session cookie found", data: null}; } const jwtDecrypted = await decrypt(jwtCookie); @@ -28,14 +28,21 @@ const fetchRequestServer = async (url, options = {}) => { if (!response.ok) { try { const errorData = await response.json(); - return { isSuccess: false, errors: errorData, data: null }; + return {isSuccess: false, errors: errorData, data: null}; } catch (error) { - return { isSuccess: false, errors: error, data: null }; + return {isSuccess: false, errors: error, data: null}; } } - const data = await response.json(); - return { isSuccess: true, errors: null, data: data }; + // Check if the response has content before parsing it as JSON + const contentType = response.headers.get('content-type'); + if (contentType && contentType.includes('application/json')) { + const data = await response.json(); + return {isSuccess: true, errors: null, data: data}; + } + // If no JSON content, return null for data + return {isSuccess: true, errors: null, data: null}; + }; export default fetchRequestServer; diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 418e9b4..c7aa2a4 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -3,37 +3,66 @@ import { useState, useEffect } from 'react'; const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { const [projectName, setProjectName] = useState(''); const [clientName, setClientName] = useState(''); - const [emails, setEmails] = useState(['']); + const [dateDebut, setDateDebut] = useState(''); + const [dateFin, setDateFin] = useState(''); + const [userIds, setUserIds] = useState(['']); + const [errors, setErrors] = useState({}); useEffect(() => { if (editingProject) { - setProjectName(editingProject.projectName); - setClientName(editingProject.clientName); - setEmails(editingProject.emails); + setProjectName(editingProject.nom); + setClientName(editingProject.nomClient); + setDateDebut(editingProject.dateDebut); + setDateFin(editingProject.dateFin); + setUserIds(editingProject.users || []); } else { setProjectName(''); setClientName(''); - setEmails(['']); + setDateDebut(''); + setDateFin(''); + setUserIds(['']); } }, [editingProject]); - const handleEmailChange = (index, value) => { - const newEmails = [...emails]; - newEmails[index] = value; - setEmails(newEmails); + const handleUserIdChange = (index, value) => { + const newUserIds = [...userIds]; + newUserIds[index] = value; + setUserIds(newUserIds); }; - const handleAddEmail = () => { - setEmails([...emails, '']); + const handleAddUserId = () => { + setUserIds([...userIds, '']); }; - const handleRemoveEmail = (index) => { - setEmails(emails.filter((_, i) => i !== index)); + const handleRemoveUserId = (index) => { + setUserIds(userIds.filter((_, i) => i !== index)); + }; + + const validateForm = () => { + const newErrors = {}; + + if (!projectName) newErrors.projectName = "Project name is required"; + if (!clientName) newErrors.clientName = "Client name is required"; + if (!dateDebut) newErrors.dateDebut = "Start date is required"; + // if (!dateFin) newErrors.dateFin = "End date is required"; + if (!userIds.length || userIds.some(id => !id)) newErrors.userIds = "At least one user ID is required"; + + setErrors(newErrors); + return Object.keys(newErrors).length === 0; }; const handleSubmit = (e) => { e.preventDefault(); - const project = { projectName, clientName, emails }; + + if (!validateForm()) return; + + const project = { + nom: projectName, + nomClient: clientName, + dateDebut, + dateFin, + users: userIds.map(id => parseInt(id)), + }; if (editingProject) { onEditProject(editingProject.id, project); @@ -43,38 +72,67 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { }; return ( -
+
- + setProjectName(e.target.value)} - className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required /> + {errors.projectName &&

{errors.projectName}

}
- + setClientName(e.target.value)} - className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required /> + {errors.clientName &&

{errors.clientName}

} +
+
+
+ + setDateDebut(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required + /> + {errors.dateDebut &&

{errors.dateDebut}

} +
+
+ + setDateFin(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + // required + /> + {errors.dateFin &&

{errors.dateFin}

} +
- - {emails.map((email, index) => ( + + {userIds.map((userId, index) => (
handleEmailChange(index, e.target.value)} - className="flex-1 px-3 py-2 bg-white border border-gray-300 rounded-md" + type="text" + value={userId} + onChange={(e) => handleUserIdChange(index, e.target.value)} + className="flex-1 px-3 py-2 bg-white border border-chicago-300 rounded-md" + required /> + {errors.userIds &&

{errors.userIds}

}
+ } ); }; diff --git a/src/app/projects/SideBar.jsx b/src/app/projects/SideBar.jsx index c182af2..35e67e9 100644 --- a/src/app/projects/SideBar.jsx +++ b/src/app/projects/SideBar.jsx @@ -1,7 +1,7 @@ const SideBar = () => { return (
-
+
{
-
- - -
- -
-
- -
-
); diff --git a/src/app/projects/page.jsx b/src/app/projects/page.jsx index 1db5d0a..adba7fe 100644 --- a/src/app/projects/page.jsx +++ b/src/app/projects/page.jsx @@ -1,54 +1,135 @@ 'use client'; import SideBar from "@/app/projects/SideBar"; -import { useState } from 'react'; +import {useEffect, useState} from 'react'; import ProjectForm from "@/app/projects/ProjectForm"; import ProjectList from "@/app/projects/ProjectList"; +import fetchRequest from "@/app/lib/fetchRequest"; + const Projects = () => { + const [pageUrl, setPageUrl] = useState('/projects/'); const [projects, setProjects] = useState([]); const [editingProject, setEditingProject] = useState(null); + {/* errors from request*/} + const [errors, setErrors] = useState(); + const [loading, setLoading] = useState(false); + const fetchProjects = async () => { + const { isSuccess, errors , data } = await fetchRequest(pageUrl); + if (isSuccess) { + setProjects(data); + setErrors(null); + } else { + console.error("Failed to fetch projects"); + setErrors(errors) + } + }; + useEffect( () => { + + fetchProjects(); + }, [pageUrl]); + + // pageURL - const handleAddProject = (project) => { - project.id = projects.length ? projects[projects.length - 1].id + 1 : 1; - setProjects([...projects, project]); - setEditingProject(null); + const handleAddProject = async (project) => { + setLoading(true); + const { isSuccess, errors , data } = await fetchRequest('/projects/', { + method: 'POST', + body: JSON.stringify(project), + }); + + if (isSuccess) { + setProjects([...projects, data]); + setEditingProject(null); + setErrors(null); + } else { + console.error("Failed to add project"); + setErrors(errors) + } + setLoading(false); }; - const handleEditProject = (id, updatedProject) => { - const updatedProjects = projects.map((project) => - project.id === id ? { ...project, ...updatedProject } : project - ); - setProjects(updatedProjects); - setEditingProject(null); + const handleEditProject = async (id, updatedProject) => { + setLoading(true); + const { isSuccess, errors , data } = await fetchRequest(`/projects/${id}/`, { + method: 'PUT', + body: JSON.stringify(updatedProject), + }); + + if (isSuccess) { + const updatedProjects = projects.map((project) => + project.id === id ? data : project + ); + setProjects(updatedProjects); + setEditingProject(null); + setErrors(null); + } else { + console.error("Failed to edit project"); + setErrors(errors) + } + setLoading(false); }; const handleEditClick = (project) => { setEditingProject(project); }; - const handleDeleteProject = (project) => { - setProjects(projects.filter((p) => p.id !== project.id)); - setEditingProject(null); + const handleDeleteProject = async (project) => { + setLoading(true) + const { isSuccess, errors } = await fetchRequest(`/projects/${project.id}/`, { + method: 'DELETE', + }); + + if (isSuccess) { + // setProjects(projects.filter((p) => p.id !== project.id)); + await fetchProjects(); + setEditingProject(null); + setErrors(null); + } else { + console.error("Failed to delete project"); + setErrors(errors) + } + setLoading(false); }; return ( -
-
+
+
-
-
-

Projets

+
+
+

Projets + {loading ? ... : null}

+ {/*errors from request*/} + {errors && errors.detail && Object.keys(errors.detail).map((key) => ( +
+
+ + + + Something went wrong +
+

+ {errors.detail[key]} +

+
+ ))} +
diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx index b9d5df2..1137e74 100644 --- a/src/app/ui/Header.jsx +++ b/src/app/ui/Header.jsx @@ -24,7 +24,7 @@ const Header = async () => {
    {isAuth ? ( <> -
  • +
  • {sessionData.username}

  • {/*
  • */} -- GitLab From 8cb765e8fe54ded231eb4b1cd2b064dd1cd554c9 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Fri, 24 May 2024 11:19:51 +0100 Subject: [PATCH 09/79] added feature/add users to projects by search+ confirmation modal --- src/app/projects/ProjectForm.jsx | 152 ++++++++++++++++++------------- src/app/projects/ProjectList.jsx | 42 +++++++-- src/app/ui/ConfirmationModal.jsx | 30 ++++++ 3 files changed, 153 insertions(+), 71 deletions(-) create mode 100644 src/app/ui/ConfirmationModal.jsx diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index c7aa2a4..350d3d0 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -1,11 +1,14 @@ import { useState, useEffect } from 'react'; +import fetchRequest from '@/app/lib/fetchRequest'; const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { const [projectName, setProjectName] = useState(''); const [clientName, setClientName] = useState(''); const [dateDebut, setDateDebut] = useState(''); const [dateFin, setDateFin] = useState(''); - const [userIds, setUserIds] = useState(['']); + const [userIds, setUserIds] = useState([]); + const [searchQuery, setSearchQuery] = useState(''); + const [userSuggestions, setUserSuggestions] = useState([]); const [errors, setErrors] = useState({}); useEffect(() => { @@ -20,18 +23,28 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { setClientName(''); setDateDebut(''); setDateFin(''); - setUserIds(['']); + setUserIds([]); } }, [editingProject]); - const handleUserIdChange = (index, value) => { - const newUserIds = [...userIds]; - newUserIds[index] = value; - setUserIds(newUserIds); - }; + useEffect(() => { + if (searchQuery.length > 1) { + const fetchUsers = async () => { + const { isSuccess, data } = await fetchRequest(`/search-users/?q=${searchQuery}`); + if (isSuccess) { + setUserSuggestions(data); + } + }; + fetchUsers(); + } else { + setUserSuggestions([]); + } + }, [searchQuery]); - const handleAddUserId = () => { - setUserIds([...userIds, '']); + const handleUserSelect = (user) => { + setUserIds([...userIds, user]); + setSearchQuery(''); + setUserSuggestions([]); }; const handleRemoveUserId = (index) => { @@ -40,12 +53,10 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { const validateForm = () => { const newErrors = {}; - if (!projectName) newErrors.projectName = "Project name is required"; if (!clientName) newErrors.clientName = "Client name is required"; if (!dateDebut) newErrors.dateDebut = "Start date is required"; - // if (!dateFin) newErrors.dateFin = "End date is required"; - if (!userIds.length || userIds.some(id => !id)) newErrors.userIds = "At least one user ID is required"; + if (!userIds.length) newErrors.userIds = "At least one user is required"; setErrors(newErrors); return Object.keys(newErrors).length === 0; @@ -61,7 +72,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { nomClient: clientName, dateDebut, dateFin, - users: userIds.map(id => parseInt(id)), + users: userIds.map(user => user.id), }; if (editingProject) { @@ -71,9 +82,13 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { } }; + const isUserChosen = (user) => { + return userIds.some(chosenUser => chosenUser.id === user.id); + }; + return (
    -
    +
    { /> {errors.projectName &&

    {errors.projectName}

    }
    -
    +
    { {errors.clientName &&

    {errors.clientName}

    }
    -
    - - setDateDebut(e.target.value)} - className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" - required - /> - {errors.dateDebut &&

    {errors.dateDebut}

    } +
    + + setDateDebut(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + required + /> + {errors.dateDebut &&

    {errors.dateDebut}

    } +
    +
    + + setDateFin(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" + /> + {errors.dateFin &&

    {errors.dateFin}

    } +
    -
    - +
    + setDateFin(e.target.value)} + type="text" + value={searchQuery} + onChange={(e) => setSearchQuery(e.target.value)} + placeholder="Type to search users..." className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" - // required /> - {errors.dateFin &&

    {errors.dateFin}

    } -
    -
    -
    - - {userIds.map((userId, index) => ( -
    - handleUserIdChange(index, e.target.value)} - className="flex-1 px-3 py-2 bg-white border border-chicago-300 rounded-md" - required - /> - -
    - ))} - + {userSuggestions.length > 0 && ( +
      + {userSuggestions.map(user => ( +
    • !isUserChosen(user) && handleUserSelect(user)} + className={`px-3 py-2 border-b border-chicago-200 cursor-pointer ${isUserChosen(user) ? 'bg-gray-200 cursor-not-allowed' : 'hover:bg-sushi-200'}`} + > + {/*add tick is chosen*/} + {isUserChosen(user) && '✔ '} + {user.username} ({user.email}) {isUserChosen(user) && '(selectionné)'} +
    • + ))} +
    + )} +
    + {userIds.map((user, index) => ( +
    + + +
    + ))} +
    {errors.userIds &&

    {errors.userIds}

    }
    + } +
    +
    +
+ + ) +} + +export default Zone \ No newline at end of file diff --git a/src/static/image/svg/cancel.svg b/src/static/image/svg/cancel.svg new file mode 100644 index 0000000..1a52a76 --- /dev/null +++ b/src/static/image/svg/cancel.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/check.svg b/src/static/image/svg/check.svg new file mode 100644 index 0000000..f169344 --- /dev/null +++ b/src/static/image/svg/check.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/delete.svg b/src/static/image/svg/delete.svg new file mode 100644 index 0000000..37a3374 --- /dev/null +++ b/src/static/image/svg/delete.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/edit.svg b/src/static/image/svg/edit.svg new file mode 100644 index 0000000..9f6730f --- /dev/null +++ b/src/static/image/svg/edit.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From 5bffa9a640ce00ba1724383f3c25dddca77e1e30 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Fri, 24 May 2024 12:55:23 +0100 Subject: [PATCH 13/79] fix: reset password and change password exceptions --- src/app/auth/change-password/page.jsx | 12 +++++++++--- src/app/auth/forgot-password/page.jsx | 11 +++++++++-- src/app/role/CreateRoleForm.jsx | 1 + src/app/role/page.jsx | 2 +- 4 files changed, 20 insertions(+), 6 deletions(-) diff --git a/src/app/auth/change-password/page.jsx b/src/app/auth/change-password/page.jsx index a89f484..6bc46ca 100644 --- a/src/app/auth/change-password/page.jsx +++ b/src/app/auth/change-password/page.jsx @@ -6,6 +6,7 @@ import React, { useEffect, useState } from 'react' import { useSearchParams } from 'next/navigation' import Loader from '@/components/Loader/Loader' import { PASSWORD_REGEX } from '@/app/lib/constants' +import { useNotification } from '@/context/NotificationContext' const ChangePassword = () => { const [isSuccess, setIsSuccess] = useState(false) @@ -14,6 +15,7 @@ const ChangePassword = () => { const [confirmPassword, setConfirmPassword] = useState("") const [isLoading, setIsLoading] = useState(false) const params = useSearchParams(); + const { toggleNotification } = useNotification() const handleChangePassword = async (event) => { event.preventDefault() setIsLoading(true) @@ -30,18 +32,22 @@ const ChangePassword = () => { else { console.log(errors) setIsLoading(false) - if (errors.type === "Validation Error") { + if (errors.type === "ValidationError") { if (errors.detail.token) { setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est invalide"]) } else { setFormErrors(["Le Mot de passe est invalide"]) } - } else if (errors?.detail?.startsWith("The OTP password entered is not valid")) { + } else if (errors?.detail?.detail?.startsWith("The OTP password entered is not valid")) { setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est déja utilisé"]) } else { - console.log("Internal Server Error") + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) } } } diff --git a/src/app/auth/forgot-password/page.jsx b/src/app/auth/forgot-password/page.jsx index 3895740..d74d5db 100644 --- a/src/app/auth/forgot-password/page.jsx +++ b/src/app/auth/forgot-password/page.jsx @@ -1,6 +1,7 @@ 'use client' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' import Image from 'next/image' import Link from 'next/link' import React, { useState } from 'react' @@ -10,6 +11,7 @@ const ForgotPassword = () => { const [isSuccess, setIsSuccess] = useState(false) const [isLoading, setIsLoading] = useState(false) const [requestErrors, setRequestErrors] = useState([]) + const { toggleNotification } = useNotification() const handleForgetPassword = async (event) => { event.preventDefault() setIsLoading(true) @@ -22,13 +24,18 @@ const ForgotPassword = () => { } else { setIsLoading(false) - if (errors.type === "Validation Error") { + if (errors.type === "ValidationError") { // setRequestErrors(Object.keys(errors.detail).reduce((prev, current) => { // return [...prev, ...errors.detail[current]] // }, [])) + toggleNotification setRequestErrors(["Nous n'avons pas pu trouver de compte associé à cet e-mail. Veuillez essayer une autre adresse e-mail."]) } else { - console.log("Internal server error") + console.log(errors) + toggleNotification({ + type: "error", + message: "Internal Server Error" + }) } } } diff --git a/src/app/role/CreateRoleForm.jsx b/src/app/role/CreateRoleForm.jsx index ea823b7..29e177c 100644 --- a/src/app/role/CreateRoleForm.jsx +++ b/src/app/role/CreateRoleForm.jsx @@ -109,6 +109,7 @@ const CreateRoleForm = ({ appendRole }) => {
{!isAllSelected ? "Sélectionner tout" : "désélectionner"}
+
{privileges?.map((privilege) => { diff --git a/src/app/role/page.jsx b/src/app/role/page.jsx index 1e77b42..78c99ed 100644 --- a/src/app/role/page.jsx +++ b/src/app/role/page.jsx @@ -44,7 +44,7 @@ const Role = () => { :
- + {roles.map((element) => { -- GitLab From 3ac2dc65a948f70bbfc428bd8b2cce4504fa2ed3 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Fri, 24 May 2024 15:08:14 +0100 Subject: [PATCH 14/79] fixed createprivilegeform --- src/app/privilege/CreatePrivilegeForm.jsx | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/privilege/CreatePrivilegeForm.jsx b/src/app/privilege/CreatePrivilegeForm.jsx index 1aefd65..d124e2e 100644 --- a/src/app/privilege/CreatePrivilegeForm.jsx +++ b/src/app/privilege/CreatePrivilegeForm.jsx @@ -2,11 +2,13 @@ import Loader from '@/components/Loader/Loader' import React, { useRef, useState } from 'react' import fetchRequest from '../lib/fetchRequest' +import {useNotification} from "@/context/NotificationContext"; const CreatePrivilegeForm = ({ appendPrivilege }) => { const [isLoading, setIsLoading] = useState(false) const [error, setError] = useState(null) - const [privilegeName, setPrivilegeName] = useState("") + const [privilegeName, setPrivilegeName] = useState(""); + const { toggleNotification } = useNotification() const handlePrivilegeNameChange = (event) => { setError("") setPrivilegeName(event.target.value) -- GitLab From 2aa245e61e05e5b7044e17e151887a3e9d780291 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Fri, 24 May 2024 17:33:13 +0100 Subject: [PATCH 15/79] feat: add update role --- src/app/privilege/PrivilegeTableRow.jsx | 68 ++++++----- src/app/role/CreateRoleForm.jsx | 72 ++++++----- src/app/role/RoleTableRows.jsx | 151 ++++++------------------ src/app/role/UpdateRoleForm.jsx | 144 ++++++++++++++++++++++ src/app/role/page.jsx | 24 ++-- src/static/image/svg/add.svg | 1 + 6 files changed, 282 insertions(+), 178 deletions(-) create mode 100644 src/app/role/UpdateRoleForm.jsx create mode 100644 src/static/image/svg/add.svg diff --git a/src/app/privilege/PrivilegeTableRow.jsx b/src/app/privilege/PrivilegeTableRow.jsx index 96f1d1d..7537e45 100644 --- a/src/app/privilege/PrivilegeTableRow.jsx +++ b/src/app/privilege/PrivilegeTableRow.jsx @@ -7,11 +7,16 @@ import EditIcon from "@/static/image/svg/edit.svg" import CancelIcon from "@/static/image/svg/cancel.svg" import CheckIcon from "@/static/image/svg/check.svg" import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '../ui/ConfirmationModal' const PrivilegeTableRow = ({ id, name, setPrivileges }) => { const [isUpdating, setIsUpdating] = useState(false) const [privilegeName, setPrivilegeName] = useState(name) const [loadingStatus, setLoadingStatus] = useState(false) const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false) + const showDeletePopup = () => { + setModalOpen(true); + } useEffect(() => { setPrivilegeName(name) inputRef.current.value = name @@ -87,6 +92,7 @@ const PrivilegeTableRow = ({ id, name, setPrivileges }) => { type: "error" }) } + setModalOpen(false) } const cancelUpdate = () => { setIsUpdating(false) @@ -126,33 +132,41 @@ const PrivilegeTableRow = ({ id, name, setPrivileges }) => { } }, [isUpdating]) return ( - - - - + <> + + + + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer l'habilitation ?`} + /> + ) } diff --git a/src/app/role/CreateRoleForm.jsx b/src/app/role/CreateRoleForm.jsx index 29e177c..546c0c0 100644 --- a/src/app/role/CreateRoleForm.jsx +++ b/src/app/role/CreateRoleForm.jsx @@ -2,10 +2,9 @@ import Loader from '@/components/Loader/Loader' import React, { useState, useRef, useEffect, useMemo } from 'react' import fetchRequest from '../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' - -const CreateRoleForm = ({ appendRole }) => { +import CancelIcon from "@/static/image/svg/cancel.svg" +const CreateRoleForm = ({ appendRole, setIsOpen }) => { const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) const [roleName, setRoleName] = useState("") const [privileges, setPrivileges] = useState(null) const [selectedPrivileges, setSelectedPrivileges] = useState([]) @@ -28,7 +27,6 @@ const CreateRoleForm = ({ appendRole }) => { getPrivileges() }, []) const handleRoleNameChange = (event) => { - setError("") setRoleName(event.target.value) } const inputRef = useRef(null) @@ -50,6 +48,7 @@ const CreateRoleForm = ({ appendRole }) => { message: "Le rôle a été créé avec succès", type: "success" }) + setIsOpen(false) } else { setIsLoading(false) if (errors.type === "ValidationError") { @@ -66,6 +65,7 @@ const CreateRoleForm = ({ appendRole }) => { message: "Erreur de validation de rôle", type: "warning" }) + setIsOpen(false) } } else if (status === 409) { @@ -74,12 +74,21 @@ const CreateRoleForm = ({ appendRole }) => { message: "Roles created with 2 or more same privileges", type: "error" }) + setIsOpen(false) + } else if (errors.detail === "Privilege matching query does not exist.") { + toggleNotification({ + visible: true, + message: "Des privilèges que vous avez utilisés ont été supprimés.", + type: "warning" + }) + setIsOpen(false) } else { toggleNotification({ visible: true, message: "Internal Server Error", type: "error" }) + setIsOpen(false) } console.log(errors) } @@ -98,32 +107,37 @@ const CreateRoleForm = ({ appendRole }) => { } var isAllSelected = useMemo(() => privileges ? privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id)) : false, [selectedPrivileges, privileges]) return ( -
-

Ajout de Rôle

-
- - -
-

{error}

- {(privileges) ?
-
- -
{!isAllSelected ? "Sélectionner tout" : "désélectionner"}
- -
-
- {privileges?.map((privilege) => { - const isSelected = selectedPrivileges.find((element) => element.id === privilege.id) !== undefined - return
handlePrivilegeClick(privilege)} key={privilege.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}>{privilege.name}
- })} -
-
:
} -
- +
+
+ setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> + +

Ajout de Rôle

+
+ + +
+ {(privileges) ?
+
+ +
{!isAllSelected ? "Sélectionner tout" : "désélectionner"}
+
+
+ {privileges.length !== 0 ? privileges?.map((privilege) => { + const isSelected = selectedPrivileges.find((element) => element.id === privilege.id) !== undefined + return
handlePrivilegeClick(privilege)} key={privilege.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}>{privilege.name}
+ }) :
+

Pas encore des habilitations

+
} +
+
:
} +
+ +
+
- +
) } diff --git a/src/app/role/RoleTableRows.jsx b/src/app/role/RoleTableRows.jsx index b6c4ea8..43444b5 100644 --- a/src/app/role/RoleTableRows.jsx +++ b/src/app/role/RoleTableRows.jsx @@ -1,67 +1,19 @@ import React, { useEffect, useRef, useState } from 'react' import fetchRequest from '../lib/fetchRequest' -import Loader from '@/components/Loader/Loader' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" -import CancelIcon from "@/static/image/svg/cancel.svg" -import CheckIcon from "@/static/image/svg/check.svg" import { useNotification } from '@/context/NotificationContext' -const RoleTableRows = ({ name, setRoles, id, privileges }) => { - const [isUpdating, setIsUpdating] = useState(false) - const [roleName, setRoleName] = useState(name) - const [loadingStatus, setLoadingStatus] = useState(false) +import ConfirmationModal from '../ui/ConfirmationModal' + +const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false); + const inputRef = useRef(null) useEffect(() => { - setRoleName(name) inputRef.current.value = name }, [name]) - const handleUpdatePrivilege = async () => { - setLoadingStatus(true) - const { isSuccess, errors, data, status } = await fetchRequest(`/roles/${id}/`, { - method: "PATCH", - body: JSON.stringify({ name: roleName }) - }) - setLoadingStatus(false) - if (isSuccess) { - setRoles((roles) => roles.map((element) => element.id === id ? data.data : element)) - setIsUpdating(false) - toggleNotification({ - visible: true, - message: "Le rôle a été modifé avec succès", - type: "success" - }) - } else { - if (errors.type === "ValidationError") { - if (errors.detail.name) { - toggleNotification({ - type: "warning", - message: "Le nom de rôle existe déjà", - visible: true, - }) - } - else { - toggleNotification({ - visible: true, - message: "Erreur de validation de rôle", - type: "warning" - }) - } - } else if (status === 404) { - toggleNotification({ - visible: true, - message: "Le rôle n'a pas été trouvé", - type: "warning" - }) - } else { - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - } - console.log(errors) - } - + const showDeletePopup = () => { + setModalOpen(true); } const handleDelete = async () => { const { isSuccess, errors, status } = await fetchRequest(`/roles/${id}/`, { method: "DELETE" }) @@ -78,7 +30,14 @@ const RoleTableRows = ({ name, setRoles, id, privileges }) => { message: "Le rôle n'a pas été trouvé", type: "warning" }) - } else { + } else if (errors.detail?.indexOf("Cannot delete some instances of model 'Role'") !== -1) { + toggleNotification({ + visible: true, + message: "Impossible de supprimer ce rôle car il est attribué à des utilisateurs", + type: "warning" + }) + } + else { console.log(errors) toggleNotification({ visible: true, @@ -86,73 +45,35 @@ const RoleTableRows = ({ name, setRoles, id, privileges }) => { type: "error" }) } + setModalOpen(false) console.log(errors) } - const cancelUpdate = () => { - setIsUpdating(false) - setRoleName(name) - inputRef.current.value = name - } - const inputRef = useRef(null) - const rowRef = useRef(null) - const handleUpdateBlur = (event) => { - const eventTarget = event.target - let isInsideRowRef = false; - let element = eventTarget; - while (element !== null) { - if (element === rowRef.current) { - isInsideRowRef = true; - break; - } - if (element.parentElement === null) { - isInsideRowRef = false; - break; - } - element = element.parentElement; - } - if (!isInsideRowRef && element?.classList.contains("privilegeRowSVG")) return; - if (!isInsideRowRef) { - cancelUpdate(); - document.removeEventListener("click", handleUpdateBlur); - } - } - useEffect(() => { - if (isUpdating && inputRef?.current) { - inputRef.current.focus() - document.addEventListener("click", handleUpdateBlur) - } - return () => { - document.removeEventListener("click", handleUpdateBlur) - } - }, [isUpdating]) + return ( -
- - + + - + + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer le rôle ?`} + /> + + ) } diff --git a/src/app/role/UpdateRoleForm.jsx b/src/app/role/UpdateRoleForm.jsx new file mode 100644 index 0000000..7bf368b --- /dev/null +++ b/src/app/role/UpdateRoleForm.jsx @@ -0,0 +1,144 @@ +import { useNotification } from '@/context/NotificationContext' +import React, { useState, useRef, useEffect, useMemo } from 'react' +import fetchRequest from '../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import CancelIcon from "@/static/image/svg/cancel.svg" +import { isArray } from '../lib/TypesHelper' +const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePrivileges, name, id }) => { + const { toggleNotification } = useNotification() + const [loadingStatus, setLoadingStatus] = useState(false) + const [roleName, setRoleName] = useState(name) + const [privileges, setPrivileges] = useState(null) + const [selectedPrivileges, setSelectedPrivileges] = useState(isArray(rolePrivileges) ? rolePrivileges : []) + const inputRef = useRef(null) + console.log("les priv de role ", rolePrivileges) + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest("/privileges") + if (isSuccess) { + setPrivileges(data) + } else { + setPrivileges([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getPrivileges() + }, []) + const handleSubmit = async (event) => { + event.preventDefault() + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/roles/${id}/`, { + method: "PATCH", + //.filter((element) => privileges.find((prvElement) => prvElement.id === element.id)) + body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.map((element) => element.id) }) + }) + console.log(data) + setLoadingStatus(false) + if (isSuccess) { + setRoles((roles) => roles.map((element) => element.id === id ? data : element)) + toggleNotification({ + visible: true, + message: "Le rôle a été modifé avec succès", + type: "success" + }) + setRoleToUpdate(null) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le nom de rôle existe déjà", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle", + type: "warning" + }) + setRoleToUpdate(null) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "Le rôle n'a pas été trouvé", + type: "warning" + }) + setRoleToUpdate(null) + } else if (errors.detail === "Privilege matching query does not exist.") { + toggleNotification({ + visible: true, + message: "Des privilèges que vous avez utilisés ont été supprimés.", + type: "warning" + }) + setRoleToUpdate(null) + } + else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setRoleToUpdate(null) + } + console.log(errors) + } + } + const handleRoleNameChange = (event) => { + setRoleName(event.target.value) + } + const handlePrivilegeClick = (privilege) => { + if (selectedPrivileges.find((element) => element.id === privilege.id)) { + setSelectedPrivileges(selectedPrivileges.filter((element) => element.id !== privilege.id)) + } else { + setSelectedPrivileges([...selectedPrivileges, privilege]) + } + } + const selectAll = () => { + if (privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id))) + setSelectedPrivileges([]) + else setSelectedPrivileges(privileges) + } + var isAllSelected = useMemo(() => privileges ? privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id)) : false, [selectedPrivileges, privileges]) + return ( +
+
+ setRoleToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
+

Modification de Rôle

+
+ + +
+ {(privileges) ?
+
+ +
{!isAllSelected ? "Sélectionner tout" : "désélectionner"}
+
+
+ {privileges.length !== 0 ? privileges?.map((privilege) => { + const isSelected = selectedPrivileges.find((element) => element.id === privilege.id) !== undefined + return
handlePrivilegeClick(privilege)} key={privilege.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}>{privilege.name}
+ }) :
+

Pas encore des habilitations

+
} +
+
:
} +
+ +
+ +
+
+ ) +} + +export default UpdateRoleForm \ No newline at end of file diff --git a/src/app/role/page.jsx b/src/app/role/page.jsx index 78c99ed..a2c5f38 100644 --- a/src/app/role/page.jsx +++ b/src/app/role/page.jsx @@ -5,10 +5,13 @@ import Loader from '@/components/Loader/Loader' import RoleTableRows from './RoleTableRows' import fetchRequest from '../lib/fetchRequest' import { isArray } from '../lib/TypesHelper' - +import AddIcon from "@/static/image/svg/add.svg" +import UpdateRoleForm from './UpdateRoleForm' const Role = () => { const [roles, setRoles] = useState([]) const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) + const [roleToUpdate, setRoleToUpdate] = useState(null) useEffect(() => { const getRoles = async () => { const { data, errors, isSuccess } = await fetchRequest("/roles") @@ -34,21 +37,28 @@ const Role = () => {
- -

List des Roles

+ {openCreatePopup && } + {roleToUpdate && } +
+

List des Roles

+ +
{isLoading &&
} {!isLoading && <> {(!isArray(roles) || roles?.length === 0) ?

Pas encore des roles

- :
+ :
HabilitationRôle Action
- setPrivilegeName(event.target.value)} defaultValue={name} type='text' className='disabled:bg-white w-full border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - - {!isUpdating - ?
- - -
- :
- - -
- } -
+ setPrivilegeName(event.target.value)} defaultValue={name} type='text' className='disabled:bg-white w-full border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } +
- setRoleName(event.target.value)} defaultValue={name} type='text' className='disabled:bg-white w-full border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - - {!isUpdating - ?
-
+ + +
+ -
- :
- - -
- } -
- + - {roles.map((element) => { - return + {roles?.map((element) => { + return })}
Rôlerôle Action
diff --git a/src/static/image/svg/add.svg b/src/static/image/svg/add.svg new file mode 100644 index 0000000..37eaf78 --- /dev/null +++ b/src/static/image/svg/add.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From 24876e59ff8ff43ceaa78fff0a63b8cf244d9176 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Fri, 24 May 2024 19:41:48 +0100 Subject: [PATCH 16/79] zoaning completed etage/zone --- src/app/etage/AddEtageComponent.jsx | 24 +++- src/app/etage/page.jsx | 45 +++++- src/app/zone/CreateNewZone.jsx | 69 ++++----- src/app/zone/RowZone.jsx | 210 +++++++++++++++++++++++++--- src/app/zone/page.jsx | 69 ++++++--- 5 files changed, 333 insertions(+), 84 deletions(-) diff --git a/src/app/etage/AddEtageComponent.jsx b/src/app/etage/AddEtageComponent.jsx index d84ae41..17e46b8 100644 --- a/src/app/etage/AddEtageComponent.jsx +++ b/src/app/etage/AddEtageComponent.jsx @@ -2,18 +2,20 @@ import Loader from '@/components/Loader/Loader' import React, { useState, useRef } from 'react' import fetchRequest from '../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' + const AddEtageComponent = ({ etagesState })=> { const [ numeroEtage, setNumeroEtage ] = useState("") const [isLoading, setIsLoading] = useState(false) - const [errorMessage, setErrorMessage] = useState(null) const inputRef = useRef(null) + const { toggleNotification } = useNotification() + const handleNewEtage = (e) => { - setErrorMessage(null) setNumeroEtage(e.target.value) } @@ -28,12 +30,25 @@ const AddEtageComponent = ({ etagesState })=> { inputRef.current.value = "" console.log(data) etagesState((prevEtagesState) => [...prevEtagesState, data]); + toggleNotification({ + type: "success", + message: "L'étage a été créer avec succès.", + visible: true, + }) } else { setIsLoading(false) if(errors.type == "ValidationError") - {setErrorMessage(errors.detail.numero)} + toggleNotification({ + type: "warning", + message: "Le numéro détage déja existe.", + visible: true, + }) else{ - setErrorMessage(errorMessage.type) + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la zone.", + visible: true, + }) } console.log(errors) } @@ -49,7 +64,6 @@ const AddEtageComponent = ({ etagesState })=> {
- {errorMessage && {errorMessage}}
) diff --git a/src/app/etage/page.jsx b/src/app/etage/page.jsx index 5177625..94f79ae 100644 --- a/src/app/etage/page.jsx +++ b/src/app/etage/page.jsx @@ -4,12 +4,20 @@ import AddEtageComponent from './AddEtageComponent' import fetchRequest from '../lib/fetchRequest' import { useState, useEffect } from 'react'; import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + const Etage = ()=> { const [etages, setEtages] = useState([]) const [ isLoadingData, setIsLoadingData ] = useState(true) + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false); + const [etageToDelete, setEtageToDelete] = useState(null); + + // Fetch data from external API useEffect(() => { const getAllEtages = async () => { @@ -39,11 +47,38 @@ const Etage = ()=> { console.log(etages) console.log("etage: ", etage) setEtages((prevEtages) => prevEtages.filter((e) => e.id !== etage.id)); + toggleNotification({ + type: "success", + message: "L'étage a été supprimer avec succès.", + visible: true, + }) + }else{ + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la suppression de l'étage.", + visible: true, + }) } }catch(error){ - console.log(error) + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true, + }) } } + + const handleDeleteClick = (etage) => { + setEtageToDelete(etage); + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDeleteEtage(etageToDelete); + setModalOpen(false); + setProjectToDelete(null); + }; + return(
@@ -56,7 +91,7 @@ const Etage = ()=> { (
  • Etage numéro: {etage.numero} -
    {handleDeleteEtage(etage)}}> +
    {handleDeleteClick(etage)}}>
  • @@ -75,6 +110,12 @@ const Etage = ()=> {
    + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer l'étage numéro "${etageToDelete?.numero}"?`} + />
    ) diff --git a/src/app/zone/CreateNewZone.jsx b/src/app/zone/CreateNewZone.jsx index cdfb8cb..d02c3f5 100644 --- a/src/app/zone/CreateNewZone.jsx +++ b/src/app/zone/CreateNewZone.jsx @@ -3,54 +3,56 @@ import React, { useState, useEffect, useRef } from 'react' import fetchRequest from '../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { Island_Moments } from 'next/font/google' +import { useNotification } from '@/context/NotificationContext' -const CreateNewZone = () => { - const [etages, setEtages] = useState([]) + +const CreateNewZone = ({ zoneState, etages }) => { const [error, setError] = useState(null) - const [isLoadingEtages, setIsLoadingEtages] = useState(true) const [isLoadingAction, setIsLoadingAction] = useState(false) - const [nomZone, setNomZone] = useState("") + const [nomZone, setNomZone] = useState(null) const [selectedEtage, setSelectedEtage] = useState(null) const inputRef = useRef(null) const selectRef = useRef(null) - useEffect(() => { - const getAllEtages = async () => { - try{ - const {isSuccess, errors, data} = await fetchRequest('/zoaning/etages/', {method: 'GET'}) - setIsLoadingEtages(false) - if(isSuccess){ - setEtages(data) - }else{ - setEtages([]) - } - }catch(error){ - setIsLoadingEtages(false) - console.log(error) - } - } - getAllEtages() - }, []) + const { toggleNotification } = useNotification() + const handleSubmit = async (event) => { event.preventDefault() - isLoadingAction(true) - const { data, errors, isSuccess } = await fetchRequest("/zoning/zones/", { + setIsLoadingAction(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/zones/", { method: "POST", - body: JSON.stringify({ name: nomZone }) + body: JSON.stringify({ nom: nomZone, id_etage: selectedEtage }) }) if (isSuccess) { - isLoadingAction(false) - appendPrivilege(data) + setIsLoadingAction(false) + zoneState((prevZoneValue) => [...prevZoneValue, {...data, id_etage: etages.find(etage => etage.id === data.id_etage)}]); inputRef.current.value = "" - setPrivilegeName("") + selectRef.current.value = "" + setNomZone(null) + setSelectedEtage(null) + toggleNotification({ + visible: true, + message: "La zone a été créer avec succès.", + type: "success" + }) } else { - isLoadingAction(false) + setIsLoadingAction(false) if (errors.type === "ValidationError") { - if (errors.detail.name) { - setError("Le privilège existe déjà") + if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone saisie existe déjà dans l'étage sélectionné.", + visible: true, + }) } + }else{ + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la zone.", + visible: true, + }) } console.log(errors) } @@ -67,6 +69,9 @@ const CreateNewZone = () => { setSelectedEtage(event.target.value) } + console.log(selectedEtage) + console.log(nomZone) + return( @@ -79,7 +84,7 @@ const CreateNewZone = () => {
    - {(etages && etages?.length) && etages.map((etage, index) => ( @@ -91,7 +96,7 @@ const CreateNewZone = () => {

    -
    diff --git a/src/app/zone/RowZone.jsx b/src/app/zone/RowZone.jsx index 1d71ca2..5ef8172 100644 --- a/src/app/zone/RowZone.jsx +++ b/src/app/zone/RowZone.jsx @@ -1,47 +1,213 @@ "use client" -import React from 'react' +import React, { useState, useEffect, useRef } from 'react'; import fetchRequest from '../lib/fetchRequest' -import { useState, useEffect } from 'react'; import Loader from '@/components/Loader/Loader' -import CreateNewZone from './CreateNewZone' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" import CancelIcon from "@/static/image/svg/cancel.svg" import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; -const RowZone = ({ id, nom, etage }) => { + + +const RowZone = ({ id, nom, etage, zonesState, etages }) => { + //states + const [isUpdating, setIsUpdating] = useState(false) + const [zoneName, setZoneName] = useState(nom) + const [selectedEtage, setSelectedEtage] = useState(etage) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRef = useRef(null) + const rowRef = useRef(null) + + //Logic + useEffect(() => { + setZoneName(nom) + setSelectedEtage(etage?.id) + selectRef.current.value = etage?.id + inputRef.current.value = nom + }, [nom, etage]) + + const handleUpdateZone = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/zones/${id}/`, { + method: "PATCH", + body: JSON.stringify({ nom: zoneName, id_etage: selectedEtage }) + }) + setLoadingStatus(false) + if (isSuccess) { + console.log(data.data) + zonesState((prevZonesValue) => prevZonesValue.map( (element) => element.id === id ? {...data.data, id_etage: etages.find(etage => etage.id === data.data.id_etage)} : element )) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "La zone a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone existe déjà", + visible: true, + }) + }else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone saisie existe déjà dans l'étage sélectionné.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la zone", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "La zone n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/zoaning/zones/${id}/`, { method: "DELETE" }) + if (isSuccess) { + zonesState((prevZonesState) => prevZonesState.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "La zone a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "La zone n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const cancelUpdate = () => { + setIsUpdating(false) + setZoneName(nom) + setSelectedEtage(etage.id) + selectRef.current.value = etage.id + inputRef.current.value = nom + } + + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("zoneRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + + useEffect(() => { + if (isUpdating && inputRef?.current && selectRef?.current) { + inputRef.current.focus() + selectRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + return( - + - + setZoneName(event.target.value)} defaultValue={nom} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - setSelectedEtage(event.target.value)} name="listEtage" id="listEtage" className="disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none"> + {(etages && etages?.length) && + etages.map((element,index) => ( + + )) + } -
    - -
    -
    - - -
    +
    + } + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer la zone "${nom}"?`} + /> ) } diff --git a/src/app/zone/page.jsx b/src/app/zone/page.jsx index a1c067e..5fc1e53 100644 --- a/src/app/zone/page.jsx +++ b/src/app/zone/page.jsx @@ -8,54 +8,77 @@ import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" import CancelIcon from "@/static/image/svg/cancel.svg" import CheckIcon from "@/static/image/svg/check.svg" +import { isArray } from '../lib/TypesHelper' import RowZone from './RowZone' const Zone = ()=> { - const [zones, setZones] = useState([]) + const [ zones, setZones ] = useState([]) const [ isLoadingData, setIsLoadingData ] = useState(true) + const [etages, setEtages] = useState([]) + + // Fetch data from external API useEffect(() => { const getAllZones = async () => { try{ const {isSuccess, errors, data} = await fetchRequest('/zoaning/zones/', {method: 'GET'}) - setIsLoadingData(false) if(isSuccess){ setZones(data) }else{ setZones([]) } }catch(error){ - setIsLoadingData(false) + console.log(error) + } + } + const getAllEtages = async () => { + try{ + const {isSuccess, errors, data} = await fetchRequest('/zoaning/etages/', {method: 'GET'}) + if(isSuccess){ + setEtages(data) + }else{ + setEtages([]) + } + }catch(error){ console.log(error) } } getAllZones() + getAllEtages() + setIsLoadingData(false) }, []) + + console.log(zones) return(
    - -

    List des Zones

    - {zones ? -
    -

    Pas encore des habilitations

    -
    - : -
    - - - - - - - {zones.map((element) => { - return - })} -
    ZoneEtageAction
    -
    - } + {!isLoadingData ? + <> + +

    List des Zones

    + {isArray(zones) && zones?.length !== 0 && isArray(etages) && etages?.length !== 0 ? +
    + + + + + + + {zones?.map((element) => { + return + })} +
    ZoneEtageAction
    +
    + : +
    +

    Pas encore des habilitations

    +
    } + + : +
    + }
    -- GitLab From b0ae3539c76e8af192aa53dd1e6a7f05bb271f6c Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Fri, 24 May 2024 20:43:07 +0100 Subject: [PATCH 17/79] started adding planing added interface for adding semaines jours type de presence --- src/app/lib/fetchRequest.js | 14 +--- src/app/planning/layout.jsx | 23 ++++++ src/app/planning/page.jsx | 1 + .../EntityForm.jsx | 59 ++++++++++++++ .../EntityList.jsx | 41 ++++++++++ .../type-presence-semaines-jours/page.jsx | 77 +++++++++++++++++++ src/app/projects/page.jsx | 2 +- src/app/{projects => ui}/SideBar.jsx | 0 8 files changed, 204 insertions(+), 13 deletions(-) create mode 100644 src/app/planning/layout.jsx create mode 100644 src/app/planning/page.jsx create mode 100644 src/app/planning/type-presence-semaines-jours/EntityForm.jsx create mode 100644 src/app/planning/type-presence-semaines-jours/EntityList.jsx create mode 100644 src/app/planning/type-presence-semaines-jours/page.jsx rename src/app/{projects => ui}/SideBar.jsx (100%) diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index a47d118..71714c0 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -28,10 +28,11 @@ const fetchRequest = async (url, options = {}) => { } } console.log('response', response) + let data = null; // Check if the response has content before parsing it as JSON const contentType = response.headers.get('content-type'); if (contentType && contentType.includes('application/json')) { - const data = await response.json(); + data = await response.json(); return {isSuccess: true, errors: null, data: data, status: response.status}; } // If no JSON content, return null for data @@ -39,14 +40,3 @@ const fetchRequest = async (url, options = {}) => { }; export default fetchRequest; - - - - - - - - - - - diff --git a/src/app/planning/layout.jsx b/src/app/planning/layout.jsx new file mode 100644 index 0000000..6386ecb --- /dev/null +++ b/src/app/planning/layout.jsx @@ -0,0 +1,23 @@ +"use client"; +import SideBar from "@/app/ui/SideBar"; +import ProjectForm from "@/app/projects/ProjectForm"; +import ProjectList from "@/app/projects/ProjectList"; + +// layout for the planning page +export default function PlanningLayout ({ children }) { + return ( +
    +
    + +
    + +
    +
    + {children} +
    +
    +
    + ); + +} + diff --git a/src/app/planning/page.jsx b/src/app/planning/page.jsx new file mode 100644 index 0000000..2d4f31d --- /dev/null +++ b/src/app/planning/page.jsx @@ -0,0 +1 @@ +"use client"; \ No newline at end of file diff --git a/src/app/planning/type-presence-semaines-jours/EntityForm.jsx b/src/app/planning/type-presence-semaines-jours/EntityForm.jsx new file mode 100644 index 0000000..04f1769 --- /dev/null +++ b/src/app/planning/type-presence-semaines-jours/EntityForm.jsx @@ -0,0 +1,59 @@ +import { useState, useEffect } from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; + +const EntityForm = ({ entity, id }) => { + const [nom, setNom] = useState(''); + const [isLoading, setIsLoading] = useState(false); + + useEffect(() => { + if (id) { + const fetchData = async () => { + const response = await fetchRequest(`/${entity}/${id}/`); + if (response.isSuccess) setNom(response.data.nom); + }; + + fetchData(); + } + }, [entity, id]); + + const handleSubmit = async (e) => { + e.preventDefault(); + setIsLoading(true); + + const method = id ? 'PUT' : 'POST'; + const url = id ? `/${entity}/${id}/` : `/${entity}/`; + const response = await fetchRequest(url, { + method, + body: JSON.stringify({ nom }), + }); + + setIsLoading(false); + if (response.isSuccess) { + // router.push('/manage'); + } + }; + + return ( +
    +
    + + setNom(e.target.value)} + className="mt-1 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" + required + /> +
    + +
    + ); +}; + +export default EntityForm; diff --git a/src/app/planning/type-presence-semaines-jours/EntityList.jsx b/src/app/planning/type-presence-semaines-jours/EntityList.jsx new file mode 100644 index 0000000..c9f904b --- /dev/null +++ b/src/app/planning/type-presence-semaines-jours/EntityList.jsx @@ -0,0 +1,41 @@ +import Link from "next/link"; + +const EntityList = ({ title, items, setState, handleDelete }) => { + return ( +
    + {/*

    {title}

    */} +
    +
    + + + + + + + + + {items.map(item => ( + + + + + ))} + +
    NomActions
    {item.nom} + + +
    +
    + ); +}; + +export default EntityList; \ No newline at end of file diff --git a/src/app/planning/type-presence-semaines-jours/page.jsx b/src/app/planning/type-presence-semaines-jours/page.jsx new file mode 100644 index 0000000..17637e0 --- /dev/null +++ b/src/app/planning/type-presence-semaines-jours/page.jsx @@ -0,0 +1,77 @@ +"use client"; + +import {useEffect, useState} from 'react'; +import Link from 'next/link'; +import fetchRequest from "@/app/lib/fetchRequest"; +import EntityList from "@/app/planning/type-presence-semaines-jours/EntityList"; +import EntityForm from "@/app/planning/type-presence-semaines-jours/EntityForm"; + +const ManagePage = () => { + const [typePresences, setTypePresences] = useState([]); + const [semaines, setSemaines] = useState([]); + const [jours, setJours] = useState([]); + + + const fetchData = async () => { + const typePresencesResponse = await fetchRequest('/type-presences/'); + const semainesResponse = await fetchRequest('/semaines/'); + const joursResponse = await fetchRequest('/jours/'); + + if (typePresencesResponse.isSuccess) setTypePresences(typePresencesResponse.data); + if (semainesResponse.isSuccess) setSemaines(semainesResponse.data); + if (joursResponse.isSuccess) setJours(joursResponse.data); + }; + useEffect(() => { + + + fetchData(); + }, []); + + const handleDelete = async (endpoint, id, setState, currentState) => { + const response = await fetchRequest(`/${endpoint}/${id}/`, {method: 'DELETE'}); + if (response.isSuccess) { + setState(currentState.filter(item => item.id !== id)); + } + }; + + return ( + <> +

    Manage Entities

    +
    +
    +

    Type Presence

    + + +
    +
    +

    Semaines

    + + +
    +
    +

    Jours

    + + +
    +
    +

    Todo: edit each entity + design + compt rendu : base cco ro

    + + ); +}; + +export default ManagePage; diff --git a/src/app/projects/page.jsx b/src/app/projects/page.jsx index adba7fe..c1c93b5 100644 --- a/src/app/projects/page.jsx +++ b/src/app/projects/page.jsx @@ -1,6 +1,6 @@ 'use client'; -import SideBar from "@/app/projects/SideBar"; +import SideBar from "@/app/ui/SideBar"; import {useEffect, useState} from 'react'; import ProjectForm from "@/app/projects/ProjectForm"; import ProjectList from "@/app/projects/ProjectList"; diff --git a/src/app/projects/SideBar.jsx b/src/app/ui/SideBar.jsx similarity index 100% rename from src/app/projects/SideBar.jsx rename to src/app/ui/SideBar.jsx -- GitLab From 0d925863933e4ceb412b66b553f80c42bb587681 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Fri, 24 May 2024 20:44:28 +0100 Subject: [PATCH 18/79] started adding planing added interface for adding semaines jours type de presence --- src/app/planning/type-presence-semaines-jours/page.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/planning/type-presence-semaines-jours/page.jsx b/src/app/planning/type-presence-semaines-jours/page.jsx index 17637e0..645f4d6 100644 --- a/src/app/planning/type-presence-semaines-jours/page.jsx +++ b/src/app/planning/type-presence-semaines-jours/page.jsx @@ -69,7 +69,7 @@ const ManagePage = () => { />
    -

    Todo: edit each entity + design + compt rendu : base cco ro

    + {/*

    Todo: edit each entity + design + compt rendu : base cco ro

    */} ); }; -- GitLab From aac4d4d1b3cd89b7cf87b6a1e34dd8fe302057dd Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 27 May 2024 09:13:28 +0100 Subject: [PATCH 19/79] fix: create role with not found privilege --- src/app/role/UpdateRoleForm.jsx | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/src/app/role/UpdateRoleForm.jsx b/src/app/role/UpdateRoleForm.jsx index 7bf368b..e980dbf 100644 --- a/src/app/role/UpdateRoleForm.jsx +++ b/src/app/role/UpdateRoleForm.jsx @@ -34,8 +34,7 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv setLoadingStatus(true) const { isSuccess, errors, data, status } = await fetchRequest(`/roles/${id}/`, { method: "PATCH", - //.filter((element) => privileges.find((prvElement) => prvElement.id === element.id)) - body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.map((element) => element.id) }) + body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.filter((element) => privileges.find((prvElement) => prvElement.id === element.id)).map((element) => element.id) }) }) console.log(data) setLoadingStatus(false) -- GitLab From 499c3bbcb1db5810753dee5c4c50a01087041a23 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 27 May 2024 09:41:56 +0100 Subject: [PATCH 20/79] fix: display privileges for each role --- src/app/role/RoleTableRows.jsx | 14 +++++++++----- src/app/role/page.jsx | 5 +++-- 2 files changed, 12 insertions(+), 7 deletions(-) diff --git a/src/app/role/RoleTableRows.jsx b/src/app/role/RoleTableRows.jsx index 43444b5..7acdf0b 100644 --- a/src/app/role/RoleTableRows.jsx +++ b/src/app/role/RoleTableRows.jsx @@ -8,10 +8,7 @@ import ConfirmationModal from '../ui/ConfirmationModal' const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { const { toggleNotification } = useNotification() const [isModalOpen, setModalOpen] = useState(false); - const inputRef = useRef(null) - useEffect(() => { - inputRef.current.value = name - }, [name]) + const showDeletePopup = () => { setModalOpen(true); } @@ -53,7 +50,14 @@ const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { <> - +

    {name}

    + + +
    + {privileges?.map((element, index) => { + return
    {element.name}
    + })} +
    diff --git a/src/app/role/page.jsx b/src/app/role/page.jsx index a2c5f38..67c4bc1 100644 --- a/src/app/role/page.jsx +++ b/src/app/role/page.jsx @@ -39,7 +39,7 @@ const Role = () => {
    {openCreatePopup && } {roleToUpdate && } -
    +

    List des Roles

    +
    + + ) +} + +export default Place \ No newline at end of file diff --git a/src/app/table/CreateNewTable.jsx b/src/app/table/CreateNewTable.jsx new file mode 100644 index 0000000..0e8478d --- /dev/null +++ b/src/app/table/CreateNewTable.jsx @@ -0,0 +1,106 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react' +import fetchRequest from '../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { Island_Moments } from 'next/font/google' +import { useNotification } from '@/context/NotificationContext' + + +const CreateNewTable = ({ tablesState, zones }) => { + const [error, setError] = useState(null) + const [isLoadingAction, setIsLoadingAction] = useState(false) + const [numeroTable, setNumeroTable] = useState(null) + const [selectedZone, setSelectedZone] = useState(null) + + const inputRef = useRef(null) + const selectRef = useRef(null) + + const { toggleNotification } = useNotification() + + + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/tables/", { + method: "POST", + body: JSON.stringify({ numero: numeroTable, id_zone: selectedZone }) + }) + if (isSuccess) { + setIsLoadingAction(false) + tablesState((prevTableState) => [...prevTableState, {...data, id_zone: zones.find(zone => zone.id === data.id_zone)}]); + inputRef.current.value = "" + selectRef.current.value = "" + setNumeroTable(null) + setSelectedZone(null) + toggleNotification({ + visible: true, + message: "La table a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + if (errors.type === "ValidationError") { + if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numéro de la table saisie déjà existe.", + visible: true, + }) + } + }else{ + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la table.", + visible: true, + }) + } + console.log(errors) + } + } + + // Handle the name of zone change + const handleChangeTable = (event) => { + setError("") + setNumeroTable(event.target.value) + } + + const handleChangeZone = (event) => { + setError("") + setSelectedZone(event.target.value) + } + + + + + return( +
    +

    Ajout d'une table

    +
    +
    + + +
    +
    + + +
    +
    +

    +
    + +
    +
    + ) +} + + +export default CreateNewTable \ No newline at end of file diff --git a/src/app/table/RowTable.jsx b/src/app/table/RowTable.jsx new file mode 100644 index 0000000..939d879 --- /dev/null +++ b/src/app/table/RowTable.jsx @@ -0,0 +1,225 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react'; +import fetchRequest from '../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + + + + +const RowZone = ({ id, numero, zone, tablesState, zones }) => { + + //states + const [isUpdating, setIsUpdating] = useState(false) + const [tableNum, setTableNum] = useState(numero) + const [selectedZone, setSelectedZone] = useState(zone) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRef = useRef(null) + const rowRef = useRef(null) + + //Logic + useEffect(() => { + setTableNum(numero) + setSelectedZone(zone?.id) + selectRef.current.value = zone?.id + inputRef.current.value = numero + }, [numero, zone]) + + const handleUpdateZone = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/tables/${id}/`, { + method: "PATCH", + body: JSON.stringify({ numero: tableNum, id_zone: selectedZone }) + }) + setLoadingStatus(false) + if (isSuccess) { + if(data.message === "NO_CHANGES"){ + toggleNotification({ + visible: true, + message: "Aucun changement n'a été effectué.", + type: "warning" + }) + setIsUpdating(false) + return + } + tablesState((prevTableState) => prevTableState.map( (element) => element.id === id ? {...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone)} : element )) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "La table a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le numero de la table existe déjà", + visible: true, + }) + }else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numero de la table saisie déjà existe.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la table", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "La table n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/zoaning/tables/${id}/`, { method: "DELETE" }) + if (isSuccess) { + tablesState((prevTableState) => prevTableState.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "La table a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "La table n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const cancelUpdate = () => { + setIsUpdating(false) + setTableNum(numero) + setSelectedZone(zone.id) + selectRef.current.value = zone.id + inputRef.current.value = numero + } + + const handleUpdateBlur = (event) => { + const eventTarget = event.target + let isInsideRowRef = false; + let element = eventTarget; + while (element !== null) { + if (element === rowRef.current) { + isInsideRowRef = true; + break; + } + if (element.parentElement === null) { + isInsideRowRef = false; + break; + } + element = element.parentElement; + } + if (!isInsideRowRef && element?.classList.contains("tableRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + + useEffect(() => { + if (isUpdating && inputRef?.current && selectRef?.current) { + inputRef.current.focus() + selectRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + return( + + + setTableNum(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + + + + {!isUpdating + ?
    + + +
    + :
    + + +
    + } + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer la table numero "${numero}"?`} + /> + + ) +} + + +export default RowZone \ No newline at end of file diff --git a/src/app/table/page.jsx b/src/app/table/page.jsx new file mode 100644 index 0000000..23ad849 --- /dev/null +++ b/src/app/table/page.jsx @@ -0,0 +1,109 @@ +"use client" +import React from 'react' +import fetchRequest from '../lib/fetchRequest' +import { useState, useEffect } from 'react'; +import Loader from '@/components/Loader/Loader' +import CreateNewTable from './CreateNewTable' +import TableIcon from "@/static/image/svg/table.svg" +import { isArray } from '../lib/TypesHelper' +import RowTable from './RowTable' + + +const Table = ()=> { + const [ tables, setTables ] = useState([]) + const [ isLoadingData, setIsLoadingData ] = useState(true) + const [ zones, setZones ] = useState([]) + + + // Fetch data from external API + useEffect(() => { + const getAllTables = async () => { + try{ + const {isSuccess, errors, data} = await fetchRequest('/zoaning/tables/', {method: 'GET'}) + if(isSuccess){ + setTables(data) + }else{ + setTables([]) + } + }catch(error){ + console.log(error) + } + } + const getAllZones = async () => { + try{ + const {isSuccess, errors, data} = await fetchRequest('/zoaning/zones/', {method: 'GET'}) + if(isSuccess){ + setZones(data) + }else{ + setZones([]) + } + }catch(error){ + console.log(error) + } + } + getAllTables() + getAllZones() + setIsLoadingData(false) + }, []) + + + const handleSearchingTable = async (e) => { + const numero= e.target.value + try{ + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/search/table/${numero}`, {method: 'GET'}) + console.log(data) + if(isSuccess){ + setTables(data) + }else{ + setTables([]) + } + }catch(error){ + console.log(error) + } + } + return( +
    +
    +
    + {!isLoadingData ? + <> + +
    +

    List des Tables

    +
    +
    + +
    + {handleSearchingTable(e)}} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des tables..." required /> +
    +
    + + {isArray(tables) && tables?.length !== 0 && isArray(zones) && zones?.length !== 0 ? +
    + + + + + + + {tables?.map((element) => { + return + })} +
    TableZone-EtageAction
    +
    + : +
    +

    Pas encore des tables

    +
    } + + : +
    + } +
    +
    +
    + + ) +} + +export default Table \ No newline at end of file diff --git a/src/app/zone/CreateNewZone.jsx b/src/app/zone/CreateNewZone.jsx index d02c3f5..76b78c6 100644 --- a/src/app/zone/CreateNewZone.jsx +++ b/src/app/zone/CreateNewZone.jsx @@ -43,7 +43,7 @@ const CreateNewZone = ({ zoneState, etages }) => { if (errors.detail.non_field_errors) { toggleNotification({ type: "warning", - message: "Le nom de la zone saisie existe déjà dans l'étage sélectionné.", + message: "Le nom de la zone saisie déjà existe.", visible: true, }) } diff --git a/src/app/zone/RowZone.jsx b/src/app/zone/RowZone.jsx index 5ef8172..baebd3a 100644 --- a/src/app/zone/RowZone.jsx +++ b/src/app/zone/RowZone.jsx @@ -41,6 +41,16 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { }) setLoadingStatus(false) if (isSuccess) { + console.log(data) + if(data.message === "NO_CHANGES"){ + toggleNotification({ + visible: true, + message: "Aucun changement n'a été effectué.", + type: "warning" + }) + setIsUpdating(false) + return + } console.log(data.data) zonesState((prevZonesValue) => prevZonesValue.map( (element) => element.id === id ? {...data.data, id_etage: etages.find(etage => etage.id === data.data.id_etage)} : element )) setIsUpdating(false) @@ -51,7 +61,7 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { }) } else { if (errors.type === "ValidationError") { - if (errors.detail.name) { + if (errors.detail.nom) { toggleNotification({ type: "warning", message: "Le nom de la zone existe déjà", @@ -60,7 +70,7 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { }else if (errors.detail.non_field_errors) { toggleNotification({ type: "warning", - message: "Le nom de la zone saisie existe déjà dans l'étage sélectionné.", + message: "Le nom de la zone saisie déjà existe.", visible: true, }) } diff --git a/src/app/zone/page.jsx b/src/app/zone/page.jsx index 5fc1e53..1e3c032 100644 --- a/src/app/zone/page.jsx +++ b/src/app/zone/page.jsx @@ -3,12 +3,12 @@ import React from 'react' import fetchRequest from '../lib/fetchRequest' import { useState, useEffect } from 'react'; import Loader from '@/components/Loader/Loader' -import CreateNewZone from './CreateNewZone' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" import CancelIcon from "@/static/image/svg/cancel.svg" import CheckIcon from "@/static/image/svg/check.svg" import { isArray } from '../lib/TypesHelper' +import CreateNewZone from './CreateNewZone' import RowZone from './RowZone' @@ -73,7 +73,7 @@ const Zone = ()=> {
    :
    -

    Pas encore des habilitations

    +

    Pas encore des zones

    } : diff --git a/src/static/image/svg/place.svg b/src/static/image/svg/place.svg new file mode 100644 index 0000000..cd980fb --- /dev/null +++ b/src/static/image/svg/place.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/table.svg b/src/static/image/svg/table.svg new file mode 100644 index 0000000..0d56955 --- /dev/null +++ b/src/static/image/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From a00b8c1d1e707099a37ebfc19505d7d807f993cd Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Tue, 28 May 2024 09:19:28 +0100 Subject: [PATCH 22/79] added planning interfaces --- .idea/misc.xml | 1 - src/app/planning/PlanningTable.jsx | 32 +++++++++ src/app/planning/page.jsx | 58 ++++++++++++++++- .../EntityForm.jsx | 49 +++++++++----- .../EntityList.jsx | 36 +++++++--- .../type-presence-semaines-jours/page.jsx | 65 +++++++++++++------ src/app/projects/ProjectForm.jsx | 26 +++++--- src/app/projects/page.jsx | 23 +++++++ src/app/ui/Dropdown.js | 20 ++++++ src/app/ui/LogoutButton.js | 6 +- 10 files changed, 260 insertions(+), 56 deletions(-) create mode 100644 src/app/planning/PlanningTable.jsx create mode 100644 src/app/ui/Dropdown.js diff --git a/.idea/misc.xml b/.idea/misc.xml index 639900d..6e86672 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/src/app/planning/PlanningTable.jsx b/src/app/planning/PlanningTable.jsx new file mode 100644 index 0000000..40e3b7a --- /dev/null +++ b/src/app/planning/PlanningTable.jsx @@ -0,0 +1,32 @@ +import React from 'react'; + +const PlanningTable = ({ data }) => { + return ( +
    + + + + + + + + + + + + + {data.map((row, rowIndex) => ( + + + {row.days.map((day, dayIndex) => ( + + ))} + + ))} + +
    Semaine / JourJ1J2J3J4J5
    {row.week}{day}
    +
    + ); +}; + +export default PlanningTable; diff --git a/src/app/planning/page.jsx b/src/app/planning/page.jsx index 2d4f31d..3467ce4 100644 --- a/src/app/planning/page.jsx +++ b/src/app/planning/page.jsx @@ -1 +1,57 @@ -"use client"; \ No newline at end of file +'use client'; +import { useState } from 'react'; +import Dropdown from "@/app/ui/Dropdown"; +import PlanningTable from "@/app/planning/PlanningTable"; + +const PlanningPage = () => { + const [selectedProject, setSelectedProject] = useState(''); + const [selectedWeek, setSelectedWeek] = useState(''); + const [selectedDay, setSelectedDay] = useState(''); + const [planningData, setPlanningData] = useState([ + { week: 'Semaine 1', days: ['', '', '', '', ''] }, + { week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel'] }, + { week: 'Semaine 3', days: ['', '', '', '', ''] }, + { week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', ''] }, + ]); + + const handleProjectChange = (e) => setSelectedProject(e.target.value); + const handleWeekChange = (e) => setSelectedWeek(e.target.value); + const handleDayChange = (e) => setSelectedDay(e.target.value); + + const fetchPlanningData = async () => { + // Fetch planning data from your API based on selectedProject, selectedWeek, and selectedDay + // Here we'll use dummy data for demonstration purposes + const data = [ + { week: 'Semaine 1', days: ['', '', '', '', ''] }, + { week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel'] }, + { week: 'Semaine 3', days: ['', '', '', '', ''] }, + { week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', ''] }, + ]; + setPlanningData(data); + }; + + return ( +
    +

    Planning

    +
    + +
    + +
    + +
    +
    + ); +}; + +export default PlanningPage; diff --git a/src/app/planning/type-presence-semaines-jours/EntityForm.jsx b/src/app/planning/type-presence-semaines-jours/EntityForm.jsx index 04f1769..83f4dfc 100644 --- a/src/app/planning/type-presence-semaines-jours/EntityForm.jsx +++ b/src/app/planning/type-presence-semaines-jours/EntityForm.jsx @@ -1,18 +1,21 @@ import { useState, useEffect } from 'react'; import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; -const EntityForm = ({ entity, id }) => { +const EntityForm = ({ entity, id, onSaved, onCancel }) => { const [nom, setNom] = useState(''); const [isLoading, setIsLoading] = useState(false); + const { toggleNotification } = useNotification() + const fetchData = async () => { + const response = await fetchRequest(`/${entity}/${id}/`); + if (response.isSuccess) setNom(response.data.nom); + }; useEffect(() => { if (id) { - const fetchData = async () => { - const response = await fetchRequest(`/${entity}/${id}/`); - if (response.isSuccess) setNom(response.data.nom); - }; - fetchData(); + } else { + setNom(''); } }, [entity, id]); @@ -29,12 +32,19 @@ const EntityForm = ({ entity, id }) => { setIsLoading(false); if (response.isSuccess) { - // router.push('/manage'); + toggleNotification({ + visible: true, + message: `${nom} a été ${id ? 'modifié' : 'créé'} avec succès`, + type: "success" + }) + setNom(''); + onSaved(); + onCancel(); } }; return ( -
    +
    { required />
    - +
    + + +
    ); }; diff --git a/src/app/planning/type-presence-semaines-jours/EntityList.jsx b/src/app/planning/type-presence-semaines-jours/EntityList.jsx index c9f904b..a829627 100644 --- a/src/app/planning/type-presence-semaines-jours/EntityList.jsx +++ b/src/app/planning/type-presence-semaines-jours/EntityList.jsx @@ -1,11 +1,23 @@ -import Link from "next/link"; +import React, { useState } from 'react'; +import ConfirmationModal from '@/app/ui/ConfirmationModal'; + +const EntityList = ({ title, items, setState, handleDelete, handleEdit }) => { + const [isModalOpen, setModalOpen] = useState(false); + const [entityToDelete, setEntityToDelete] = useState(null); + + const handleDeleteClick = (entity) => { + setEntityToDelete(entity); + setModalOpen(true); + }; + + const handleConfirmDelete = () => { + handleDelete(title.toLowerCase(), entityToDelete.id, setState, items); + setModalOpen(false); + setEntityToDelete(null); + }; -const EntityList = ({ title, items, setState, handleDelete }) => { return (
    - {/*

    {title}

    */} -
    -
    @@ -19,12 +31,13 @@ const EntityList = ({ title, items, setState, handleDelete }) => {
    {item.nom}
    + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Are you sure you want to delete this ${title.toLowerCase()} "${entityToDelete?.nom}"?`} + />
    ); }; -export default EntityList; \ No newline at end of file +export default EntityList; diff --git a/src/app/planning/type-presence-semaines-jours/page.jsx b/src/app/planning/type-presence-semaines-jours/page.jsx index 645f4d6..f6d566c 100644 --- a/src/app/planning/type-presence-semaines-jours/page.jsx +++ b/src/app/planning/type-presence-semaines-jours/page.jsx @@ -1,16 +1,17 @@ "use client"; import {useEffect, useState} from 'react'; -import Link from 'next/link'; import fetchRequest from "@/app/lib/fetchRequest"; import EntityList from "@/app/planning/type-presence-semaines-jours/EntityList"; import EntityForm from "@/app/planning/type-presence-semaines-jours/EntityForm"; +import {useNotification} from "@/context/NotificationContext"; const ManagePage = () => { const [typePresences, setTypePresences] = useState([]); const [semaines, setSemaines] = useState([]); const [jours, setJours] = useState([]); - + const [editingEntity, setEditingEntity] = useState({ entity: null, id: null }); + const { toggleNotification } = useNotification() const fetchData = async () => { const typePresencesResponse = await fetchRequest('/type-presences/'); @@ -21,9 +22,8 @@ const ManagePage = () => { if (semainesResponse.isSuccess) setSemaines(semainesResponse.data); if (joursResponse.isSuccess) setJours(joursResponse.data); }; - useEffect(() => { - + useEffect(() => { fetchData(); }, []); @@ -31,45 +31,68 @@ const ManagePage = () => { const response = await fetchRequest(`/${endpoint}/${id}/`, {method: 'DELETE'}); if (response.isSuccess) { setState(currentState.filter(item => item.id !== id)); + toggleNotification({ + visible: true, + message: `${currentState.find(item => item.id === id).nom} a été supprimé avec succès`, + type: "success" + }) } + await fetchData(); }; return ( <>

    Manage Entities

    -
    +

    Type Presence

    - + setEditingEntity({ entity: null, id: null })} + /> setEditingEntity({ entity: 'type-presences', id })} />

    Semaines

    - - + setEditingEntity({ entity: null, id: null })} + /> + setEditingEntity({ entity: 'semaines', id })} + />

    Jours

    - - + setEditingEntity({ entity: null, id: null })} + /> + setEditingEntity({ entity: 'jours', id })} + />
    - {/*

    Todo: edit each entity + design + compt rendu : base cco ro

    */} ); }; diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 350d3d0..0c4f1d1 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -1,7 +1,7 @@ import { useState, useEffect } from 'react'; import fetchRequest from '@/app/lib/fetchRequest'; -const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { +const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingProject }) => { const [projectName, setProjectName] = useState(''); const [clientName, setClientName] = useState(''); const [dateDebut, setDateDebut] = useState(''); @@ -66,15 +66,15 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { e.preventDefault(); if (!validateForm()) return; - const project = { nom: projectName, nomClient: clientName, dateDebut, dateFin, - users: userIds.map(user => user.id), + user_ids: userIds.map(user => user.id), }; - + console.log(project) + console.log(project.users) if (editingProject) { onEditProject(editingProject.id, project); } else { @@ -148,11 +148,11 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => {
  • !isUserChosen(user) && handleUserSelect(user)} - className={`px-3 py-2 border-b border-chicago-200 cursor-pointer ${isUserChosen(user) ? 'bg-gray-200 cursor-not-allowed' : 'hover:bg-sushi-200'}`} + className={`px-3 py-2 border-b border-chicago-200 ${isUserChosen(user) ? 'bg-gray-200 cursor-not-allowed' : 'hover:bg-sushi-200 cursor-pointer'}`} > {/*add tick is chosen*/} {isUserChosen(user) && '✔ '} - {user.username} ({user.email}) {isUserChosen(user) && '(selectionné)'} + {user.first_name} {user.last_name} ({user.role.name}) {user.email}
  • ))} @@ -162,7 +162,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => {
    @@ -178,9 +178,19 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => {
    {errors.userIds &&

    {errors.userIds}

    }
    - + {/* cancel*/} + {editingProject && ( + + )} ); }; diff --git a/src/app/projects/page.jsx b/src/app/projects/page.jsx index c1c93b5..98cf397 100644 --- a/src/app/projects/page.jsx +++ b/src/app/projects/page.jsx @@ -5,6 +5,7 @@ import {useEffect, useState} from 'react'; import ProjectForm from "@/app/projects/ProjectForm"; import ProjectList from "@/app/projects/ProjectList"; import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; const Projects = () => { const [pageUrl, setPageUrl] = useState('/projects/'); @@ -13,6 +14,8 @@ const Projects = () => { {/* errors from request*/} const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); + const { toggleNotification } = useNotification() + const fetchProjects = async () => { const { isSuccess, errors , data } = await fetchRequest(pageUrl); if (isSuccess) { @@ -31,6 +34,9 @@ const Projects = () => { // pageURL const handleAddProject = async (project) => { + console.log("proj",project) + console.log("proj strinjify",JSON.stringify(project)) + setLoading(true); const { isSuccess, errors , data } = await fetchRequest('/projects/', { method: 'POST', @@ -38,6 +44,11 @@ const Projects = () => { }); if (isSuccess) { + toggleNotification({ + visible: true, + message: `${project.nom} a été ajouté avec succès`, + type: "success" + }) setProjects([...projects, data]); setEditingProject(null); setErrors(null); @@ -56,6 +67,11 @@ const Projects = () => { }); if (isSuccess) { + toggleNotification({ + visible: true, + message: `${updatedProject.nom} a été modifié avec succès`, + type: "success" + }) const updatedProjects = projects.map((project) => project.id === id ? data : project ); @@ -70,6 +86,7 @@ const Projects = () => { }; const handleEditClick = (project) => { + console.log(project) setEditingProject(project); }; @@ -80,6 +97,11 @@ const Projects = () => { }); if (isSuccess) { + toggleNotification({ + visible: true, + message: `${project.nom} a été supprimé avec succès`, + type: "success" + }) // setProjects(projects.filter((p) => p.id !== project.id)); await fetchProjects(); setEditingProject(null); @@ -105,6 +127,7 @@ const Projects = () => { onAddProject={handleAddProject} onEditProject={handleEditProject} editingProject={editingProject} + setEditingProject={setEditingProject} /> {/*errors from request*/} {errors && errors.detail && Object.keys(errors.detail).map((key) => ( diff --git a/src/app/ui/Dropdown.js b/src/app/ui/Dropdown.js new file mode 100644 index 0000000..53c7dad --- /dev/null +++ b/src/app/ui/Dropdown.js @@ -0,0 +1,20 @@ +import React from 'react'; + +const Dropdown = ({ label, options, value, onChange }) => { + return ( +
    + + +
    + ); +}; + +export default Dropdown; diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.js index b219fc4..3f9f7e0 100644 --- a/src/app/ui/LogoutButton.js +++ b/src/app/ui/LogoutButton.js @@ -6,7 +6,8 @@ import fetchRequest from "@/app/lib/fetchRequest"; const LogoutButton = () => { const logout = async () => { const response = await fetchRequest(`/logout`, { - method: 'GET'}); + method: 'GET' + }); console.log(response); if (response.isSuccess) { console.log('logout successful'); @@ -17,7 +18,8 @@ const LogoutButton = () => { }; return ( - ); -- GitLab From 7620c15a67933199b0bccfefaff6af0681e3eea2 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Tue, 28 May 2024 11:32:24 +0100 Subject: [PATCH 23/79] adding planning --- src/app/planning/PlanningTable.jsx | 46 +++++++++++-- src/app/planning/page.jsx | 69 +++++++++++++++---- .../type-presence-semaines-jours/page.jsx | 66 +++++++++--------- 3 files changed, 127 insertions(+), 54 deletions(-) diff --git a/src/app/planning/PlanningTable.jsx b/src/app/planning/PlanningTable.jsx index 40e3b7a..4ddc99b 100644 --- a/src/app/planning/PlanningTable.jsx +++ b/src/app/planning/PlanningTable.jsx @@ -1,10 +1,34 @@ -import React from 'react'; +import React, {useEffect, useState} from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; + +const PlanningTable = ({ data, onTypePresenceChange }) => { + // fetch type presence + const [typePresences, setTypePresences] = useState([]); + const [errors, setErrors] = useState(); + const [loading, setLoading] = useState(false); + const {toggleNotification} = useNotification() + const [selectedTypePresence, setSelectedTypePresence] = useState(); + + const fetchTypePresences = async () => { + const { isSuccess, errors, data } = await fetchRequest('/type-presences/'); + if (isSuccess) { + setTypePresences(data); + setErrors(null); + } + else { + console.error("Failed to fetch type presences"); + setErrors(errors) + } + } + useEffect(() => { + fetchTypePresences() + }, []); -const PlanningTable = ({ data }) => { return (
    - - +
    + @@ -16,10 +40,20 @@ const PlanningTable = ({ data }) => { {data.map((row, rowIndex) => ( - + {row.days.map((day, dayIndex) => ( - + ))} ))} diff --git a/src/app/planning/page.jsx b/src/app/planning/page.jsx index 3467ce4..42a6408 100644 --- a/src/app/planning/page.jsx +++ b/src/app/planning/page.jsx @@ -1,46 +1,85 @@ 'use client'; -import { useState } from 'react'; +import {useEffect, useState} from 'react'; import Dropdown from "@/app/ui/Dropdown"; import PlanningTable from "@/app/planning/PlanningTable"; +import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; const PlanningPage = () => { + const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(''); - const [selectedWeek, setSelectedWeek] = useState(''); - const [selectedDay, setSelectedDay] = useState(''); const [planningData, setPlanningData] = useState([ - { week: 'Semaine 1', days: ['', '', '', '', ''] }, - { week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel'] }, - { week: 'Semaine 3', days: ['', '', '', '', ''] }, - { week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', ''] }, + {week: 'Semaine 1', days: ['', '', '', '', '']}, + {week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel']}, + {week: 'Semaine 3', days: ['', '', '', '', '']}, + {week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', '']}, + {week: 'Semaine 5', days: ['Présentiel', 'Présentiel', '', '', '']}, ]); + const [errors, setErrors] = useState(); + const [loading, setLoading] = useState(false); + const {toggleNotification} = useNotification() + + const fetchProjects = async () => { + const {isSuccess, errors, data} = await fetchRequest('/projects/'); + if (isSuccess) { + setProjects(data); + setErrors(null); + } else { + console.error("Failed to fetch projects"); + setErrors(errors) + } + }; + useEffect(() => { + + fetchProjects(); + }, []); const handleProjectChange = (e) => setSelectedProject(e.target.value); - const handleWeekChange = (e) => setSelectedWeek(e.target.value); - const handleDayChange = (e) => setSelectedDay(e.target.value); const fetchPlanningData = async () => { // Fetch planning data from your API based on selectedProject, selectedWeek, and selectedDay // Here we'll use dummy data for demonstration purposes const data = [ - { week: 'Semaine 1', days: ['', '', '', '', ''] }, - { week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel'] }, - { week: 'Semaine 3', days: ['', '', '', '', ''] }, - { week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', ''] }, + {week: 'Semaine 1', days: ['', '', '', '', '']}, + {week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel']}, + {week: 'Semaine 3', days: ['', '', '', '', '']}, + {week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', '']}, + {week: 'Semaine 5', days: ['Présentiel', 'Présentiel', '', '', '']}, ]; setPlanningData(data); }; + const handleTypePresenceChange = (weekIndex, dayIndex, value) => { + const updatedData = [...planningData]; + updatedData[weekIndex].days[dayIndex] = value; + setPlanningData(updatedData); + }; + return (

    Planning

    + {/*project using select*/} +
    - +
    ); diff --git a/src/app/planning/type-presence-semaines-jours/page.jsx b/src/app/planning/type-presence-semaines-jours/page.jsx index f6d566c..c29923c 100644 --- a/src/app/planning/type-presence-semaines-jours/page.jsx +++ b/src/app/planning/type-presence-semaines-jours/page.jsx @@ -44,7 +44,7 @@ const ManagePage = () => { <>

    Manage Entities

    -
    +

    Type Presence

    { handleEdit={id => setEditingEntity({ entity: 'type-presences', id })} />
    -
    -

    Semaines

    - setEditingEntity({ entity: null, id: null })} - /> - setEditingEntity({ entity: 'semaines', id })} - /> -
    -
    -

    Jours

    - setEditingEntity({ entity: null, id: null })} - /> - setEditingEntity({ entity: 'jours', id })} - /> -
    + {/*
    */} + {/*

    Semaines

    */} + {/* setEditingEntity({ entity: null, id: null })}*/} + {/* />*/} + {/* setEditingEntity({ entity: 'semaines', id })}*/} + {/* />*/} + {/*
    */} + {/*
    */} + {/*

    Jours

    */} + {/* setEditingEntity({ entity: null, id: null })}*/} + {/* />*/} + {/* setEditingEntity({ entity: 'jours', id })}*/} + {/* />*/} + {/*
    */}
    ); -- GitLab From 33e29d8ab1ab76204c6b7ad1e0c97fa77e21280d Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 28 May 2024 11:34:06 +0100 Subject: [PATCH 24/79] feature: add & update user --- src/app/role/RoleTableRows.jsx | 4 +- src/app/role/page.jsx | 8 +- src/app/user/CreateUserForm.jsx | 216 ++++++++++++++++++++++++++++++++ src/app/user/UpdateUserForm.jsx | 9 ++ src/app/user/UserTableRow.jsx | 88 +++++++++++++ src/app/user/page.jsx | 77 ++++++++++++ 6 files changed, 397 insertions(+), 5 deletions(-) create mode 100644 src/app/user/CreateUserForm.jsx create mode 100644 src/app/user/UpdateUserForm.jsx create mode 100644 src/app/user/UserTableRow.jsx create mode 100644 src/app/user/page.jsx diff --git a/src/app/role/RoleTableRows.jsx b/src/app/role/RoleTableRows.jsx index 7acdf0b..5c44906 100644 --- a/src/app/role/RoleTableRows.jsx +++ b/src/app/role/RoleTableRows.jsx @@ -62,10 +62,10 @@ const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => {
    diff --git a/src/app/role/page.jsx b/src/app/role/page.jsx index 67c4bc1..411c32f 100644 --- a/src/app/role/page.jsx +++ b/src/app/role/page.jsx @@ -7,11 +7,13 @@ import fetchRequest from '../lib/fetchRequest' import { isArray } from '../lib/TypesHelper' import AddIcon from "@/static/image/svg/add.svg" import UpdateRoleForm from './UpdateRoleForm' +import { useNotification } from '@/context/NotificationContext' const Role = () => { const [roles, setRoles] = useState([]) const [isLoading, setIsLoading] = useState(true) const [openCreatePopup, setOpenCreatePopup] = useState(null) const [roleToUpdate, setRoleToUpdate] = useState(null) + const { toggleNotification } = useNotification() useEffect(() => { const getRoles = async () => { const { data, errors, isSuccess } = await fetchRequest("/roles") @@ -46,15 +48,15 @@ const Role = () => {

    Rôle

    - {isLoading &&
    } + {isLoading &&
    } {!isLoading && <> {(!isArray(roles) || roles?.length === 0) - ?
    + ?

    Pas encore des roles

    :
    Semaine / Jour J1
    {row.week}{day} + +
    - + diff --git a/src/app/user/CreateUserForm.jsx b/src/app/user/CreateUserForm.jsx new file mode 100644 index 0000000..f06c5ca --- /dev/null +++ b/src/app/user/CreateUserForm.jsx @@ -0,0 +1,216 @@ +import React, { useState, useEffect } from 'react' +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" + + + + +function generateRandomPassword() { + const length = Math.floor(Math.random() * 3) + 8; // Length between 8 and 10 + const uppercase = 'ABCDEFGHIJKLMNOPQRSTUVWXYZ'; + const lowercase = 'abcdefghijklmnopqrstuvwxyz'; + const numbers = '0123456789'; + const symbols = '!@#$%^&*()_+[]{}|;:,.<>?'; + + let password = ''; + + password += uppercase[Math.floor(Math.random() * uppercase.length)]; + password += numbers[Math.floor(Math.random() * numbers.length)]; + password += symbols[Math.floor(Math.random() * symbols.length)]; + + const allCharacters = uppercase + lowercase + numbers + symbols; + + for (let i = 3; i < length; i++) { + const randomIndex = Math.floor(Math.random() * allCharacters.length); + password += allCharacters[randomIndex]; + } + + password = password.split('').sort(() => 0.5 - Math.random()).join(''); + + return password; +} +const CreateUserForm = ({ setIsOpen, appendUser }) => { + const [isLoading, setIsLoading] = useState(false) + const [selectedRole, setSelectedRole] = useState(null) + const { toggleNotification } = useNotification() + const [selectProjects, setSelectedProjects] = useState([]) + + const [roles, setRoles] = useState(null) + const [projects, setProjects] = useState(null) + useEffect(() => { + const getRoles = async () => { + const { data, errors, isSuccess } = await fetchRequest("/roles/") + if (isSuccess) { + setRoles(data) + } else { + setRoles([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + const getProjects = async () => { + const { isSuccess, errors, data } = await fetchRequest("/projects/"); + if (isSuccess) { + setProjects(data); + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getRoles() + getProjects() + }, []) + const handleFieldChange = (event) => setUserData({ ...userData, [event.target.name]: event.target.value }) + + const [userData, setUserData] = useState({ email: "", password: generateRandomPassword(), first_name: "", last_name: "" }) + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoading(true) + const { data, errors, isSuccess, status } = await fetchRequest("/users/", { + method: "POST", + body: JSON.stringify({ ...userData, username: userData.email, role: selectedRole.id, project_ids: selectProjects.map((element) => element.id) }) + }) + if (isSuccess) { + setIsLoading(false) + setSelectedRole(null) + appendUser(data.data) + toggleNotification({ + visible: true, + message: "Le rôle a été créé avec succès", + type: "success" + }) + setIsOpen(false) + } else { + setIsLoading(false) + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + visible: true, + message: "Le rôle existe déjà", + type: "warning" + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle", + type: "warning" + }) + setIsOpen(false) + } + } + else if (status === 409) { + toggleNotification({ + visible: true, + message: "role created with 2 or more same privileges", + type: "error" + }) + setIsOpen(false) + } else if (errors.detail === "Privilege matching query does not exist.") { + toggleNotification({ + visible: true, + message: "Des privilèges que vous avez utilisés ont été supprimés.", + type: "warning" + }) + setIsOpen(false) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setIsOpen(false) + } + console.log(errors) + } + } + const handleRoleClick = (newRole) => { + if (selectedRole?.id === newRole.id) { + setSelectedRole(null) + } else { + setSelectedRole(newRole) + } + } + const handleProjectClick = (project) => { + if (selectProjects.find((element) => element.id === project.id)) { + setSelectedProjects(selectProjects.filter((element) => element.id !== project.id)) + } else { + setSelectedProjects([...selectProjects, project]) + } + } + return ( +
    +
    + setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
    +

    Ajout d utilisateur

    +
    +
    + + +
    +
    + + +
    +
    +
    + + +
    +
    + + +
    + {(roles) ?
    +
    + +
    +
    + {roles.length !== 0 ? roles?.map((role) => { + const isSelected = selectedRole?.id === role.id + return
    handleRoleClick(role)} key={role.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}> + {role.name} +
    + }) :
    +

    Pas encore des habilitations

    +
    } +
    +
    :
    } + {(projects) ?
    +
    + +
    +
    + {projects.length !== 0 ? projects?.map((project) => { + const isSelected = selectProjects.find((element) => element.id === project.id) !== undefined + return
    handleProjectClick(project)} key={project.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}> + {project.nom} +
    + }) :
    +

    Pas encore des projets

    +
    } +
    +
    :
    } +
    + +
    + +
    +
    + ) +} + +export default CreateUserForm \ No newline at end of file diff --git a/src/app/user/UpdateUserForm.jsx b/src/app/user/UpdateUserForm.jsx new file mode 100644 index 0000000..d214f18 --- /dev/null +++ b/src/app/user/UpdateUserForm.jsx @@ -0,0 +1,9 @@ +import React from 'react' + +const UpdateUserForm = () => { + return ( +
    UpdateUserForm
    + ) +} + +export default UpdateUserForm \ No newline at end of file diff --git a/src/app/user/UserTableRow.jsx b/src/app/user/UserTableRow.jsx new file mode 100644 index 0000000..1860ee8 --- /dev/null +++ b/src/app/user/UserTableRow.jsx @@ -0,0 +1,88 @@ +import React, { useState } from 'react' +import DeleteIcon from "@/static/image/svg/delete.svg" +import EditIcon from "@/static/image/svg/edit.svg" +import ConfirmationModal from '../ui/ConfirmationModal' +import { useNotification } from '@/context/NotificationContext' +import fetchRequest from '../lib/fetchRequest' + +const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projects }) => { + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false); + const showDeletePopup = () => { + setModalOpen(true); + } + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/users/${id}/`, { method: "DELETE" }) + if (isSuccess) { + setUsers((users) => users.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "L'utilisateur a été supprimé avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "L'utilisateur n'a pas été trouvé", + type: "warning" + }) + } else if (errors.detail?.indexOf("Cannot delete some instances of model 'Role'") !== -1) { + toggleNotification({ + visible: true, + message: "Impossible de supprimer cet utilisateur car il est attribué à des utilisateurs", + type: "warning" + }) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + setModalOpen(false) + console.log(errors) + } + return ( + <> + + + + + + + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer le rôle ?`} + /> + + ) +} + +export default UserTableRow \ No newline at end of file diff --git a/src/app/user/page.jsx b/src/app/user/page.jsx new file mode 100644 index 0000000..ff8ef67 --- /dev/null +++ b/src/app/user/page.jsx @@ -0,0 +1,77 @@ +'use client'; +import React, { useEffect, useState } from 'react' +import CreateUserForm from './CreateUserForm' +import UpdateUserForm from './UpdateUserForm' +import AddIcon from "@/static/image/svg/add.svg" +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../lib/fetchRequest'; +import { isArray } from '../lib/TypesHelper'; +import { useNotification } from '@/context/NotificationContext'; +import UserTableRow from './UserTableRow'; + +const UserPage = () => { + const [users, setUsers] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) + const [userToUpdate, setUserToUpdate] = useState(null) + const { toggleNotification } = useNotification() + + useEffect(() => { + const getUsers = async () => { + const { data, errors, isSuccess } = await fetchRequest("/users") + setIsLoading(false) + if (isSuccess) { + console.log(data) + setUsers(data) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getUsers() + }, []) + const appendUser = (newUser) => { + setUsers([newUser, ...users]) + } + return (
    +
    +
    + {openCreatePopup && } + {userToUpdate && } +
    +

    List des Utilisateurs

    + +
    + {(isLoading) &&
    } + {(!isLoading) && <> {(!isArray(users) || users?.length === 0) + ?
    +

    Pas encore des utilisateurs

    +
    + :
    +
    rôleRôle Habilitations Action
    +

    {first_name} {last_name}

    +
    +

    {email}

    +
    +

    {role?.name || ""}

    +
    +
      + {(!projects || projects.length === 0) &&

      -

      } + {projects?.map((project) => { + return
    • {project.nom}
    • + })} +
    +
    +
    + + +
    +
    + + + + + + + + {users?.map((element) => { + return + })} +
    NomEmailRôleProjectsAction
    +
    + }} +
    +
    +
    + ) +} + +export default UserPage \ No newline at end of file -- GitLab From d6ebca05d349f81b58cde78cd943cefbd8152eb2 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Tue, 28 May 2024 11:36:20 +0100 Subject: [PATCH 25/79] Feature zoanning CRUD Compeleted_1 --- src/app/place/CreateNewPlace.jsx | 2 +- src/app/table/CreateNewTable.jsx | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/place/CreateNewPlace.jsx b/src/app/place/CreateNewPlace.jsx index 1d6342d..8036b0a 100644 --- a/src/app/place/CreateNewPlace.jsx +++ b/src/app/place/CreateNewPlace.jsx @@ -79,7 +79,7 @@ const CreateNewPlace = ({placesState, tables}) => {

    Ajout d'une place

    - +
    diff --git a/src/app/table/CreateNewTable.jsx b/src/app/table/CreateNewTable.jsx index 0e8478d..fc2f3cf 100644 --- a/src/app/table/CreateNewTable.jsx +++ b/src/app/table/CreateNewTable.jsx @@ -78,7 +78,7 @@ const CreateNewTable = ({ tablesState, zones }) => {
    - +
    -- GitLab From f24a66943101928a6b7a2ca4b74a3b5ea80f72f2 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 28 May 2024 17:59:17 +0100 Subject: [PATCH 26/79] feature: update user --- src/app/lib/constants.js | 3 +- src/app/projects/ProjectForm.jsx | 2 +- src/app/projects/ProjectList.jsx | 154 +++++++++++------------ src/app/projects/page.jsx | 17 +-- src/app/role/RoleTableRows.jsx | 4 +- src/app/user/CreateUserForm.jsx | 192 ++++++++++++++++------------- src/app/user/UpdateUserForm.jsx | 203 ++++++++++++++++++++++++++++++- src/app/user/UserTableRow.jsx | 10 +- src/app/user/page.jsx | 2 +- 9 files changed, 403 insertions(+), 184 deletions(-) diff --git a/src/app/lib/constants.js b/src/app/lib/constants.js index f4da663..889cdc8 100644 --- a/src/app/lib/constants.js +++ b/src/app/lib/constants.js @@ -1 +1,2 @@ -export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; \ No newline at end of file +export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; +export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@teamwillgroup\.com$/ \ No newline at end of file diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 350d3d0..e8a184e 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -72,7 +72,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject }) => { nomClient: clientName, dateDebut, dateFin, - users: userIds.map(user => user.id), + user_ids: userIds.map(user => user.id), }; if (editingProject) { diff --git a/src/app/projects/ProjectList.jsx b/src/app/projects/ProjectList.jsx index df3458c..16d3fe3 100644 --- a/src/app/projects/ProjectList.jsx +++ b/src/app/projects/ProjectList.jsx @@ -1,5 +1,5 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; -import {useState} from "react"; +import { useState } from "react"; const ProjectList = ({ projects, onEdit, onDelete, onHandlePageUrl }) => { const [isModalOpen, setModalOpen] = useState(false); @@ -18,90 +18,90 @@ const ProjectList = ({ projects, onEdit, onDelete, onHandlePageUrl }) => { <> - - - - - - + + + + + + - {projects - .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) - .map((project, index) => ( - - - - - - - ))} + {projects + .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) + .map((project, index) => ( + + + + + + + ))}
    ProjetMembres de léquipeNom du clientActions
    ProjetMembres de léquipeNom du clientActions
    {project.nom}{project.users.length}{project.nomClient} - - -
    {project.nom}{project.users.length}{project.nomClient} + + +
    {/* Pagination */} {projects.count && -
    -
      -
    1. - -
    2. -
    3. - 1 -
    4. -
    5. - +
    6. +
    7. + 1 +
    8. +
    9. + -
    10. -
    -
    + Next Page + + + + + + +
    } {/* Confirmation Modal */} { const [pageUrl, setPageUrl] = useState('/projects/'); const [projects, setProjects] = useState([]); const [editingProject, setEditingProject] = useState(null); - {/* errors from request*/} + {/* errors from request*/ } const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); const fetchProjects = async () => { - const { isSuccess, errors , data } = await fetchRequest(pageUrl); + const { isSuccess, errors, data } = await fetchRequest(pageUrl); if (isSuccess) { setProjects(data); setErrors(null); @@ -23,16 +23,16 @@ const Projects = () => { setErrors(errors) } }; - useEffect( () => { + useEffect(() => { - fetchProjects(); + fetchProjects(); }, [pageUrl]); // pageURL const handleAddProject = async (project) => { setLoading(true); - const { isSuccess, errors , data } = await fetchRequest('/projects/', { + const { isSuccess, errors, data } = await fetchRequest('/projects/', { method: 'POST', body: JSON.stringify(project), }); @@ -42,6 +42,7 @@ const Projects = () => { setEditingProject(null); setErrors(null); } else { + console.log(errors); console.error("Failed to add project"); setErrors(errors) } @@ -50,7 +51,7 @@ const Projects = () => { const handleEditProject = async (id, updatedProject) => { setLoading(true); - const { isSuccess, errors , data } = await fetchRequest(`/projects/${id}/`, { + const { isSuccess, errors, data } = await fetchRequest(`/projects/${id}/`, { method: 'PUT', body: JSON.stringify(updatedProject), }); @@ -94,7 +95,7 @@ const Projects = () => { return (
    - +
    diff --git a/src/app/role/RoleTableRows.jsx b/src/app/role/RoleTableRows.jsx index 5c44906..c937dc0 100644 --- a/src/app/role/RoleTableRows.jsx +++ b/src/app/role/RoleTableRows.jsx @@ -1,4 +1,4 @@ -import React, { useEffect, useRef, useState } from 'react' +import React, { useState } from 'react' import fetchRequest from '../lib/fetchRequest' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" @@ -50,7 +50,7 @@ const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { <> -

    {name}

    +

    {name}

    diff --git a/src/app/user/CreateUserForm.jsx b/src/app/user/CreateUserForm.jsx index f06c5ca..a3346f5 100644 --- a/src/app/user/CreateUserForm.jsx +++ b/src/app/user/CreateUserForm.jsx @@ -3,6 +3,7 @@ import Loader from '@/components/Loader/Loader' import fetchRequest from '../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" +import { EMAIL_REGEX } from '../lib/constants' @@ -35,7 +36,7 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { const [isLoading, setIsLoading] = useState(false) const [selectedRole, setSelectedRole] = useState(null) const { toggleNotification } = useNotification() - const [selectProjects, setSelectedProjects] = useState([]) + const [selectedProject, setSelectedProject] = useState([]) const [roles, setRoles] = useState(null) const [projects, setProjects] = useState(null) @@ -70,103 +71,109 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { getRoles() getProjects() }, []) - const handleFieldChange = (event) => setUserData({ ...userData, [event.target.name]: event.target.value }) - + const handleFieldChange = (event) => { + setUserData({ ...userData, [event.target.name]: event.target.value }) + setErrors({ ...errors, [event.target.name]: "" }) + } + const [errors, setErrors] = useState({ first_name: "", last_name: "", email: "", role: "" }) const [userData, setUserData] = useState({ email: "", password: generateRandomPassword(), first_name: "", last_name: "" }) + + const isValidFields = () => { + const localErrors = { first_name: "", last_name: "", email: "", role: "" } + if (userData.first_name === "") localErrors.first_name = "Le prénom doit être spécifier." + if (userData.last_name === "") localErrors.last_name = "Le nom doit être spécifier." + if (!selectedRole) localErrors.role = "Le rôle doit être spécifier." + if (!userData.email) localErrors.email = "L'email doit être spécifier." + else if (!EMAIL_REGEX.test(userData.email)) localErrors.email = "Votre adresse email n'est pas valide." + setErrors({ ...localErrors }) + return Object.values(localErrors).find((element) => element !== "") === undefined + } const handleSubmit = async (event) => { event.preventDefault() - setIsLoading(true) - const { data, errors, isSuccess, status } = await fetchRequest("/users/", { - method: "POST", - body: JSON.stringify({ ...userData, username: userData.email, role: selectedRole.id, project_ids: selectProjects.map((element) => element.id) }) - }) - if (isSuccess) { - setIsLoading(false) - setSelectedRole(null) - appendUser(data.data) - toggleNotification({ - visible: true, - message: "Le rôle a été créé avec succès", - type: "success" + if (isValidFields()) { + setIsLoading(true) + const { data, errors: requestErrors, isSuccess, status } = await fetchRequest("/users/", { + method: "POST", + body: JSON.stringify({ ...userData, username: userData.email, role: selectedRole, projects: selectedProject }) }) - setIsOpen(false) - } else { - setIsLoading(false) - if (errors.type === "ValidationError") { - if (errors.detail.name) { - toggleNotification({ - visible: true, - message: "Le rôle existe déjà", - type: "warning" - }) + if (isSuccess) { + setIsLoading(false) + setSelectedRole(null) + appendUser(data) + toggleNotification({ + visible: true, + message: "L'utilisateur a été créé avec succès", + type: "success" + }) + setIsOpen(false) + } else { + setIsLoading(false) + if (requestErrors.type === "ValidationError") { + if (requestErrors.detail.email) { + setErrors({ ...errors, email: "Cette adresse email est déjà utilisée." }) + } + else if (requestErrors.detail.role) { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle (le rôle peut être supprimé)", + type: "warning" + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de utilisateur", + type: "warning" + }) + setIsOpen(false) + } } else { toggleNotification({ visible: true, - message: "Erreur de validation de rôle", - type: "warning" + message: "Internal Server Error", + type: "error" }) setIsOpen(false) } + console.log(requestErrors) } - else if (status === 409) { - toggleNotification({ - visible: true, - message: "role created with 2 or more same privileges", - type: "error" - }) - setIsOpen(false) - } else if (errors.detail === "Privilege matching query does not exist.") { - toggleNotification({ - visible: true, - message: "Des privilèges que vous avez utilisés ont été supprimés.", - type: "warning" - }) - setIsOpen(false) - } else { - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - setIsOpen(false) - } - console.log(errors) } + } - const handleRoleClick = (newRole) => { - if (selectedRole?.id === newRole.id) { - setSelectedRole(null) - } else { - setSelectedRole(newRole) - } + const handleRoleChange = (event) => { + setErrors({ ...errors, role: "" }) + if (event.target.value) + setSelectedRole(event.target.value) + else setSelectedRole(null) } - const handleProjectClick = (project) => { - if (selectProjects.find((element) => element.id === project.id)) { - setSelectedProjects(selectProjects.filter((element) => element.id !== project.id)) - } else { - setSelectedProjects([...selectProjects, project]) - } + const handleProjectChange = (event) => { + if (event.target.value) + setSelectedProject([event.target.value]) + else setSelectedProject([]) } return (
    setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" />
    -

    Ajout d utilisateur

    +

    Ajout d'utilisateur

    - - + + +

    {errors.last_name}

    - - + + +

    {errors.first_name}

    - + +

    {errors.email}

    @@ -176,30 +183,43 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
    -
    - {roles.length !== 0 ? roles?.map((role) => { - const isSelected = selectedRole?.id === role.id - return
    handleRoleClick(role)} key={role.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}> - {role.name} +
    + {roles.length !== 0 ? +
    + +

    {errors.role}

    - }) :
    -

    Pas encore des habilitations

    -
    } + :
    +

    Pas encore des rôles

    +
    }
    :
    } {(projects) ?
    - +
    -
    - {projects.length !== 0 ? projects?.map((project) => { - const isSelected = selectProjects.find((element) => element.id === project.id) !== undefined - return
    handleProjectClick(project)} key={project.id} className={`${!isSelected ? 'text-neutral-400 hover:border-neutral-400 hover:text-neutral-500 border-neutral-300 bg-neutral-100' : 'text-indigo-500 hover:border-indigo-500 hover:text-indigo-500 border-indigo-400 bg-indigo-100'} will-change-contents h-6 text-sm flex items-center justify-center duration-150 delay-75 cursor-pointer text-semibold leading-[0] border-2 py-0.5 rounded-full w-fit px-2`}> - {project.nom} +
    + {projects.length !== 0 ? +
    +
    - }) :
    -

    Pas encore des projets

    -
    } + :
    +

    Pas encore des projets

    +
    }
    :
    }
    diff --git a/src/app/user/UpdateUserForm.jsx b/src/app/user/UpdateUserForm.jsx index d214f18..8d06a96 100644 --- a/src/app/user/UpdateUserForm.jsx +++ b/src/app/user/UpdateUserForm.jsx @@ -1,8 +1,205 @@ -import React from 'react' +import React, { useState, useEffect } from 'react' +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import { EMAIL_REGEX } from '../lib/constants' -const UpdateUserForm = () => { + +const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => { + const [isLoading, setIsLoading] = useState(false) + const [selectedRole, setSelectedRole] = useState(userToUpdate.role?.id || null) + const { toggleNotification } = useNotification() + const [selectedProject, setSelectedProject] = useState(userToUpdate.projects?.length ? [userToUpdate.projects[0].id] : []) + + const [roles, setRoles] = useState(null) + const [projects, setProjects] = useState(null) + useEffect(() => { + const getRoles = async () => { + const { data, errors, isSuccess } = await fetchRequest("/roles/") + if (isSuccess) { + setRoles(data) + } else { + setRoles([]) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + const getProjects = async () => { + const { isSuccess, errors, data } = await fetchRequest("/projects/"); + if (isSuccess) { + setProjects(data); + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + console.log(errors) + } + } + getRoles() + getProjects() + }, []) + const handleFieldChange = (event) => { + setUserData({ ...userData, [event.target.name]: event.target.value }) + setErrors({ ...errors, [event.target.name]: "" }) + } + const [errors, setErrors] = useState({ first_name: "", last_name: "", email: "", role: "" }) + const [userData, setUserData] = useState({ email: userToUpdate.email, first_name: userToUpdate.first_name, last_name: userToUpdate.last_name }) + + const isValidFields = () => { + const localErrors = { first_name: "", last_name: "", email: "", role: "" } + if (userData.first_name === "") localErrors.first_name = "Le prénom doit être spécifier." + if (userData.last_name === "") localErrors.last_name = "Le nom doit être spécifier." + if (!selectedRole) localErrors.role = "Le rôle doit être spécifier." + if (!userData.email) localErrors.email = "L'email doit être spécifier." + else if (!EMAIL_REGEX.test(userData.email)) localErrors.email = "Votre adresse email n'est pas valide." + setErrors({ ...localErrors }) + return Object.values(localErrors).find((element) => element !== "") === undefined + } + const handleSubmit = async (event) => { + event.preventDefault() + if (isValidFields()) { + setIsLoading(true) + const { data, errors: requestErrors, isSuccess, status } = await fetchRequest(`/users/${userToUpdate.id}/`, { + method: "PATCH", + body: JSON.stringify({ ...userData, username: userData.email, role: selectedRole, projects: selectedProject }) + }) + if (isSuccess) { + setUsers((users) => users.map((element) => element.id === userToUpdate.id ? data : element)) + setIsLoading(false) + setSelectedRole(null) + toggleNotification({ + visible: true, + message: "L'utilisateur a été modifié avec succès", + type: "success" + }) + setUserToUpdate(null) + } else { + setIsLoading(false) + if (requestErrors.type === "ValidationError") { + if (requestErrors.detail.email) { + setErrors({ ...errors, email: "Cette adresse email est déjà utilisée." }) + } + else if (requestErrors.detail.role) { + toggleNotification({ + visible: true, + message: "Erreur de validation de rôle (le rôle peut être supprimé)", + type: "warning" + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de utilisateur", + type: "warning" + }) + setUserToUpdate(null) + } + } + else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + setUserToUpdate(null) + } + console.log(requestErrors) + } + } + + } + const handleRoleChange = (event) => { + setErrors({ ...errors, role: "" }) + if (event.target.value) + setSelectedRole(event.target.value) + else setSelectedRole(null) + } + const handleProjectChange = (event) => { + if (event.target.value) + setSelectedProject([event.target.value]) + else setSelectedProject([]) + } return ( -
    UpdateUserForm
    +
    +
    + setUserToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> + +

    Modification d'utilisateur

    +
    +
    + + +

    {errors.last_name}

    +
    +
    + + +

    {errors.first_name}

    +
    +
    +
    + + +

    {errors.email}

    +
    + {(roles) ?
    +
    + +
    +
    + {roles.length !== 0 ? +
    + +

    {errors.role}

    +
    + :
    +

    Pas encore des rôles

    +
    } +
    +
    :
    } + {(projects) ?
    +
    + +
    +
    + {projects.length !== 0 ? +
    + +
    + :
    +

    Pas encore des projets

    +
    } +
    +
    :
    } +
    + +
    + +
    +
    ) } diff --git a/src/app/user/UserTableRow.jsx b/src/app/user/UserTableRow.jsx index 1860ee8..e057532 100644 --- a/src/app/user/UserTableRow.jsx +++ b/src/app/user/UserTableRow.jsx @@ -5,7 +5,7 @@ import ConfirmationModal from '../ui/ConfirmationModal' import { useNotification } from '@/context/NotificationContext' import fetchRequest from '../lib/fetchRequest' -const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projects }) => { +const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projects, setUserToUpdate }) => { const { toggleNotification } = useNotification() const [isModalOpen, setModalOpen] = useState(false); const showDeletePopup = () => { @@ -23,13 +23,13 @@ const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projec } else if (status == 404) { toggleNotification({ visible: true, - message: "L'utilisateur n'a pas été trouvé", + message: "L'utilisateur n'a pas été trouvé", type: "warning" }) - } else if (errors.detail?.indexOf("Cannot delete some instances of model 'Role'") !== -1) { + } else if (errors.detail?.indexOf("Cannot delete some instances of model") !== -1) { toggleNotification({ visible: true, - message: "Impossible de supprimer cet utilisateur car il est attribué à des utilisateurs", + message: "Impossible de supprimer cet utilisateur car il est attribué à d'autre objets", type: "warning" }) } @@ -66,7 +66,7 @@ const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projec
    -
    - + :
    }
    ) diff --git a/src/app/role/UpdateRoleForm.jsx b/src/app/role/UpdateRoleForm.jsx index e980dbf..348ebfc 100644 --- a/src/app/role/UpdateRoleForm.jsx +++ b/src/app/role/UpdateRoleForm.jsx @@ -109,13 +109,13 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv
    setRoleToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> -
    + {(privileges) ?

    Modification de Rôle

    - {(privileges) ?
    +
    {!isAllSelected ? "Sélectionner tout" : "désélectionner"}
    @@ -128,13 +128,13 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv

    Pas encore des habilitations

    }
    -
    :
    } +
    - + :
    }
    ) diff --git a/src/app/user/CreateUserForm.jsx b/src/app/user/CreateUserForm.jsx index a3346f5..1b4540d 100644 --- a/src/app/user/CreateUserForm.jsx +++ b/src/app/user/CreateUserForm.jsx @@ -156,7 +156,7 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
    setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> -
    + {(roles && projects) ?

    Ajout d'utilisateur

    @@ -179,7 +179,7 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
    - {(roles) ?
    +
    @@ -200,8 +200,8 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {

    Pas encore des rôles

    }
    -
    :
    } - {(projects) ?
    +
    +
    @@ -221,13 +221,13 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {

    Pas encore des projets

    }
    -
    :
    } +
    - + :
    }
    ) diff --git a/src/app/user/UpdateUserForm.jsx b/src/app/user/UpdateUserForm.jsx index 8d06a96..1e62cce 100644 --- a/src/app/user/UpdateUserForm.jsx +++ b/src/app/user/UpdateUserForm.jsx @@ -130,7 +130,7 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => {
    setUserToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> -
    + {(roles && projects) ?

    Modification d'utilisateur

    @@ -149,7 +149,7 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => {

    {errors.email}

    - {(roles) ?
    +
    @@ -170,8 +170,8 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => {

    Pas encore des rôles

    }
    -
    :
    } - {(projects) ?
    +
    +
    @@ -191,13 +191,13 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => {

    Pas encore des projets

    }
    -
    :
    } +
    - + :
    }
    ) diff --git a/src/app/user/page.jsx b/src/app/user/page.jsx index edba6ff..de82ec7 100644 --- a/src/app/user/page.jsx +++ b/src/app/user/page.jsx @@ -1,5 +1,5 @@ 'use client'; -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useDeferredValue } from 'react' import CreateUserForm from './CreateUserForm' import UpdateUserForm from './UpdateUserForm' import AddIcon from "@/static/image/svg/add.svg" @@ -8,34 +8,49 @@ import fetchRequest from '../lib/fetchRequest'; import { isArray } from '../lib/TypesHelper'; import { useNotification } from '@/context/NotificationContext'; import UserTableRow from './UserTableRow'; - +import { PAGINATION_SIZE } from '../lib/constants'; +import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" +import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" const UserPage = () => { const [users, setUsers] = useState([]) + const [paginationData, setPaginationData] = useState(null) const [isLoading, setIsLoading] = useState(true) const [openCreatePopup, setOpenCreatePopup] = useState(null) const [userToUpdate, setUserToUpdate] = useState(null) const { toggleNotification } = useNotification() - + const [query, setQuery] = useState(''); + const getUsers = async (pageNumber = 1, signal) => { + setIsLoading(true) + if (search) var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}&search=${query}`, { signal: signal }) + else var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}`) + setIsLoading(false) + if (isSuccess) { + console.log(data) + setUsers(data.results) + setPaginationData({ pagesNumber: Math.ceil((data.count || 0) / PAGINATION_SIZE), currentPage: pageNumber }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } useEffect(() => { - const getUsers = async () => { - const { data, errors, isSuccess } = await fetchRequest("/users") - setIsLoading(false) - if (isSuccess) { - console.log(data) - setUsers(data) - } else { - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - } + const controller = new AbortController() + const signal = controller.signal + getUsers(1, signal) + return () => { + controller.abort("fetching another users") } - getUsers() - }, []) + }, [query]) const appendUser = (newUser) => { setUsers([newUser, ...users]) } + + const handleSearchChange = (event) => { + setQuery(event.target.value) + } return (
    @@ -48,12 +63,16 @@ const UserPage = () => {

    Utilisateur

    - {(isLoading) &&
    } +
    + +
    + {(isLoading) &&
    } {(!isLoading) && <> {(!isArray(users) || users?.length === 0) - ?
    + ?

    Pas encore des utilisateurs

    - :
    + : +
    @@ -66,8 +85,31 @@ const UserPage = () => { return })}
    Nom
    -
    - }} +
    } + + } +
    + {(paginationData) &&
    + {(paginationData.currentPage > 1) &&
    getUsers(paginationData.currentPage - 1)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> + +
    } + {paginationData && Array.from({ length: paginationData.pagesNumber }, (_, index) => index).map((element, index) => { + if (element + 1 === paginationData.currentPage + 3) return

    + ... +

    + else if (paginationData.currentPage === element + 1) return

    + {element + 1} +

    + else if (paginationData.currentPage < element + 3 && element - paginationData.currentPage < 3) return

    getUsers(element + 1)} key={element} className='h-8 w-9 hover:bg-neutral-200 cursor-pointer duration-150 delay-75 font-bold text-neutral-700 text-sm flex items-center justify-center' > + {element + 1} +

    + else return <> + })} + {(paginationData.currentPage !== paginationData.pagesNumber) &&
    getUsers(paginationData.currentPage + 1)} className='flex h-8 w-9 items-center hover:bg-neutral-200 duration-150 delay-75 cursor-pointer justify-center'> + +
    } +
    } +
    diff --git a/src/static/image/svg/chevron-left.svg b/src/static/image/svg/chevron-left.svg new file mode 100644 index 0000000..bfbedda --- /dev/null +++ b/src/static/image/svg/chevron-left.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/chevron-right.svg b/src/static/image/svg/chevron-right.svg new file mode 100644 index 0000000..49c8a53 --- /dev/null +++ b/src/static/image/svg/chevron-right.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From 5cb3513e1ef15378af5ce8ec297d6970bb15f3ff Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Thu, 30 May 2024 15:42:05 +0100 Subject: [PATCH 28/79] updated planning interfaces --- src/app/globals.css | 62 +++++ src/app/planning/PlanningTable.jsx | 48 ++-- src/app/planning/page.jsx | 222 +++++++++++++++--- .../type-presence-semaines-jours/page.jsx | 100 -------- .../EntityForm.jsx | 0 .../EntityList.jsx | 6 +- src/app/planning/type-presence/page.jsx | 62 +++++ src/app/projects/ProjectForm.jsx | 4 +- src/app/ui/Header.jsx | 4 +- src/app/ui/LogoutButton.js | 2 +- src/app/ui/SideBar.jsx | 64 +---- 11 files changed, 345 insertions(+), 229 deletions(-) delete mode 100644 src/app/planning/type-presence-semaines-jours/page.jsx rename src/app/planning/{type-presence-semaines-jours => type-presence}/EntityForm.jsx (100%) rename src/app/planning/{type-presence-semaines-jours => type-presence}/EntityList.jsx (79%) create mode 100644 src/app/planning/type-presence/page.jsx diff --git a/src/app/globals.css b/src/app/globals.css index b6ba7d3..83bb86b 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -30,4 +30,66 @@ ::-webkit-scrollbar-track { background-color: rgba(0, 0, 0, 0.1); /* Color of the track */ +} + +/* Tab content - closed */ +.tab-content { + max-height: 0; + -webkit-transition: max-height .35s; + -o-transition: max-height .35s; + transition: max-height .35s; +} +/* :checked - resize to full height */ +.tab input:checked ~ .tab-content { + max-height: 100%; +} +/* Label formatting when open */ +.tab input:checked + label{ + /*@apply text-xl p-5 border-l-2 border-indigo-500 bg-gray-100 text-indigo*/ + font-size: 1.25rem; /*.text-xl*/ + padding: 1.25rem; /*.p-5*/ + /*border-left-width: 2px; !*.border-l-2*!*/ + /*border-color: #93a84c; !*.border-indigo*!*/ + /*color: #93a84c; !*.text-indigo*!*/ +} +/* Icon */ +.tab label::after { + float:right; + right: 0; + top: 0; + display: block; + width: 1.5em; + height: 1.5em; + line-height: 1.25; + font-size: 1.25rem; + text-align: center; + -webkit-transition: all .35s; + -o-transition: all .35s; + transition: all .35s; +} +/* Icon formatting - closed */ +.tab input[type=checkbox] + label::after { + content: "+"; + font-weight:bold; /*.font-bold*/ + border-width: 1px; /*.border*/ + border-radius: 9999px; /*.rounded-full */ + border-color: #5c5c5c; /*.border-grey*/ +} +.tab input[type=radio] + label::after { + content: "\25BE"; + font-weight:bold; /*.font-bold*/ + border-width: 1px; /*.border*/ + border-radius: 9999px; /*.rounded-full */ + border-color: #5c5c5c; /*.border-grey*/ +} +/* Icon formatting - open */ +.tab input[type=checkbox]:checked + label::after { + transform: rotate(315deg); + background-color: #93a84c; /*.bg-indigo*/ + color: #f8fafc; /*.text-grey-lightest*/ +} +.tab input[type=radio]:checked + label::after { + transform: rotateX(180deg); + background-color: #93a84c; /*.bg-indigo*/ + color: #f8fafc; /*.text-grey-lightest*/ } \ No newline at end of file diff --git a/src/app/planning/PlanningTable.jsx b/src/app/planning/PlanningTable.jsx index 4ddc99b..6b90c54 100644 --- a/src/app/planning/PlanningTable.jsx +++ b/src/app/planning/PlanningTable.jsx @@ -2,35 +2,18 @@ import React, {useEffect, useState} from 'react'; import fetchRequest from "@/app/lib/fetchRequest"; import {useNotification} from "@/context/NotificationContext"; -const PlanningTable = ({ data, onTypePresenceChange }) => { +const PlanningTable = ({ data, typePresences, onTypePresenceChange }) => { // fetch type presence - const [typePresences, setTypePresences] = useState([]); const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); const {toggleNotification} = useNotification() - const [selectedTypePresence, setSelectedTypePresence] = useState(); - - const fetchTypePresences = async () => { - const { isSuccess, errors, data } = await fetchRequest('/type-presences/'); - if (isSuccess) { - setTypePresences(data); - setErrors(null); - } - else { - console.error("Failed to fetch type presences"); - setErrors(errors) - } - } - useEffect(() => { - fetchTypePresences() - }, []); return (
    - + - + @@ -40,19 +23,20 @@ const PlanningTable = ({ data, onTypePresenceChange }) => { {data.map((row, rowIndex) => ( - - + + {row.days.map((day, dayIndex) => ( - ))} diff --git a/src/app/planning/page.jsx b/src/app/planning/page.jsx index 42a6408..f8c140a 100644 --- a/src/app/planning/page.jsx +++ b/src/app/planning/page.jsx @@ -4,20 +4,28 @@ import Dropdown from "@/app/ui/Dropdown"; import PlanningTable from "@/app/planning/PlanningTable"; import fetchRequest from "@/app/lib/fetchRequest"; import {useNotification} from "@/context/NotificationContext"; +import ConfirmationModal from "@/app/ui/ConfirmationModal"; const PlanningPage = () => { + const blankData = { + planning_data: [ + {week: 1, days: [{}, {}, {}, {}, {}]}, + {week: 2, days: [{}, {}, {}, {}, {}]}, + {week: 3, days: [{}, {}, {}, {}, {}]}, + {week: 4, days: [{}, {}, {}, {}, {}]}, + {week: 5, days: [{}, {}, {}, {}, {}]}, + ] + } const [projects, setProjects] = useState([]); const [selectedProject, setSelectedProject] = useState(''); - const [planningData, setPlanningData] = useState([ - {week: 'Semaine 1', days: ['', '', '', '', '']}, - {week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel']}, - {week: 'Semaine 3', days: ['', '', '', '', '']}, - {week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', '']}, - {week: 'Semaine 5', days: ['Présentiel', 'Présentiel', '', '', '']}, - ]); + const [planningData, setPlanningData] = useState(blankData); const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); - const {toggleNotification} = useNotification() + const {toggleNotification} = useNotification(); + const [typePresences, setTypePresences] = useState([]); + const [isModalOpen, setModalOpen] = useState(false); + + const fetchProjects = async () => { const {isSuccess, errors, data} = await fetchRequest('/projects/'); @@ -37,39 +45,148 @@ const PlanningPage = () => { const handleProjectChange = (e) => setSelectedProject(e.target.value); const fetchPlanningData = async () => { - // Fetch planning data from your API based on selectedProject, selectedWeek, and selectedDay - // Here we'll use dummy data for demonstration purposes - const data = [ - {week: 'Semaine 1', days: ['', '', '', '', '']}, - {week: 'Semaine 2', days: ['Présentiel', '', 'Présentiel', '', 'Présentiel']}, - {week: 'Semaine 3', days: ['', '', '', '', '']}, - {week: 'Semaine 4', days: ['Présentiel', 'Présentiel', '', '', '']}, - {week: 'Semaine 5', days: ['Présentiel', 'Présentiel', '', '', '']}, - ]; - setPlanningData(data); + if (!selectedProject) return; + + // const { isSuccess, errors, data } = await fetchRequest(`/planning/?project=${selectedProject}`); + const {isSuccess, errors, data} = await fetchRequest(`/plannings/project/${selectedProject}/`); + if (isSuccess) { + // if the project have no data the response is [] in this case we should set the blankData + if (data.length === 0) { + setPlanningData(blankData); + toggleNotification({ + visible: true, + message: `Projet ${selectedProject} n'a pas de données de planification`, + type: "warning" + }) + setErrors(null); + return; + } + setPlanningData(data[0]); + setErrors(null); + } else { + console.error("Failed to fetch planning data"); + setPlanningData(blankData) + setErrors(errors); + } }; + useEffect(() => { + fetchPlanningData(); + }, [selectedProject]); + + const fetchTypePresences = async () => { + const {isSuccess, errors, data} = await fetchRequest('/type-presences/'); + if (isSuccess) { + setTypePresences(data); + setErrors(null); + } else { + console.error("Failed to fetch type presences"); + setErrors(errors) + } + } + useEffect(() => { + fetchTypePresences() + }, []); const handleTypePresenceChange = (weekIndex, dayIndex, value) => { - const updatedData = [...planningData]; - updatedData[weekIndex].days[dayIndex] = value; + const updatedData = {...planningData}; + // get the presence type by id (value is a string convert it to int + const typePresence = typePresences.find(typePresence => typePresence.id === parseInt(value)); + console.log(typePresence) + + // the value I want to add should be {"id": 1, "nom": "Travail à Temps Partiel"} and not just the id + updatedData.planning_data[weekIndex].days[dayIndex] = typePresence; setPlanningData(updatedData); + console.log(value) + console.log(updatedData); + }; + + const handleSave = async () => { + setLoading(true); + const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; + console.log({id_project: selectedProject, planning_data: planningData.planning_data}) + const {isSuccess, errors} = await fetchRequest(`/plannings/`, { + method: 'POST', + body: JSON.stringify(requestBody) + }); + setLoading(false); + if (isSuccess) { + toggleNotification({ + visible: true, + message: 'Planning data saved successfully', + type: 'success' + }); + fetchPlanningData() + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Failed to save planning data', + type: 'error' + }); + } + }; + + + + const handleUpdate = async () => { + setLoading(true); + const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; + const {isSuccess, errors} = await fetchRequest(`/plannings/`, { + method: 'PUT', + body: JSON.stringify(requestBody) + }); + setLoading(false); + if (isSuccess) { + toggleNotification({ + visible: true, + message: 'Planning data updated successfully', + type: 'success' + }); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Failed to update planning data', + type: 'error' + }); + } + } + + const handleDelete = async () => { + setLoading(true); + // dlete by planning id not project id + const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { + method: 'DELETE' + }); + setLoading(false); + if (isSuccess) { + setPlanningData(blankData); + toggleNotification({ + visible: true, + message: 'Planning data deleted successfully', + type: 'success' + }); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Failed to delete planning data', + type: 'error' + }); + } + }; + const handleDeleteClick = () => { + setModalOpen(true); + }; + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); }; return (

    Planning

    -
    - -
    - {/*project using select*/} -
    - + +
    + {/* crud buttons*/} +
    + + {planningData.id ? + <> + + + : + + } +
    + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Are you sure you want to delete this planning data?`} + />
    ); }; diff --git a/src/app/planning/type-presence-semaines-jours/page.jsx b/src/app/planning/type-presence-semaines-jours/page.jsx deleted file mode 100644 index c29923c..0000000 --- a/src/app/planning/type-presence-semaines-jours/page.jsx +++ /dev/null @@ -1,100 +0,0 @@ -"use client"; - -import {useEffect, useState} from 'react'; -import fetchRequest from "@/app/lib/fetchRequest"; -import EntityList from "@/app/planning/type-presence-semaines-jours/EntityList"; -import EntityForm from "@/app/planning/type-presence-semaines-jours/EntityForm"; -import {useNotification} from "@/context/NotificationContext"; - -const ManagePage = () => { - const [typePresences, setTypePresences] = useState([]); - const [semaines, setSemaines] = useState([]); - const [jours, setJours] = useState([]); - const [editingEntity, setEditingEntity] = useState({ entity: null, id: null }); - const { toggleNotification } = useNotification() - - const fetchData = async () => { - const typePresencesResponse = await fetchRequest('/type-presences/'); - const semainesResponse = await fetchRequest('/semaines/'); - const joursResponse = await fetchRequest('/jours/'); - - if (typePresencesResponse.isSuccess) setTypePresences(typePresencesResponse.data); - if (semainesResponse.isSuccess) setSemaines(semainesResponse.data); - if (joursResponse.isSuccess) setJours(joursResponse.data); - }; - - useEffect(() => { - fetchData(); - }, []); - - const handleDelete = async (endpoint, id, setState, currentState) => { - const response = await fetchRequest(`/${endpoint}/${id}/`, {method: 'DELETE'}); - if (response.isSuccess) { - setState(currentState.filter(item => item.id !== id)); - toggleNotification({ - visible: true, - message: `${currentState.find(item => item.id === id).nom} a été supprimé avec succès`, - type: "success" - }) - } - await fetchData(); - }; - - return ( - <> -

    Manage Entities

    -
    -
    -

    Type Presence

    - setEditingEntity({ entity: null, id: null })} - /> - setEditingEntity({ entity: 'type-presences', id })} - /> -
    - {/*
    */} - {/*

    Semaines

    */} - {/* setEditingEntity({ entity: null, id: null })}*/} - {/* />*/} - {/* setEditingEntity({ entity: 'semaines', id })}*/} - {/* />*/} - {/*
    */} - {/*
    */} - {/*

    Jours

    */} - {/* setEditingEntity({ entity: null, id: null })}*/} - {/* />*/} - {/* setEditingEntity({ entity: 'jours', id })}*/} - {/* />*/} - {/*
    */} -
    - - ); -}; - -export default ManagePage; diff --git a/src/app/planning/type-presence-semaines-jours/EntityForm.jsx b/src/app/planning/type-presence/EntityForm.jsx similarity index 100% rename from src/app/planning/type-presence-semaines-jours/EntityForm.jsx rename to src/app/planning/type-presence/EntityForm.jsx diff --git a/src/app/planning/type-presence-semaines-jours/EntityList.jsx b/src/app/planning/type-presence/EntityList.jsx similarity index 79% rename from src/app/planning/type-presence-semaines-jours/EntityList.jsx rename to src/app/planning/type-presence/EntityList.jsx index a829627..372907b 100644 --- a/src/app/planning/type-presence-semaines-jours/EntityList.jsx +++ b/src/app/planning/type-presence/EntityList.jsx @@ -32,13 +32,15 @@ const EntityList = ({ title, items, setState, handleDelete, handleEdit }) => { ))} @@ -162,6 +215,12 @@ const AffectingZoneProject = () => { :
    } + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer l'affectation "${selectedAffectaionToDelete?.id_project.nom}"?`} + /> ); -- GitLab From 5024acce693e53ff25fb7e8c273541e7fa9a9247 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Thu, 6 Jun 2024 13:24:36 +0100 Subject: [PATCH 42/79] reservation components --- .../privilege/CreatePrivilegeForm.jsx | 2 +- src/app/(dashboard)/reservation/TableUI.jsx | 45 +++++++ src/app/(dashboard)/reservation/ZoneUI.jsx | 17 +++ src/app/(dashboard)/reservation/page.jsx | 120 ++++++++++++++++++ 4 files changed, 183 insertions(+), 1 deletion(-) create mode 100644 src/app/(dashboard)/reservation/TableUI.jsx create mode 100644 src/app/(dashboard)/reservation/ZoneUI.jsx create mode 100644 src/app/(dashboard)/reservation/page.jsx diff --git a/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx index 97c87c0..861c65b 100644 --- a/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx +++ b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx @@ -59,7 +59,7 @@ const CreatePrivilegeForm = ({ appendPrivilege }) => { } } return ( -
    +

    Ajout d'habilitation

    diff --git a/src/app/(dashboard)/reservation/TableUI.jsx b/src/app/(dashboard)/reservation/TableUI.jsx new file mode 100644 index 0000000..6a22027 --- /dev/null +++ b/src/app/(dashboard)/reservation/TableUI.jsx @@ -0,0 +1,45 @@ +import React from 'react' + +const TableUI = ({ tableName, places }) => { + function groupConsecutive(arr) { + if (arr.length === 0) { + return []; + } + + const grouped = []; + var counter = 0 + while (counter < arr.length) { + if (counter + 1 < arr.length) { + grouped.push([arr[counter], arr[counter + 1]]) + } + else { + grouped.push([arr[counter]]) + } + counter += 2; + } + + return grouped; + } + const proccessedPlaces = groupConsecutive(places).reverse() + return ( +
    + {proccessedPlaces.map((element, index) => { + return
    +
    +
    + {element[0]} +
    +
    +
    + {(element.length > 1) &&
    +
    + {element[1]} +
    +
    } +
    + })} +
    + ) +} + +export default TableUI \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx new file mode 100644 index 0000000..f30e8b0 --- /dev/null +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import TableUI from './TableUI' + +const ZoneUI = ({ tables, zoneName }) => { + return ( +
    +

    {zoneName}

    +
    + {tables.map((table, index) => { + return + })} +
    +
    + ) +} + +export default ZoneUI \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx new file mode 100644 index 0000000..414ce0d --- /dev/null +++ b/src/app/(dashboard)/reservation/page.jsx @@ -0,0 +1,120 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import ZoneUI from './ZoneUI' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' + +const Reservation = () => { + const [selectedFloor, setSelectedFloor] = useState(5) + const [floors, setFloors] = useState([]) + const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) + const [isLoadingData, setIsLoadingData] = useState(false) + + useEffect(() => { + const getAllFloors = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) + setIsLoadingSelectsData(false) + if (isSuccess) { + setFloors(data) + } else { + setFloors([]) + } + } catch (error) { + setIsLoadingSelectsData(false) + console.log(error) + } + } + getAllFloors() + }, []) + + const [zones, setZones] = useState([ + { + zoneName: "A", + tables: [ + { + tableName: 1, + places: [1, 2, 3, 4, 5, 6, 7, 8] + }, + // { + // tableName: 2, + // places: [1, 2, 3, 4, 5, 6, 7, 8] + // }, + // { + // tableName: 3, + // places: [1, 2, 3, 4, 5, 6, 7] + // } + ] + }, + { + zoneName: "B", + tables: [ + { + tableName: 1, + places: [1, 2, 3, 4, 5, 6, 7, 8] + }, + // { + // tableName: 2, + // places: [1, 2, 3, 4, 5, 6, 8] + // } + ] + } + ]) + if (isLoadingSelectsData) + return
    + +
    + return ( +
    +
    + + + + + +
    +
    +
    +
    +

    Indisponible

    +
    +
    +
    +

    Disponible

    +
    +
    +
    +

    Réservé par vous

    +
    +
    +
    +

    Confirmé

    +
    +
    + {(!isLoadingData) + ?
    + {zones.map((zone, index) => { + return + })} +
    + :
    + +
    + } +
    + ) +} + +export default Reservation \ No newline at end of file -- GitLab From fac19be80fa6b44240c8db47c5c042aafb46c941 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Fri, 7 Jun 2024 09:17:54 +0100 Subject: [PATCH 43/79] zone exception full availability --- .../assign_zone_project/AssignProject.jsx | 23 +++++++++++++++---- .../(dashboard)/assign_zone_project/page.jsx | 6 +---- 2 files changed, 19 insertions(+), 10 deletions(-) diff --git a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx index 60fd26d..afafe64 100644 --- a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx +++ b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx @@ -9,7 +9,7 @@ import AddIcon from "@/static/image/svg/add.svg" import fetchRequest from "@/app/lib/fetchRequest"; -const AssignProject = ({ setIsOpen, listProjects }) => { +const AssignProject = ({ setIsOpen, listProjects, affectations }) => { const [loading, setLoading] = useState(false) const [projects, setProjects] = useState([]) const [zones, setZones] = useState([]) @@ -22,11 +22,12 @@ const AssignProject = ({ setIsOpen, listProjects }) => { const [ collabsAttributed, setCollabsAttributed ] = useState(0) const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) const [ otherPlaces, setOtherPlaces ] = useState([]) + const [ affectaionsRelatedToZones, setAffectationsRelatedToZones ] = useState([]) const { toggleNotification } = useNotification() const attributedCollabsRef = useRef() - + console.log(affectations) useEffect(() => { const fetchProjectsandZones = async () => { setLoading(true) @@ -80,11 +81,18 @@ const AssignProject = ({ setIsOpen, listProjects }) => { if(selectedDay && selectedWeek) fetchProjectsandZones() }, [selectedDay, selectedWeek]) + const handleZoneSelection = async (e) => { const zone_id = e.target.value + const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) try{ - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces/${zone_id}`, {method: 'GET'}) + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })}) if(isSuccess){ + console.log(data.places) setSelectedZone(zone_id) setPlaces(data.places) setOtherPlaces([]) @@ -144,9 +152,15 @@ const AssignProject = ({ setIsOpen, listProjects }) => { const handleOtherZoneSelection = async (e) => { const zone_id = e.target.value + const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) setSelectedOtherZone(zone_id) try{ - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces/${zone_id}`, {method: 'GET'}) + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })} + ) if(isSuccess){ setOtherPlaces(data.places) }else{ @@ -237,7 +251,6 @@ const AssignProject = ({ setIsOpen, listProjects }) => { } - console.log("other zone ::::" , selectedOtherZone) return (
    diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/assign_zone_project/page.jsx index 1c49160..85c5bee 100644 --- a/src/app/(dashboard)/assign_zone_project/page.jsx +++ b/src/app/(dashboard)/assign_zone_project/page.jsx @@ -67,7 +67,6 @@ const AffectingZoneProject = () => { const handleDeleteAffectation = async () => { try{ - console.log("qsdsqdqsdsqdq") var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, {method: 'DELETE'}) if(isSuccess){ toggleNotification({ @@ -106,18 +105,15 @@ const AffectingZoneProject = () => { const handleConfirmDelete = () => { - console.log("qsdsq") handleDeleteAffectation(); - console.log("fdsfsd") setModalOpen(false); setSelectedAffectationToDelete(null); }; - return (
    - {isOpen && } + {isOpen && }

    List des Projets attribuer

    Il y a des projets qui ne sont pas complètement affecter. -- GitLab From 7feda36126564253cff447d279b9748931affef6 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 10 Jun 2024 10:41:16 +0100 Subject: [PATCH 44/79] to fix date --- .../assign_zone_project/AssignProject.jsx | 142 ++++++------- .../(dashboard)/assign_zone_project/page.jsx | 66 +++---- src/app/(dashboard)/reservation/TableUI.jsx | 58 +++++- src/app/(dashboard)/reservation/ZoneUI.jsx | 8 +- src/app/(dashboard)/reservation/page.jsx | 187 ++++++++++++------ 5 files changed, 289 insertions(+), 172 deletions(-) diff --git a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx index afafe64..a5e90b0 100644 --- a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx +++ b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx @@ -13,16 +13,16 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { const [loading, setLoading] = useState(false) const [projects, setProjects] = useState([]) const [zones, setZones] = useState([]) - const [ selectedDay, setSelectedDay ] = useState('') - const [ selectedWeek, setSelectedWeek ] = useState('') - const [ selectedZone, setSelectedZone ] = useState(null) - const [ selectedProject, setSelectedProject ] = useState(null) - const [ places , setPlaces ] = useState([]) - const [ nbrCollabs, setNbrCollabs ] = useState(0) - const [ collabsAttributed, setCollabsAttributed ] = useState(0) - const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) - const [ otherPlaces, setOtherPlaces ] = useState([]) - const [ affectaionsRelatedToZones, setAffectationsRelatedToZones ] = useState([]) + const [selectedDay, setSelectedDay] = useState('') + const [selectedWeek, setSelectedWeek] = useState('') + const [selectedZone, setSelectedZone] = useState(null) + const [selectedProject, setSelectedProject] = useState(null) + const [places, setPlaces] = useState([]) + const [nbrCollabs, setNbrCollabs] = useState(0) + const [collabsAttributed, setCollabsAttributed] = useState(0) + const [selectedOtherZone, setSelectedOtherZone] = useState(null) + const [otherPlaces, setOtherPlaces] = useState([]) + const [affectaionsRelatedToZones, setAffectationsRelatedToZones] = useState([]) const { toggleNotification } = useNotification() @@ -32,12 +32,12 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { const fetchProjectsandZones = async () => { setLoading(true) try { - const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${selectedWeek}/${selectedDay}`, {method: 'GET'}) - if(isSuccess){ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject/${selectedWeek}/${selectedDay}`, { method: 'GET' }) + if (isSuccess) { setCollabsAttributed(0) setNbrCollabs(0) setPlaces([]) - if ( (data.projects && data.projects.length === 0) && (data.zones && data.zones.length === 0)){ + if ((data.projects && data.projects.length === 0) && (data.zones && data.zones.length === 0)) { toggleNotification({ visible: true, message: "Il y'a pas de projets et de zones pour cette semaine et ce jour.", @@ -45,29 +45,29 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { }) setProjects([]) setZones([]) - }else{ - if(data.projects && data.projects.length === 0){ + } else { + if (data.projects && data.projects.length === 0) { toggleNotification({ visible: true, message: "Il y'a pas de projets pour cette semaine et ce jour.", type: "warning" }) setProjects([]) - }else{ + } else { setProjects(data.projects) } - if(data.zones && data.zones.length === 0){ + if (data.zones && data.zones.length === 0) { toggleNotification({ visible: true, message: "Il y'a pas de zones pour cette semaine et ce jour.", type: "warning" }) setZones([]) - }else{ + } else { setZones(data.zones) } } - }else{ + } else { // handle error setLoading(false) } @@ -78,58 +78,60 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { setLoading(false) } } - if(selectedDay && selectedWeek) fetchProjectsandZones() + if (selectedDay && selectedWeek) fetchProjectsandZones() }, [selectedDay, selectedWeek]) const handleZoneSelection = async (e) => { const zone_id = e.target.value - const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) - try{ + const related_affecations = affectations.filter((element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) + try { const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, - {method: 'POST', body: JSON.stringify({ - related_affecations: related_affecations, - zone_id: zone_id - })}) - if(isSuccess){ + { + method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + }) + }) + if (isSuccess) { console.log(data.places) setSelectedZone(zone_id) setPlaces(data.places) setOtherPlaces([]) setSelectedOtherZone(null) - }else{ + } else { // handle error setPlaces([]) } - }catch(error){ + } catch (error) { console.log(error) } } const handleProjectSelection = async (e) => { const project_id = e.target.value - try{ - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingCollabs/${project_id}`, {method: 'GET'}) - if(isSuccess){ + try { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingCollabs/${project_id}`, { method: 'GET' }) + if (isSuccess) { setSelectedProject(project_id) setNbrCollabs(data.user_count) setOtherPlaces([]) setSelectedOtherZone(null) - }else{ + } else { // handle error setNbrCollabs(0) } - }catch(error){ + } catch (error) { console.log(error) } } - useEffect( () => { - if(nbrCollabs > 0 && places.length > 0){ - if( nbrCollabs <= places.length){ + useEffect(() => { + if (nbrCollabs > 0 && places.length > 0) { + if (nbrCollabs <= places.length) { setCollabsAttributed(nbrCollabs) attributedCollabsRef.current = nbrCollabs - }else{ + } else { setCollabsAttributed(places.length) attributedCollabsRef.current = places.length } @@ -137,12 +139,12 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { }, [nbrCollabs, places]) - const handleAddCollab = () =>{ + const handleAddCollab = () => { setCollabsAttributed(collabsAttributed + 1) attributedCollabsRef.current = collabsAttributed + 1 } - const handleMinusCollab = () =>{ + const handleMinusCollab = () => { setCollabsAttributed(collabsAttributed - 1) attributedCollabsRef.current = collabsAttributed - 1 } @@ -152,21 +154,23 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { const handleOtherZoneSelection = async (e) => { const zone_id = e.target.value - const related_affecations = affectations.filter( (element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) + const related_affecations = affectations.filter((element) => element.jour == selectedDay && element.semaine == selectedWeek && element.id_zone.id == zone_id).map(element => element.id) setSelectedOtherZone(zone_id) - try{ + try { const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, - {method: 'POST', body: JSON.stringify({ - related_affecations: related_affecations, - zone_id: zone_id - })} + { + method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + }) + } ) - if(isSuccess){ + if (isSuccess) { setOtherPlaces(data.places) - }else{ + } else { // handle error } - }catch(error){ + } catch (error) { console.log(error) toggleNotification({ visible: true, @@ -176,8 +180,8 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { } } - useEffect( () => { - if( (collabsAttributed - places.length) <= 0 && selectedOtherZone ) { + useEffect(() => { + if ((collabsAttributed - places.length) <= 0 && selectedOtherZone) { setSelectedOtherZone(null) setOtherPlaces([]) } @@ -193,27 +197,27 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { nombre_personnes: collabsAttributed, places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, - places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) + places: (collabsAttributed > places.length) ? places.map((element) => element.id) : places.map((element, index) => index < collabsAttributed && element.id).filter(id => id !== false) } - if( selectedOtherZone && otherPlaces.length > 0){ + if (selectedOtherZone && otherPlaces.length > 0) { finalData.otherZone = { id_zone: selectedOtherZone, - places_disponibles: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? 0 : otherPlaces.length - (collabsAttributed - places.length), - places_occuper: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.length : collabsAttributed - places.length, - places: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.map( (element) => element.id) : otherPlaces.map( (element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) + places_disponibles: ((collabsAttributed - places.length) > otherPlaces.length) ? 0 : otherPlaces.length - (collabsAttributed - places.length), + places_occuper: ((collabsAttributed - places.length) > otherPlaces.length) ? otherPlaces.length : collabsAttributed - places.length, + places: ((collabsAttributed - places.length) > otherPlaces.length) ? otherPlaces.map((element) => element.id) : otherPlaces.map((element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) } } - try{ - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) - if(isSuccess){ - listProjects( (prevListProjects) => [...prevListProjects, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])]) + try { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, { method: 'POST', body: JSON.stringify(finalData) }) + if (isSuccess) { + listProjects((prevListProjects) => [...prevListProjects, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])]) toggleNotification({ visible: true, message: "Projet affecté avec succès.", type: "success" }) closePopup(false) - }else{ + } else { console.log(errors) toggleNotification({ visible: true, @@ -221,7 +225,7 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { type: "error" }) } - }catch(error){ + } catch (error) { console.log(error) toggleNotification({ visible: true, @@ -255,7 +259,7 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => {

    Attribuer un projet à une zone.

    - {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> + { setIsOpen(false) }} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" />

    Veuillez sélectionner une date

    @@ -285,7 +289,7 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => {
    {(!loading && (projects && projects.length)) ? - { handleProjectSelection(e) }} disabled={projects.length === 0 || loading} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none'> {projects.map((element, index) => )} @@ -313,7 +317,7 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => {

    : {places.length}

    - {collabsAttributed > 0 && + {collabsAttributed > 0 && <>
    Collaborateurs attribués @@ -323,7 +327,7 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { -
    @@ -335,7 +339,7 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { 0) ? "fill-red-400" : "fill-sushi-500"}`} />
    -
    0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
    +
    0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length + otherPlaces.length) - collabsAttributed}
    0) ? "fill-red-400" : "fill-sushi-500"}`} />
    @@ -345,9 +349,9 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { {((collabsAttributed - places.length) > 0) &&

    Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

    - handleOtherZoneSelection(e)} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none'> - {zones.filter( (element) => element.id != selectedZone)?.map( (element, index) => )} + {zones.filter((element) => element.id != selectedZone)?.map((element, index) => )}

    : {otherPlaces.length}

    diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/assign_zone_project/page.jsx index 85c5bee..615a681 100644 --- a/src/app/(dashboard)/assign_zone_project/page.jsx +++ b/src/app/(dashboard)/assign_zone_project/page.jsx @@ -1,6 +1,6 @@ "use client" import React, { useEffect, useState } from 'react'; -import AddIcon from "@/static/image/svg/add.svg"; +import AddIcon from "@/static/image/svg/add.svg"; import AssignProject from './AssignProject'; import { useNotification } from '@/context/NotificationContext' @@ -15,31 +15,31 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; const AffectingZoneProject = () => { const [isOpen, setIsOpen] = useState(false) - const [ listProjectsAffected, setListProjectsAffected ] = useState([]) - const [ isLoadingListProjects, setIsLoadingListProjects ] = useState(false) + const [listProjectsAffected, setListProjectsAffected] = useState([]) + const [isLoadingListProjects, setIsLoadingListProjects] = useState(false) const { toggleNotification } = useNotification() - const [ selectedWeek, setSelectedWeek ] = useState(null) - const [ selectedDay, setSelectedDay ] = useState(null) - const [ selectedAffectaionToDelete, setSelectedAffectationToDelete ] = useState(null) + const [selectedWeek, setSelectedWeek] = useState(null) + const [selectedDay, setSelectedDay] = useState(null) + const [selectedAffectaionToDelete, setSelectedAffectationToDelete] = useState(null) const [isModalOpen, setModalOpen] = useState(false); useEffect(() => { const getListOfAffectedProjects = async () => { setIsLoadingListProjects(true) - try{ - if(selectedDay && selectedWeek){ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, {method: 'GET'}) - }else if (selectedWeek){ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, {method: 'GET'}) - }else if (selectedDay){ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, {method: 'GET'}) - }else{ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, {method: 'GET'}) + try { + if (selectedDay && selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedDay) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, { method: 'GET' }) + } else { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, { method: 'GET' }) } - if(isSuccess){ + if (isSuccess) { setListProjectsAffected(data) - }else{ + } else { toggleNotification({ visible: true, message: errors[0].message, @@ -47,7 +47,7 @@ const AffectingZoneProject = () => { }) } setIsLoadingListProjects(false) - }catch(error){ + } catch (error) { setIsLoadingListProjects(false) console.log(error) toggleNotification({ @@ -66,29 +66,29 @@ const AffectingZoneProject = () => { } const handleDeleteAffectation = async () => { - try{ - var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, {method: 'DELETE'}) - if(isSuccess){ + try { + var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, { method: 'DELETE' }) + if (isSuccess) { toggleNotification({ visible: true, message: "Affectation supprimer avec succès", type: "success" }) setListProjectsAffected(listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id)) - }else if(status === 404){ + } else if (status === 404) { toggleNotification({ visible: true, message: "Affectation introuvable", type: "error" }) - }else{ + } else { toggleNotification({ visible: true, message: errors[0].message, type: "error" }) } - }catch(error){ + } catch (error) { console.log(error) toggleNotification({ visible: true, @@ -107,7 +107,7 @@ const AffectingZoneProject = () => { const handleConfirmDelete = () => { handleDeleteAffectation(); setModalOpen(false); - setSelectedAffectationToDelete(null); + setSelectedAffectationToDelete(null); }; return ( @@ -118,7 +118,7 @@ const AffectingZoneProject = () => {
    Il y a des projets qui ne sont pas complètement affecter.
    @@ -129,7 +129,7 @@ const AffectingZoneProject = () => { - +
    Semaine / JourSemaine/Jour J1 J2 J3
    {row.week}
    Semaine {row.week} - + +
    diff --git a/src/app/planning/type-presence/page.jsx b/src/app/planning/type-presence/page.jsx new file mode 100644 index 0000000..2f383cf --- /dev/null +++ b/src/app/planning/type-presence/page.jsx @@ -0,0 +1,62 @@ +"use client"; + +import {useEffect, useState} from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import EntityList from "@/app/planning/type-presence/EntityList"; +import EntityForm from "@/app/planning/type-presence/EntityForm"; +import {useNotification} from "@/context/NotificationContext"; + +const ManagePage = () => { + const [typePresences, setTypePresences] = useState([]); + const [editingEntity, setEditingEntity] = useState({ entity: null, id: null }); + const { toggleNotification } = useNotification() + + const fetchData = async () => { + const typePresencesResponse = await fetchRequest('/type-presences/'); + + if (typePresencesResponse.isSuccess) setTypePresences(typePresencesResponse.data); + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleDelete = async (endpoint, id, setState, currentState) => { + const response = await fetchRequest(`/${endpoint}/${id}/`, {method: 'DELETE'}); + if (response.isSuccess) { + setState(currentState.filter(item => item.id !== id)); + toggleNotification({ + visible: true, + message: `${currentState.find(item => item.id === id).nom} a été supprimé avec succès`, + type: "success" + }) + } + await fetchData(); + }; + + return ( + <> +

    Gérer Les Entités

    +
    +
    +

    Type Presence

    + setEditingEntity({ entity: null, id: null })} + /> + setEditingEntity({ entity: 'type-presences', id })} + /> +
    +
    + + ); +}; + +export default ManagePage; diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 0c4f1d1..425cb09 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -87,7 +87,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr }; return ( -
    +
    )} -
    +
    {userIds.map((user, index) => (
    { console.log('isAuth', isAuth) console.log('sessionData', sessionData) return ( -
    +

    TeamBook

    @@ -25,7 +25,7 @@ const Header = async () => { {isAuth ? ( <>
  • -

    {sessionData.username}

    +

    {sessionData.username}

  • {/*
  • */} {/* diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index 35e67e9..6696cca 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -1,23 +1,15 @@ const SideBar = () => { return ( -
    -
    +
    +
    -
    - - L - -
    - -
    +
    • General @@ -26,7 +18,7 @@ const SideBar = () => {
    • Administration @@ -50,7 +42,7 @@ const SideBar = () => {
    • Banned Users @@ -59,7 +51,7 @@ const SideBar = () => {
    • Calendar @@ -71,7 +63,7 @@ const SideBar = () => {
    • Billing @@ -80,7 +72,7 @@ const SideBar = () => {
    • Invoices @@ -89,7 +81,7 @@ const SideBar = () => {
    • Consultaion @@ -113,7 +105,7 @@ const SideBar = () => {
    • Details @@ -122,7 +114,7 @@ const SideBar = () => {
    • Security @@ -132,7 +124,7 @@ const SideBar = () => { @@ -145,36 +137,6 @@ const SideBar = () => {
    - -
    - - - -
    -- GitLab From 2caf89aaad7e0bb1430b293a8ffdcd3a39d824aa Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Thu, 30 May 2024 15:42:27 +0100 Subject: [PATCH 29/79] preparing feature/affecting/project-zone --- src/app/assign_zone_project/AssignProject.jsx | 112 ++++++++++++++ src/app/assign_zone_project/page.jsx | 146 ++++++++++++++++++ src/app/place/CreateNewPlace.jsx | 2 +- src/static/image/svg/desk.svg | 1 + src/static/image/svg/study-desk.svg | 1 + src/static/image/svg/user.svg | 1 + 6 files changed, 262 insertions(+), 1 deletion(-) create mode 100644 src/app/assign_zone_project/AssignProject.jsx create mode 100644 src/app/assign_zone_project/page.jsx create mode 100644 src/static/image/svg/desk.svg create mode 100644 src/static/image/svg/study-desk.svg create mode 100644 src/static/image/svg/user.svg diff --git a/src/app/assign_zone_project/AssignProject.jsx b/src/app/assign_zone_project/AssignProject.jsx new file mode 100644 index 0000000..8e9ac54 --- /dev/null +++ b/src/app/assign_zone_project/AssignProject.jsx @@ -0,0 +1,112 @@ +"use client" +import React, { useState, useEffect } from 'react' +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import UserIcon from "@/static/image/svg/user.svg" +import DeskIcon from "@/static/image/svg/study-desk.svg" + + +const AssignProject = ({ setIsOpen }) => { + + return ( +
    +
    +

    Attribuer un projet à une zone.

    + {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
    +
    +

    Veuillez sélectionner une date

    +
    +
    + +
    +
    + +
    +
    +
    +
    +
    + +
    +

    : 50

    +
    +
    +
    + +
    +

    : 50

    +
    +
    +
    +
    + + +
    +
    +
    20
    + +
    + restant +
    +
    +
    +
    +

    Veuillez sélectionner une autre zonne (optionnel)

    +
    + +
    +

    Places: 50

    +
    +
    +
    +
    + +
    +
    +
    +
    +
    + ) +} + +export default AssignProject \ No newline at end of file diff --git a/src/app/assign_zone_project/page.jsx b/src/app/assign_zone_project/page.jsx new file mode 100644 index 0000000..0283164 --- /dev/null +++ b/src/app/assign_zone_project/page.jsx @@ -0,0 +1,146 @@ +"use client" +import React, { useState } from 'react'; +import AddIcon from "@/static/image/svg/add.svg"; +import AssignProject from './AssignProject'; + + +const AffectingZoneProject = () => { + const [isOpen, setIsOpen] = useState(false) + + + const handleOpenAssignProject = () => { + setIsOpen(!isOpen) + } + return ( +
    +
    + {isOpen && } +

    List des Projets attribuer

    +
    + + +
    + +
    + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + +
    + Date + + Plateau + + Projet + + Places occupées + + Nombre des personnes + + Places disponible + + Actions +
    + Apple MacBook Pro 17" + + Silver + + Laptop + + $2999 + + $2999 + + $2999 + + Edit +
    + Apple MacBook Pro 17" + + Silver + + Laptop + + $2999 + + $2999 + + $2999 + + Edit +
    + Apple MacBook Pro 17" + + Silver + + Laptop + + $2999 + + $2999 + + $2999 + + Edit +
    +
    + + ); +} + + +export default AffectingZoneProject; \ No newline at end of file diff --git a/src/app/place/CreateNewPlace.jsx b/src/app/place/CreateNewPlace.jsx index 8036b0a..061a0cb 100644 --- a/src/app/place/CreateNewPlace.jsx +++ b/src/app/place/CreateNewPlace.jsx @@ -88,7 +88,7 @@ const CreateNewPlace = ({placesState, tables}) => { {(tables && tables?.length) && tables.map((table, index) => ( - + )) } diff --git a/src/static/image/svg/desk.svg b/src/static/image/svg/desk.svg new file mode 100644 index 0000000..9c413fe --- /dev/null +++ b/src/static/image/svg/desk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/study-desk.svg b/src/static/image/svg/study-desk.svg new file mode 100644 index 0000000..7b2fcbe --- /dev/null +++ b/src/static/image/svg/study-desk.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/user.svg b/src/static/image/svg/user.svg new file mode 100644 index 0000000..e501f45 --- /dev/null +++ b/src/static/image/svg/user.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From 9d7d2ec8a5425284d29b16767a402da9a5ab3e3c Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Fri, 31 May 2024 11:25:07 +0100 Subject: [PATCH 30/79] added scroll for users in projects --- src/app/projects/ProjectForm.jsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 425cb09..18005e6 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -87,7 +87,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr }; return ( -
    +
    Date: Fri, 31 May 2024 14:47:14 +0100 Subject: [PATCH 31/79] fixed update planning --- src/app/planning/page.jsx | 2 +- src/app/projects/page.jsx | 68 +++++++++++++++++++++++---------------- 2 files changed, 41 insertions(+), 29 deletions(-) diff --git a/src/app/planning/page.jsx b/src/app/planning/page.jsx index f8c140a..cf842ea 100644 --- a/src/app/planning/page.jsx +++ b/src/app/planning/page.jsx @@ -131,7 +131,7 @@ const PlanningPage = () => { const handleUpdate = async () => { setLoading(true); const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; - const {isSuccess, errors} = await fetchRequest(`/plannings/`, { + const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { method: 'PUT', body: JSON.stringify(requestBody) }); diff --git a/src/app/projects/page.jsx b/src/app/projects/page.jsx index 69aef4b..3a3a521 100644 --- a/src/app/projects/page.jsx +++ b/src/app/projects/page.jsx @@ -11,10 +11,10 @@ const Projects = () => { const [pageUrl, setPageUrl] = useState('/projects/'); const [projects, setProjects] = useState([]); const [editingProject, setEditingProject] = useState(null); - {/* errors from request*/ } const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); - const { toggleNotification } = useNotification() + const [isFormOpen, setIsFormOpen] = useState(false); // State for accordion + const { toggleNotification } = useNotification(); const fetchProjects = async () => { const { isSuccess, errors, data } = await fetchRequest(pageUrl); @@ -23,20 +23,15 @@ const Projects = () => { setErrors(null); } else { console.error("Failed to fetch projects"); - setErrors(errors) + setErrors(errors); } }; - useEffect(() => { + useEffect(() => { fetchProjects(); }, [pageUrl]); - // pageURL - const handleAddProject = async (project) => { - console.log("proj",project) - console.log("proj strinjify",JSON.stringify(project)) - setLoading(true); const { isSuccess, errors, data } = await fetchRequest('/projects/', { method: 'POST', @@ -48,14 +43,14 @@ const Projects = () => { visible: true, message: `${project.nom} a été ajouté avec succès`, type: "success" - }) + }); setProjects([...projects, data]); setEditingProject(null); setErrors(null); + setIsFormOpen(false); // Close the form on success } else { - console.log(errors); console.error("Failed to add project"); - setErrors(errors) + setErrors(errors); } setLoading(false); }; @@ -72,27 +67,28 @@ const Projects = () => { visible: true, message: `${updatedProject.nom} a été modifié avec succès`, type: "success" - }) + }); const updatedProjects = projects.map((project) => project.id === id ? data : project ); setProjects(updatedProjects); setEditingProject(null); setErrors(null); + setIsFormOpen(false); // Close the form on success } else { console.error("Failed to edit project"); - setErrors(errors) + setErrors(errors); } setLoading(false); }; const handleEditClick = (project) => { - console.log(project) setEditingProject(project); + setIsFormOpen(true); // Open the form for editing }; const handleDeleteProject = async (project) => { - setLoading(true) + setLoading(true); const { isSuccess, errors } = await fetchRequest(`/projects/${project.id}/`, { method: 'DELETE', }); @@ -102,18 +98,21 @@ const Projects = () => { visible: true, message: `${project.nom} a été supprimé avec succès`, type: "success" - }) - // setProjects(projects.filter((p) => p.id !== project.id)); + }); await fetchProjects(); setEditingProject(null); setErrors(null); } else { console.error("Failed to delete project"); - setErrors(errors) + setErrors(errors); } setLoading(false); }; + const toggleForm = () => { + setIsFormOpen(!isFormOpen); // Toggle the form visibility + }; + return (
    @@ -122,15 +121,28 @@ const Projects = () => {
    +

    Projets {loading ? ... : null}

    - - {/*errors from request*/} + {/* Accordion Toggle Button */} + +
    + {/* Accordion Content */} + {isFormOpen && ( + + )} + + {/* Errors from request */} {errors && errors.detail && Object.keys(errors.detail).map((key) => (
    @@ -159,6 +171,6 @@ const Projects = () => {
    ); -} -export default Projects; +}; +export default Projects; -- GitLab From 8202a747a1f63563dbf7d7a8c50c75177b9dd82c Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Mon, 3 Jun 2024 14:40:41 +0100 Subject: [PATCH 32/79] added search pagination to projects --- src/app/planning/PlanningTable.jsx | 3 +- src/app/planning/page.jsx | 13 +-- src/app/planning/type-presence/EntityList.jsx | 8 +- src/app/projects/ProjectForm.jsx | 4 +- src/app/projects/ProjectList.jsx | 7 +- src/app/projects/page.jsx | 88 +++++++++++++++---- src/app/ui/Pagination.jsx | 54 ++++++++++++ 7 files changed, 147 insertions(+), 30 deletions(-) create mode 100644 src/app/ui/Pagination.jsx diff --git a/src/app/planning/PlanningTable.jsx b/src/app/planning/PlanningTable.jsx index 6b90c54..2fbdf40 100644 --- a/src/app/planning/PlanningTable.jsx +++ b/src/app/planning/PlanningTable.jsx @@ -30,7 +30,8 @@ const PlanningTable = ({ data, typePresences, onTypePresenceChange }) => {
  • diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 18005e6..65c06c0 100644 --- a/src/app/projects/ProjectForm.jsx +++ b/src/app/projects/ProjectForm.jsx @@ -148,7 +148,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr
  • !isUserChosen(user) && handleUserSelect(user)} - className={`px-3 py-2 border-b border-chicago-200 ${isUserChosen(user) ? 'bg-gray-200 cursor-not-allowed' : 'hover:bg-sushi-200 cursor-pointer'}`} + className={`px-3 py-2 border-b border-chicago-200 ${isUserChosen(user) ? 'bg-chicago-100 cursor-not-allowed' : 'hover:bg-sushi-200 cursor-pointer'}`} > {/*add tick is chosen*/} {isUserChosen(user) && '✔ '} @@ -164,7 +164,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr type="text" value={`${user.first_name} ${user.last_name} (${user.role.name})`} readOnly - className="flex-1 px-3 py-2 bg-gray-200 border border-chicago-300 rounded-md" + className="flex-1 px-3 py-2 bg-chicago-100/50 border border-chicago-300 rounded-md" />
  • - 1 + {/* page Number note that i have count it contains total number of projects overal and a constant PAGINATION_SIZE*/} + {/* current page*/} + {Math.ceil(projects.count / PAGINATION_SIZE) === 0 ? 1 : Math.ceil(projects.count / PAGINATION_SIZE) === 1 ? 1 : projects.next ? Math.ceil(projects.count / PAGINATION_SIZE) - 1 : Math.ceil(projects.count / PAGINATION_SIZE)} + / {Math.ceil(projects.count / PAGINATION_SIZE)}
  • +
    +

    Projets + {loading ? ... : null} +

    + {/* Accordion Toggle Button */} +
    + +
    + +
    + {/* Accordion Content */} {isFormOpen && ( { projects={projects} onEdit={handleEditClick} onDelete={handleDeleteProject} - onHandlePageUrl={setPageUrl} + onHandlePageUrl={handlePageChange} /> + diff --git a/src/app/ui/Pagination.jsx b/src/app/ui/Pagination.jsx new file mode 100644 index 0000000..6b9d2f0 --- /dev/null +++ b/src/app/ui/Pagination.jsx @@ -0,0 +1,54 @@ +import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" +import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" + +const Pagination = ({ paginationData, onPageChange }) => { + if (!paginationData) return null; + + const { currentPage, pagesNumber } = paginationData; + + const handlePageClick = (pageNumber) => { + onPageChange(pageNumber); + }; + + return ( +
    +
    + {currentPage > 1 && ( +
    handlePageClick(`/projects/pagination/?page=${currentPage - 1}`)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> + +
    + )} + {Array.from({ length: pagesNumber }, (_, index) => index).map((element, index) => { + if (element + 1 === currentPage + 3) { + return ( +

    + ... +

    + ); + } else if (currentPage === element + 1) { + return ( +

    + {element + 1} +

    + ); + } else if (currentPage < element + 3 && element - currentPage < 3) { + return ( +

    handlePageClick(`/projects/pagination/?page=${element + 1}`)} key={element} className='h-8 w-9 hover:bg-neutral-200 cursor-pointer duration-150 delay-75 font-bold text-neutral-700 text-sm flex items-center justify-center'> + {element + 1} +

    + ); + } else { + return <>; + } + })} + {currentPage !== pagesNumber && ( +
    handlePageClick(`/projects/pagination/?page=${currentPage + 1}`)} className='flex h-8 w-9 items-center hover:bg-neutral-200 duration-150 delay-75 cursor-pointer justify-center'> + +
    + )} +
    +
    + ); +}; + +export default Pagination; \ No newline at end of file -- GitLab From e753c7587c69405456b40925057a0ce0c504ceec Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 3 Jun 2024 14:50:35 +0100 Subject: [PATCH 33/79] fix app layout --- .../assign_zone_project/AssignProject.jsx | 54 ++-- .../assign_zone_project/page.jsx | 0 .../etage/AddEtageComponent.jsx | 26 +- src/app/{ => (dashboard)}/etage/page.jsx | 64 ++--- src/app/(dashboard)/layout.jsx | 20 ++ .../place/CreateNewPlace.jsx | 30 +-- src/app/{ => (dashboard)}/place/RowPlace.jsx | 14 +- src/app/{ => (dashboard)}/place/page.jsx | 64 ++--- .../planning/PlanningTable.jsx | 0 src/app/{ => (dashboard)}/planning/page.jsx | 54 ++-- .../planning/type-presence/EntityForm.jsx | 0 .../planning/type-presence/EntityList.jsx | 0 .../planning/type-presence/page.jsx | 10 +- .../privilege/CreatePrivilegeForm.jsx | 6 +- .../privilege/PrivilegeTableRow.jsx | 4 +- src/app/(dashboard)/privilege/page.jsx | 83 ++++++ .../projects/ProjectForm.jsx | 0 .../projects/ProjectList.jsx | 0 src/app/{ => (dashboard)}/projects/page.jsx | 12 +- .../{ => (dashboard)}/role/CreateRoleForm.jsx | 4 +- .../{ => (dashboard)}/role/RoleTableRows.jsx | 4 +- .../{ => (dashboard)}/role/UpdateRoleForm.jsx | 6 +- src/app/(dashboard)/role/page.jsx | 71 +++++ .../table/CreateNewTable.jsx | 26 +- src/app/{ => (dashboard)}/table/RowTable.jsx | 16 +- src/app/{ => (dashboard)}/table/page.jsx | 66 ++--- .../{ => (dashboard)}/user/CreateUserForm.jsx | 12 +- .../{ => (dashboard)}/user/UpdateUserForm.jsx | 12 +- .../{ => (dashboard)}/user/UserTableRow.jsx | 4 +- src/app/(dashboard)/user/page.jsx | 117 ++++++++ .../{ => (dashboard)}/zone/CreateNewZone.jsx | 26 +- src/app/{ => (dashboard)}/zone/RowZone.jsx | 14 +- src/app/{ => (dashboard)}/zone/page.jsx | 48 ++-- src/app/globals.css | 151 +++++++++-- src/app/layout.js | 1 - src/app/planning/layout.jsx | 23 -- src/app/privilege/page.jsx | 55 ---- src/app/role/page.jsx | 75 ------ src/app/ui/Burger.jsx | 25 ++ src/app/ui/Header.jsx | 23 +- src/app/ui/LogoutButton.js | 18 +- src/app/ui/SideBar.jsx | 255 +++++++++--------- src/app/ui/SideBarLink.jsx | 22 ++ src/app/user/page.jsx | 119 -------- src/static/image/svg/logout.svg | 1 + src/static/image/svg/role.svg | 1 + 46 files changed, 902 insertions(+), 734 deletions(-) rename src/app/{ => (dashboard)}/assign_zone_project/AssignProject.jsx (71%) rename src/app/{ => (dashboard)}/assign_zone_project/page.jsx (100%) rename src/app/{ => (dashboard)}/etage/AddEtageComponent.jsx (66%) rename src/app/{ => (dashboard)}/etage/page.jsx (58%) create mode 100644 src/app/(dashboard)/layout.jsx rename src/app/{ => (dashboard)}/place/CreateNewPlace.jsx (73%) rename src/app/{ => (dashboard)}/place/RowPlace.jsx (95%) rename src/app/{ => (dashboard)}/place/page.jsx (67%) rename src/app/{ => (dashboard)}/planning/PlanningTable.jsx (100%) rename src/app/{ => (dashboard)}/planning/page.jsx (79%) rename src/app/{ => (dashboard)}/planning/type-presence/EntityForm.jsx (100%) rename src/app/{ => (dashboard)}/planning/type-presence/EntityList.jsx (100%) rename src/app/{ => (dashboard)}/planning/type-presence/page.jsx (88%) rename src/app/{ => (dashboard)}/privilege/CreatePrivilegeForm.jsx (93%) rename src/app/{ => (dashboard)}/privilege/PrivilegeTableRow.jsx (98%) create mode 100644 src/app/(dashboard)/privilege/page.jsx rename src/app/{ => (dashboard)}/projects/ProjectForm.jsx (100%) rename src/app/{ => (dashboard)}/projects/ProjectList.jsx (100%) rename src/app/{ => (dashboard)}/projects/page.jsx (94%) rename src/app/{ => (dashboard)}/role/CreateRoleForm.jsx (97%) rename src/app/{ => (dashboard)}/role/RoleTableRows.jsx (97%) rename src/app/{ => (dashboard)}/role/UpdateRoleForm.jsx (96%) create mode 100644 src/app/(dashboard)/role/page.jsx rename src/app/{ => (dashboard)}/table/CreateNewTable.jsx (75%) rename src/app/{ => (dashboard)}/table/RowTable.jsx (94%) rename src/app/{ => (dashboard)}/table/page.jsx (67%) rename src/app/{ => (dashboard)}/user/CreateUserForm.jsx (96%) rename src/app/{ => (dashboard)}/user/UpdateUserForm.jsx (96%) rename src/app/{ => (dashboard)}/user/UserTableRow.jsx (97%) create mode 100644 src/app/(dashboard)/user/page.jsx rename src/app/{ => (dashboard)}/zone/CreateNewZone.jsx (74%) rename src/app/{ => (dashboard)}/zone/RowZone.jsx (95%) rename src/app/{ => (dashboard)}/zone/page.jsx (72%) delete mode 100644 src/app/planning/layout.jsx delete mode 100644 src/app/privilege/page.jsx delete mode 100644 src/app/role/page.jsx create mode 100644 src/app/ui/Burger.jsx create mode 100644 src/app/ui/SideBarLink.jsx delete mode 100644 src/app/user/page.jsx create mode 100644 src/static/image/svg/logout.svg create mode 100644 src/static/image/svg/role.svg diff --git a/src/app/assign_zone_project/AssignProject.jsx b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx similarity index 71% rename from src/app/assign_zone_project/AssignProject.jsx rename to src/app/(dashboard)/assign_zone_project/AssignProject.jsx index 8e9ac54..4d141b3 100644 --- a/src/app/assign_zone_project/AssignProject.jsx +++ b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx @@ -1,7 +1,7 @@ "use client" import React, { useState, useEffect } from 'react' import Loader from '@/components/Loader/Loader' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" import UserIcon from "@/static/image/svg/user.svg" @@ -14,21 +14,21 @@ const AssignProject = ({ setIsOpen }) => {

    Attribuer un projet à une zone.

    - {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> + { setIsOpen(false) }} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" />

    Veuillez sélectionner une date

    - - -
    + +
    - - - - - - - -
    -

    : 50

    -
    + +
    +

    : 50

    +
    - -
    -

    : 50

    -
    + +
    +

    : 50

    +
    diff --git a/src/app/assign_zone_project/page.jsx b/src/app/(dashboard)/assign_zone_project/page.jsx similarity index 100% rename from src/app/assign_zone_project/page.jsx rename to src/app/(dashboard)/assign_zone_project/page.jsx diff --git a/src/app/etage/AddEtageComponent.jsx b/src/app/(dashboard)/etage/AddEtageComponent.jsx similarity index 66% rename from src/app/etage/AddEtageComponent.jsx rename to src/app/(dashboard)/etage/AddEtageComponent.jsx index 17e46b8..42cfdca 100644 --- a/src/app/etage/AddEtageComponent.jsx +++ b/src/app/(dashboard)/etage/AddEtageComponent.jsx @@ -1,14 +1,14 @@ 'use client' import Loader from '@/components/Loader/Loader' import React, { useState, useRef } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' -const AddEtageComponent = ({ etagesState })=> { - const [ numeroEtage, setNumeroEtage ] = useState("") +const AddEtageComponent = ({ etagesState }) => { + const [numeroEtage, setNumeroEtage] = useState("") const [isLoading, setIsLoading] = useState(false) const inputRef = useRef(null) const { toggleNotification } = useNotification() @@ -37,13 +37,13 @@ const AddEtageComponent = ({ etagesState })=> { }) } else { setIsLoading(false) - if(errors.type == "ValidationError") + if (errors.type == "ValidationError") toggleNotification({ type: "warning", message: "Le numéro détage déja existe.", visible: true, }) - else{ + else { toggleNotification({ type: "error", message: "Une erreur s'est produite lors de la création de la zone.", @@ -55,18 +55,18 @@ const AddEtageComponent = ({ etagesState })=> { } - return( + return (
    Nouveau étage: -
    - -
    - +
    + +
    +
    - + ) } - -export default(AddEtageComponent) \ No newline at end of file + +export default (AddEtageComponent) \ No newline at end of file diff --git a/src/app/etage/page.jsx b/src/app/(dashboard)/etage/page.jsx similarity index 58% rename from src/app/etage/page.jsx rename to src/app/(dashboard)/etage/page.jsx index 94f79ae..ca6ba13 100644 --- a/src/app/etage/page.jsx +++ b/src/app/(dashboard)/etage/page.jsx @@ -1,7 +1,7 @@ "use client" import React from 'react' import AddEtageComponent from './AddEtageComponent' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import { useState, useEffect } from 'react'; import Loader from '@/components/Loader/Loader' import { useNotification } from '@/context/NotificationContext' @@ -10,9 +10,9 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; -const Etage = ()=> { +const Etage = () => { const [etages, setEtages] = useState([]) - const [ isLoadingData, setIsLoadingData ] = useState(true) + const [isLoadingData, setIsLoadingData] = useState(true) const { toggleNotification } = useNotification() const [isModalOpen, setModalOpen] = useState(false); const [etageToDelete, setEtageToDelete] = useState(null); @@ -21,15 +21,15 @@ const Etage = ()=> { // Fetch data from external API useEffect(() => { const getAllEtages = async () => { - try{ - const {isSuccess, errors, data} = await fetchRequest('/zoaning/etages/', {method: 'GET'}) + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) setIsLoadingData(false) - if(isSuccess){ + if (isSuccess) { setEtages(data) - }else{ + } else { setEtages([]) } - }catch(error){ + } catch (error) { setIsLoadingData(false) console.log(error) } @@ -37,13 +37,13 @@ const Etage = ()=> { getAllEtages() }, []) const handleDeleteEtage = async (etage) => { - try{ + try { console.log(etage) - const {isSuccess, errors, data} = await fetchRequest(`/zoaning/etages/${etage.id}/`, {method: "DELETE"}) + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/etages/${etage.id}/`, { method: "DELETE" }) console.log(isSuccess) console.log(errors) console.log(data) - if(isSuccess){ + if (isSuccess) { console.log(etages) console.log("etage: ", etage) setEtages((prevEtages) => prevEtages.filter((e) => e.id !== etage.id)); @@ -52,14 +52,14 @@ const Etage = ()=> { message: "L'étage a été supprimer avec succès.", visible: true, }) - }else{ + } else { toggleNotification({ type: "error", message: "Une erreur s'est produite lors de la suppression de l'étage.", visible: true, }) } - }catch(error){ + } catch (error) { toggleNotification({ type: "error", message: "Internal Server Error", @@ -79,7 +79,7 @@ const Etage = ()=> { setProjectToDelete(null); }; - return( + return (
    @@ -87,24 +87,24 @@ const Etage = ()=> {
    {(!isLoadingData) ? etages && etages?.length ?
      - {etages?.map((etage, index) => - ( -
    • - Etage numéro: {etage.numero} -
      {handleDeleteClick(etage)}}> - -
      -
    • - ) + {etages?.map((etage, index) => + ( +
    • + Etage numéro: {etage.numero} +
      { handleDeleteClick(etage) }}> + +
      +
    • + ) )} -
    : -
    - Aucun étage n'a été ajouté -
    - : -
    - -
    + : +
    + Aucun étage n'a été ajouté +
    + : +
    + +
    }
    @@ -117,7 +117,7 @@ const Etage = ()=> { message={`Êtes-vous sûr de vouloir supprimer l'étage numéro "${etageToDelete?.numero}"?`} />
    - + ) } diff --git a/src/app/(dashboard)/layout.jsx b/src/app/(dashboard)/layout.jsx new file mode 100644 index 0000000..a4cd337 --- /dev/null +++ b/src/app/(dashboard)/layout.jsx @@ -0,0 +1,20 @@ +import React from 'react' +import SideBar from '../ui/SideBar' +import Header from '../ui/Header' + +const layout = ({ children }) => { + return ( + <> +
    +
    + +
    + {children} +
    +
    + + + ) +} + +export default layout \ No newline at end of file diff --git a/src/app/place/CreateNewPlace.jsx b/src/app/(dashboard)/place/CreateNewPlace.jsx similarity index 73% rename from src/app/place/CreateNewPlace.jsx rename to src/app/(dashboard)/place/CreateNewPlace.jsx index 061a0cb..0d9a017 100644 --- a/src/app/place/CreateNewPlace.jsx +++ b/src/app/(dashboard)/place/CreateNewPlace.jsx @@ -1,12 +1,12 @@ "use client" import React, { useState, useEffect, useRef } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { Island_Moments } from 'next/font/google' import { useNotification } from '@/context/NotificationContext' -const CreateNewPlace = ({placesState, tables}) => { +const CreateNewPlace = ({ placesState, tables }) => { const [error, setError] = useState(null) const [isLoadingAction, setIsLoadingAction] = useState(false) const [numPlace, setNumPlace] = useState(null) @@ -27,7 +27,7 @@ const CreateNewPlace = ({placesState, tables}) => { }) if (isSuccess) { setIsLoadingAction(false) - placesState((prevPlaceState) => [...prevPlaceState, {...data, id_table: tables.find(table => table.id === data.id_table)}]); + placesState((prevPlaceState) => [...prevPlaceState, { ...data, id_table: tables.find(table => table.id === data.id_table) }]); inputRef.current.value = "" selectRef.current.value = "" setNumPlace(null) @@ -47,7 +47,7 @@ const CreateNewPlace = ({placesState, tables}) => { visible: true, }) } - }else{ + } else { toggleNotification({ type: "error", message: "Une erreur s'est produite lors de la création de la table.", @@ -65,7 +65,7 @@ const CreateNewPlace = ({placesState, tables}) => { } const handleChangeEtage = (event) => { - setError("") + setError("") setSelectedTable(event.target.value) } @@ -74,7 +74,7 @@ const CreateNewPlace = ({placesState, tables}) => { - return( + return (

    Ajout d'une place

    @@ -84,19 +84,19 @@ const CreateNewPlace = ({placesState, tables}) => {
    - +

    -
    diff --git a/src/app/place/RowPlace.jsx b/src/app/(dashboard)/place/RowPlace.jsx similarity index 95% rename from src/app/place/RowPlace.jsx rename to src/app/(dashboard)/place/RowPlace.jsx index acb7a5b..6e05792 100644 --- a/src/app/place/RowPlace.jsx +++ b/src/app/(dashboard)/place/RowPlace.jsx @@ -1,6 +1,6 @@ "use client" import React, { useState, useEffect, useRef } from 'react'; -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" @@ -42,7 +42,7 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { setLoadingStatus(false) if (isSuccess) { console.log(data) - if(data.message === "NO_CHANGES"){ + if (data.message === "NO_CHANGES") { toggleNotification({ visible: true, message: "Aucun changement n'a été effectué.", @@ -52,7 +52,7 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { return } console.log(data.data) - placesState((prevPlacesState) => prevPlacesState.map( (element) => element.id === id ? {...data.data, id_table: tables.find(table => table.id === data.data.id_table)} : element )) + placesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) setIsUpdating(false) toggleNotification({ visible: true, @@ -67,7 +67,7 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { message: "Le numéro de la place déjà existe.", visible: true, }) - }else if (errors.detail.non_field_errors) { + } else if (errors.detail.non_field_errors) { toggleNotification({ type: "warning", message: "Le numéro de la place saisie existe déjà.", @@ -174,15 +174,15 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { setModalOpen(false); }; - return( + return (
  • setNumPlace(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> {handleSearchingPlace(e)}} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des places..." required /> + { handleSearchingPlace(e) }} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des places..." required /> - {isArray(places) && places?.length !== 0 && isArray(tables) && tables?.length !== 0 ? + {isArray(places) && places?.length !== 0 && isArray(tables) && tables?.length !== 0 ?
    - + @@ -92,18 +92,18 @@ const Place = ()=> { })}
    TableTable Table-Zone-Etage Action
    - : -
    -

    Pas encore des places

    -
    } + : +
    +

    Pas encore des places

    +
    } :
    - } + } - + ) } diff --git a/src/app/planning/PlanningTable.jsx b/src/app/(dashboard)/planning/PlanningTable.jsx similarity index 100% rename from src/app/planning/PlanningTable.jsx rename to src/app/(dashboard)/planning/PlanningTable.jsx diff --git a/src/app/planning/page.jsx b/src/app/(dashboard)/planning/page.jsx similarity index 79% rename from src/app/planning/page.jsx rename to src/app/(dashboard)/planning/page.jsx index f8c140a..57b80b5 100644 --- a/src/app/planning/page.jsx +++ b/src/app/(dashboard)/planning/page.jsx @@ -1,19 +1,19 @@ 'use client'; -import {useEffect, useState} from 'react'; +import { useEffect, useState } from 'react'; import Dropdown from "@/app/ui/Dropdown"; -import PlanningTable from "@/app/planning/PlanningTable"; +import PlanningTable from "@/app/(dashboard)/planning/PlanningTable"; import fetchRequest from "@/app/lib/fetchRequest"; -import {useNotification} from "@/context/NotificationContext"; +import { useNotification } from "@/context/NotificationContext"; import ConfirmationModal from "@/app/ui/ConfirmationModal"; const PlanningPage = () => { const blankData = { planning_data: [ - {week: 1, days: [{}, {}, {}, {}, {}]}, - {week: 2, days: [{}, {}, {}, {}, {}]}, - {week: 3, days: [{}, {}, {}, {}, {}]}, - {week: 4, days: [{}, {}, {}, {}, {}]}, - {week: 5, days: [{}, {}, {}, {}, {}]}, + { week: 1, days: [{}, {}, {}, {}, {}] }, + { week: 2, days: [{}, {}, {}, {}, {}] }, + { week: 3, days: [{}, {}, {}, {}, {}] }, + { week: 4, days: [{}, {}, {}, {}, {}] }, + { week: 5, days: [{}, {}, {}, {}, {}] }, ] } const [projects, setProjects] = useState([]); @@ -21,14 +21,14 @@ const PlanningPage = () => { const [planningData, setPlanningData] = useState(blankData); const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); - const {toggleNotification} = useNotification(); + const { toggleNotification } = useNotification(); const [typePresences, setTypePresences] = useState([]); const [isModalOpen, setModalOpen] = useState(false); const fetchProjects = async () => { - const {isSuccess, errors, data} = await fetchRequest('/projects/'); + const { isSuccess, errors, data } = await fetchRequest('/projects/'); if (isSuccess) { setProjects(data); setErrors(null); @@ -48,7 +48,7 @@ const PlanningPage = () => { if (!selectedProject) return; // const { isSuccess, errors, data } = await fetchRequest(`/planning/?project=${selectedProject}`); - const {isSuccess, errors, data} = await fetchRequest(`/plannings/project/${selectedProject}/`); + const { isSuccess, errors, data } = await fetchRequest(`/plannings/project/${selectedProject}/`); if (isSuccess) { // if the project have no data the response is [] in this case we should set the blankData if (data.length === 0) { @@ -74,7 +74,7 @@ const PlanningPage = () => { }, [selectedProject]); const fetchTypePresences = async () => { - const {isSuccess, errors, data} = await fetchRequest('/type-presences/'); + const { isSuccess, errors, data } = await fetchRequest('/type-presences/'); if (isSuccess) { setTypePresences(data); setErrors(null); @@ -88,7 +88,7 @@ const PlanningPage = () => { }, []); const handleTypePresenceChange = (weekIndex, dayIndex, value) => { - const updatedData = {...planningData}; + const updatedData = { ...planningData }; // get the presence type by id (value is a string convert it to int const typePresence = typePresences.find(typePresence => typePresence.id === parseInt(value)); console.log(typePresence) @@ -102,9 +102,9 @@ const PlanningPage = () => { const handleSave = async () => { setLoading(true); - const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; - console.log({id_project: selectedProject, planning_data: planningData.planning_data}) - const {isSuccess, errors} = await fetchRequest(`/plannings/`, { + const requestBody = { id_project: selectedProject, planning_data: planningData.planning_data }; + console.log({ id_project: selectedProject, planning_data: planningData.planning_data }) + const { isSuccess, errors } = await fetchRequest(`/plannings/`, { method: 'POST', body: JSON.stringify(requestBody) }); @@ -130,8 +130,8 @@ const PlanningPage = () => { const handleUpdate = async () => { setLoading(true); - const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; - const {isSuccess, errors} = await fetchRequest(`/plannings/`, { + const requestBody = { id_project: selectedProject, planning_data: planningData.planning_data }; + const { isSuccess, errors } = await fetchRequest(`/plannings/`, { method: 'PUT', body: JSON.stringify(requestBody) }); @@ -155,7 +155,7 @@ const PlanningPage = () => { const handleDelete = async () => { setLoading(true); // dlete by planning id not project id - const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { + const { isSuccess, errors } = await fetchRequest(`/plannings/${planningData.id}/`, { method: 'DELETE' }); setLoading(false); @@ -199,30 +199,30 @@ const PlanningPage = () => {
    + onTypePresenceChange={handleTypePresenceChange} />
    {/* crud buttons*/}
    {planningData.id ? <> : } diff --git a/src/app/planning/type-presence/EntityForm.jsx b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx similarity index 100% rename from src/app/planning/type-presence/EntityForm.jsx rename to src/app/(dashboard)/planning/type-presence/EntityForm.jsx diff --git a/src/app/planning/type-presence/EntityList.jsx b/src/app/(dashboard)/planning/type-presence/EntityList.jsx similarity index 100% rename from src/app/planning/type-presence/EntityList.jsx rename to src/app/(dashboard)/planning/type-presence/EntityList.jsx diff --git a/src/app/planning/type-presence/page.jsx b/src/app/(dashboard)/planning/type-presence/page.jsx similarity index 88% rename from src/app/planning/type-presence/page.jsx rename to src/app/(dashboard)/planning/type-presence/page.jsx index 2f383cf..1a6250e 100644 --- a/src/app/planning/type-presence/page.jsx +++ b/src/app/(dashboard)/planning/type-presence/page.jsx @@ -1,10 +1,10 @@ "use client"; -import {useEffect, useState} from 'react'; +import { useEffect, useState } from 'react'; import fetchRequest from "@/app/lib/fetchRequest"; -import EntityList from "@/app/planning/type-presence/EntityList"; -import EntityForm from "@/app/planning/type-presence/EntityForm"; -import {useNotification} from "@/context/NotificationContext"; +import EntityList from "@/app/(dashboard)/planning/type-presence/EntityList"; +import EntityForm from "@/app/(dashboard)/planning/type-presence/EntityForm"; +import { useNotification } from "@/context/NotificationContext"; const ManagePage = () => { const [typePresences, setTypePresences] = useState([]); @@ -22,7 +22,7 @@ const ManagePage = () => { }, []); const handleDelete = async (endpoint, id, setState, currentState) => { - const response = await fetchRequest(`/${endpoint}/${id}/`, {method: 'DELETE'}); + const response = await fetchRequest(`/${endpoint}/${id}/`, { method: 'DELETE' }); if (response.isSuccess) { setState(currentState.filter(item => item.id !== id)); toggleNotification({ diff --git a/src/app/privilege/CreatePrivilegeForm.jsx b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx similarity index 93% rename from src/app/privilege/CreatePrivilegeForm.jsx rename to src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx index d124e2e..97c87c0 100644 --- a/src/app/privilege/CreatePrivilegeForm.jsx +++ b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx @@ -1,8 +1,8 @@ 'use client' import Loader from '@/components/Loader/Loader' import React, { useRef, useState } from 'react' -import fetchRequest from '../lib/fetchRequest' -import {useNotification} from "@/context/NotificationContext"; +import fetchRequest from '../../lib/fetchRequest' +import { useNotification } from "@/context/NotificationContext"; const CreatePrivilegeForm = ({ appendPrivilege }) => { const [isLoading, setIsLoading] = useState(false) @@ -59,7 +59,7 @@ const CreatePrivilegeForm = ({ appendPrivilege }) => { } } return ( - +

    Ajout d'habilitation

    diff --git a/src/app/privilege/PrivilegeTableRow.jsx b/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx similarity index 98% rename from src/app/privilege/PrivilegeTableRow.jsx rename to src/app/(dashboard)/privilege/PrivilegeTableRow.jsx index 7537e45..d0527ec 100644 --- a/src/app/privilege/PrivilegeTableRow.jsx +++ b/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx @@ -1,13 +1,13 @@ 'use client' import React, { useEffect, useRef, useState } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" import CancelIcon from "@/static/image/svg/cancel.svg" import CheckIcon from "@/static/image/svg/check.svg" import { useNotification } from '@/context/NotificationContext' -import ConfirmationModal from '../ui/ConfirmationModal' +import ConfirmationModal from '../../ui/ConfirmationModal' const PrivilegeTableRow = ({ id, name, setPrivileges }) => { const [isUpdating, setIsUpdating] = useState(false) const [privilegeName, setPrivilegeName] = useState(name) diff --git a/src/app/(dashboard)/privilege/page.jsx b/src/app/(dashboard)/privilege/page.jsx new file mode 100644 index 0000000..86fc118 --- /dev/null +++ b/src/app/(dashboard)/privilege/page.jsx @@ -0,0 +1,83 @@ +'use client' +import React, { useEffect, useState } from 'react' +import CreatePrivilegeForm from './CreatePrivilegeForm' +import fetchRequest from '../../lib/fetchRequest' +import { isArray } from '../../lib/TypesHelper' +import PrivilegeTableRows from './PrivilegeTableRow' +import Loader from '@/components/Loader/Loader' +const Privilege = () => { + const [privileges, setPrivileges] = useState([]) + const [isLoading, setIsLoading] = useState(true) + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest("/privileges") + setIsLoading(false) + if (isSuccess) { + setPrivileges(data) + } else { + console.log(errors) + } + } + getPrivileges() + }, []) + const appendPrivilege = (privilege) => { + setPrivileges((data) => [privilege, ...data]) + } + return ( +
    + +

    List des habilitations

    + {isLoading &&
    } + {!isLoading && <> {(!isArray(privileges) || privileges?.length === 0) + ?
    +

    Pas encore des habilitations

    +
    + :
    + + + + + + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + {privileges.map((element) => { + return + })} + +
    HabilitationAction
    +
    + }} +
    + ) +} + +export default Privilege \ No newline at end of file diff --git a/src/app/projects/ProjectForm.jsx b/src/app/(dashboard)/projects/ProjectForm.jsx similarity index 100% rename from src/app/projects/ProjectForm.jsx rename to src/app/(dashboard)/projects/ProjectForm.jsx diff --git a/src/app/projects/ProjectList.jsx b/src/app/(dashboard)/projects/ProjectList.jsx similarity index 100% rename from src/app/projects/ProjectList.jsx rename to src/app/(dashboard)/projects/ProjectList.jsx diff --git a/src/app/projects/page.jsx b/src/app/(dashboard)/projects/page.jsx similarity index 94% rename from src/app/projects/page.jsx rename to src/app/(dashboard)/projects/page.jsx index 69aef4b..49579d8 100644 --- a/src/app/projects/page.jsx +++ b/src/app/(dashboard)/projects/page.jsx @@ -1,11 +1,11 @@ 'use client'; import SideBar from "@/app/ui/SideBar"; -import {useEffect, useState} from 'react'; -import ProjectForm from "@/app/projects/ProjectForm"; -import ProjectList from "@/app/projects/ProjectList"; +import { useEffect, useState } from 'react'; +import ProjectForm from "@/app/(dashboard)/projects/ProjectForm"; +import ProjectList from "@/app/(dashboard)/projects/ProjectList"; import fetchRequest from "@/app/lib/fetchRequest"; -import {useNotification} from "@/context/NotificationContext"; +import { useNotification } from "@/context/NotificationContext"; const Projects = () => { const [pageUrl, setPageUrl] = useState('/projects/'); @@ -34,8 +34,8 @@ const Projects = () => { // pageURL const handleAddProject = async (project) => { - console.log("proj",project) - console.log("proj strinjify",JSON.stringify(project)) + console.log("proj", project) + console.log("proj strinjify", JSON.stringify(project)) setLoading(true); const { isSuccess, errors, data } = await fetchRequest('/projects/', { diff --git a/src/app/role/CreateRoleForm.jsx b/src/app/(dashboard)/role/CreateRoleForm.jsx similarity index 97% rename from src/app/role/CreateRoleForm.jsx rename to src/app/(dashboard)/role/CreateRoleForm.jsx index d7c479d..223840e 100644 --- a/src/app/role/CreateRoleForm.jsx +++ b/src/app/(dashboard)/role/CreateRoleForm.jsx @@ -1,6 +1,6 @@ import Loader from '@/components/Loader/Loader' import React, { useState, useRef, useEffect, useMemo } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '@/app/lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" const CreateRoleForm = ({ appendRole, setIsOpen }) => { @@ -108,7 +108,7 @@ const CreateRoleForm = ({ appendRole, setIsOpen }) => { var isAllSelected = useMemo(() => privileges ? privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id)) : false, [selectedPrivileges, privileges]) return (
    -
    +
    setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> {(privileges) ?

    Ajout de Rôle

    diff --git a/src/app/role/RoleTableRows.jsx b/src/app/(dashboard)/role/RoleTableRows.jsx similarity index 97% rename from src/app/role/RoleTableRows.jsx rename to src/app/(dashboard)/role/RoleTableRows.jsx index c937dc0..89fcaa4 100644 --- a/src/app/role/RoleTableRows.jsx +++ b/src/app/(dashboard)/role/RoleTableRows.jsx @@ -1,9 +1,9 @@ import React, { useState } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '@/app/lib/fetchRequest' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" import { useNotification } from '@/context/NotificationContext' -import ConfirmationModal from '../ui/ConfirmationModal' +import ConfirmationModal from '@/app/ui/ConfirmationModal' const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { const { toggleNotification } = useNotification() diff --git a/src/app/role/UpdateRoleForm.jsx b/src/app/(dashboard)/role/UpdateRoleForm.jsx similarity index 96% rename from src/app/role/UpdateRoleForm.jsx rename to src/app/(dashboard)/role/UpdateRoleForm.jsx index 348ebfc..685b2ed 100644 --- a/src/app/role/UpdateRoleForm.jsx +++ b/src/app/(dashboard)/role/UpdateRoleForm.jsx @@ -1,9 +1,9 @@ import { useNotification } from '@/context/NotificationContext' import React, { useState, useRef, useEffect, useMemo } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' import CancelIcon from "@/static/image/svg/cancel.svg" -import { isArray } from '../lib/TypesHelper' +import { isArray } from '../../lib/TypesHelper' const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePrivileges, name, id }) => { const { toggleNotification } = useNotification() const [loadingStatus, setLoadingStatus] = useState(false) @@ -107,7 +107,7 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv var isAllSelected = useMemo(() => privileges ? privileges.every((element) => selectedPrivileges.find((priv) => priv.id === element.id)) : false, [selectedPrivileges, privileges]) return (
    -
    +
    setRoleToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> {(privileges) ?

    Modification de Rôle

    diff --git a/src/app/(dashboard)/role/page.jsx b/src/app/(dashboard)/role/page.jsx new file mode 100644 index 0000000..7e682d2 --- /dev/null +++ b/src/app/(dashboard)/role/page.jsx @@ -0,0 +1,71 @@ +'use client' +import React, { useState, useEffect } from 'react' +import CreateRoleForm from './CreateRoleForm' +import Loader from '@/components/Loader/Loader' +import RoleTableRows from './RoleTableRows' +import fetchRequest from '@/app/lib/fetchRequest' +import { isArray } from '../../lib/TypesHelper' +import AddIcon from "@/static/image/svg/add.svg" +import UpdateRoleForm from './UpdateRoleForm' +import { useNotification } from '@/context/NotificationContext' +const Role = () => { + const [roles, setRoles] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) + const [roleToUpdate, setRoleToUpdate] = useState(null) + const { toggleNotification } = useNotification() + useEffect(() => { + const getRoles = async () => { + const { data, errors, isSuccess } = await fetchRequest("/roles") + setIsLoading(false) + if (isSuccess) { + console.log(data) + setRoles(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getRoles() + }, []) + const appendRole = (newRole) => { + setRoles([newRole, ...roles]) + } + return ( +
    + {openCreatePopup && } + {roleToUpdate && } +
    +

    List des Roles

    + +
    + {isLoading &&
    } + {!isLoading && <> {(!isArray(roles) || roles?.length === 0) + ?
    +

    Pas encore des roles

    +
    + :
    + + + + + + + {roles?.map((element) => { + return + })} +
    RôleHabilitationsAction
    +
    + }} +
    + ) +} + +export default Role \ No newline at end of file diff --git a/src/app/table/CreateNewTable.jsx b/src/app/(dashboard)/table/CreateNewTable.jsx similarity index 75% rename from src/app/table/CreateNewTable.jsx rename to src/app/(dashboard)/table/CreateNewTable.jsx index fc2f3cf..50c0f90 100644 --- a/src/app/table/CreateNewTable.jsx +++ b/src/app/(dashboard)/table/CreateNewTable.jsx @@ -1,6 +1,6 @@ "use client" import React, { useState, useEffect, useRef } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { Island_Moments } from 'next/font/google' import { useNotification } from '@/context/NotificationContext' @@ -27,7 +27,7 @@ const CreateNewTable = ({ tablesState, zones }) => { }) if (isSuccess) { setIsLoadingAction(false) - tablesState((prevTableState) => [...prevTableState, {...data, id_zone: zones.find(zone => zone.id === data.id_zone)}]); + tablesState((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); inputRef.current.value = "" selectRef.current.value = "" setNumeroTable(null) @@ -47,7 +47,7 @@ const CreateNewTable = ({ tablesState, zones }) => { visible: true, }) } - }else{ + } else { toggleNotification({ type: "error", message: "Une erreur s'est produite lors de la création de la table.", @@ -72,7 +72,7 @@ const CreateNewTable = ({ tablesState, zones }) => { - return( + return (

    Ajout d'une table

    @@ -82,19 +82,19 @@ const CreateNewTable = ({ tablesState, zones }) => {
    - +

    -
    diff --git a/src/app/table/RowTable.jsx b/src/app/(dashboard)/table/RowTable.jsx similarity index 94% rename from src/app/table/RowTable.jsx rename to src/app/(dashboard)/table/RowTable.jsx index 939d879..7e16dd2 100644 --- a/src/app/table/RowTable.jsx +++ b/src/app/(dashboard)/table/RowTable.jsx @@ -1,6 +1,6 @@ "use client" import React, { useState, useEffect, useRef } from 'react'; -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" @@ -42,16 +42,16 @@ const RowZone = ({ id, numero, zone, tablesState, zones }) => { }) setLoadingStatus(false) if (isSuccess) { - if(data.message === "NO_CHANGES"){ + if (data.message === "NO_CHANGES") { toggleNotification({ visible: true, message: "Aucun changement n'a été effectué.", type: "warning" }) setIsUpdating(false) - return + return } - tablesState((prevTableState) => prevTableState.map( (element) => element.id === id ? {...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone)} : element )) + tablesState((prevTableState) => prevTableState.map((element) => element.id === id ? { ...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone) } : element)) setIsUpdating(false) toggleNotification({ visible: true, @@ -66,7 +66,7 @@ const RowZone = ({ id, numero, zone, tablesState, zones }) => { message: "Le numero de la table existe déjà", visible: true, }) - }else if (errors.detail.non_field_errors) { + } else if (errors.detail.non_field_errors) { toggleNotification({ type: "warning", message: "Le numero de la table saisie déjà existe.", @@ -173,15 +173,15 @@ const RowZone = ({ id, numero, zone, tablesState, zones }) => { setModalOpen(false); }; - return( + return (
    setTableNum(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> {handleSearchingTable(e)}} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des tables..." required /> + { handleSearchingTable(e) }} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des tables..." required /> - - {isArray(tables) && tables?.length !== 0 && isArray(zones) && zones?.length !== 0 ? + + {isArray(tables) && tables?.length !== 0 && isArray(zones) && zones?.length !== 0 ?
    - + @@ -91,18 +91,18 @@ const Table = ()=> { })}
    TableTable Zone-Etage Action
    - : -
    -

    Pas encore des tables

    -
    } + : +
    +

    Pas encore des tables

    +
    } :
    - } + } - + ) } diff --git a/src/app/user/CreateUserForm.jsx b/src/app/(dashboard)/user/CreateUserForm.jsx similarity index 96% rename from src/app/user/CreateUserForm.jsx rename to src/app/(dashboard)/user/CreateUserForm.jsx index 1b4540d..90b1799 100644 --- a/src/app/user/CreateUserForm.jsx +++ b/src/app/(dashboard)/user/CreateUserForm.jsx @@ -1,9 +1,9 @@ import React, { useState, useEffect } from 'react' import Loader from '@/components/Loader/Loader' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" -import { EMAIL_REGEX } from '../lib/constants' +import { EMAIL_REGEX } from '../../lib/constants' @@ -154,8 +154,8 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { } return (
    -
    - setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
    + setIsOpen(false)} className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600" /> {(roles && projects) ?

    Ajout d'utilisateur

    @@ -183,7 +183,7 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
    -
    +
    {roles.length !== 0 ?
    @@ -171,7 +171,7 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => {
    }
    -
    +
    diff --git a/src/app/user/UserTableRow.jsx b/src/app/(dashboard)/user/UserTableRow.jsx similarity index 97% rename from src/app/user/UserTableRow.jsx rename to src/app/(dashboard)/user/UserTableRow.jsx index e057532..da43045 100644 --- a/src/app/user/UserTableRow.jsx +++ b/src/app/(dashboard)/user/UserTableRow.jsx @@ -1,9 +1,9 @@ import React, { useState } from 'react' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" -import ConfirmationModal from '../ui/ConfirmationModal' +import ConfirmationModal from '../../ui/ConfirmationModal' import { useNotification } from '@/context/NotificationContext' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projects, setUserToUpdate }) => { const { toggleNotification } = useNotification() diff --git a/src/app/(dashboard)/user/page.jsx b/src/app/(dashboard)/user/page.jsx new file mode 100644 index 0000000..faa6c8e --- /dev/null +++ b/src/app/(dashboard)/user/page.jsx @@ -0,0 +1,117 @@ +'use client'; +import React, { useEffect, useState } from 'react' +import CreateUserForm from './CreateUserForm' +import UpdateUserForm from './UpdateUserForm' +import AddIcon from "@/static/image/svg/add.svg" +import Loader from '@/components/Loader/Loader' +import fetchRequest from '../../lib/fetchRequest'; +import { isArray } from '../../lib/TypesHelper'; +import { useNotification } from '@/context/NotificationContext'; +import UserTableRow from './UserTableRow'; +import { PAGINATION_SIZE } from '../../lib/constants'; +import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" +import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" +const UserPage = () => { + const [users, setUsers] = useState([]) + const [paginationData, setPaginationData] = useState(null) + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) + const [userToUpdate, setUserToUpdate] = useState(null) + const { toggleNotification } = useNotification() + const [query, setQuery] = useState(''); + const getUsers = async (pageNumber = 1, signal) => { + setIsLoading(true) + if (search) var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}&search=${query}`, { signal: signal }) + else var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}`) + setIsLoading(false) + if (isSuccess) { + console.log(data) + setUsers(data.results) + setPaginationData({ pagesNumber: Math.ceil((data.count || 0) / PAGINATION_SIZE), currentPage: pageNumber }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + useEffect(() => { + const controller = new AbortController() + const signal = controller.signal + getUsers(1, signal) + return () => { + controller.abort("fetching another users") + } + }, [query]) + const appendUser = (newUser) => { + setUsers([newUser, ...users]) + } + + const handleSearchChange = (event) => { + setQuery(event.target.value) + } + return ( +
    + {openCreatePopup && } + {userToUpdate && } +
    + +
    +
    +

    List des Utilisateurs

    + +
    + {(isLoading) &&
    } + {(!isLoading) && <> {(!isArray(users) || users?.length === 0) + ?
    +

    Pas encore des utilisateurs

    +
    + : +
    + + + + + + + + + {users?.map((element) => { + return + })} + +
    NomEmailRôleProjectsAction
    +
    } + + } +
    + {(paginationData) &&
    + {(paginationData.currentPage > 1) &&
    getUsers(paginationData.currentPage - 1)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> + +
    } + {paginationData && Array.from({ length: paginationData.pagesNumber }, (_, index) => index).map((element, index) => { + if (element + 1 === paginationData.currentPage + 3) return

    + ... +

    + else if (paginationData.currentPage === element + 1) return

    + {element + 1} +

    + else if (paginationData.currentPage < element + 3 && element - paginationData.currentPage < 3) return

    getUsers(element + 1)} key={element} className='h-8 w-9 hover:bg-neutral-200 cursor-pointer duration-150 delay-75 font-bold text-neutral-700 text-sm flex items-center justify-center' > + {element + 1} +

    + else return <> + })} + {(paginationData.currentPage !== paginationData.pagesNumber) &&
    getUsers(paginationData.currentPage + 1)} className='flex h-8 w-9 items-center hover:bg-neutral-200 duration-150 delay-75 cursor-pointer justify-center'> + +
    } +
    } +
    +
    + ) +} + +export default UserPage \ No newline at end of file diff --git a/src/app/zone/CreateNewZone.jsx b/src/app/(dashboard)/zone/CreateNewZone.jsx similarity index 74% rename from src/app/zone/CreateNewZone.jsx rename to src/app/(dashboard)/zone/CreateNewZone.jsx index 76b78c6..ccd197d 100644 --- a/src/app/zone/CreateNewZone.jsx +++ b/src/app/(dashboard)/zone/CreateNewZone.jsx @@ -1,6 +1,6 @@ "use client" import React, { useState, useEffect, useRef } from 'react' -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { Island_Moments } from 'next/font/google' import { useNotification } from '@/context/NotificationContext' @@ -27,7 +27,7 @@ const CreateNewZone = ({ zoneState, etages }) => { }) if (isSuccess) { setIsLoadingAction(false) - zoneState((prevZoneValue) => [...prevZoneValue, {...data, id_etage: etages.find(etage => etage.id === data.id_etage)}]); + zoneState((prevZoneValue) => [...prevZoneValue, { ...data, id_etage: etages.find(etage => etage.id === data.id_etage) }]); inputRef.current.value = "" selectRef.current.value = "" setNomZone(null) @@ -47,7 +47,7 @@ const CreateNewZone = ({ zoneState, etages }) => { visible: true, }) } - }else{ + } else { toggleNotification({ type: "error", message: "Une erreur s'est produite lors de la création de la zone.", @@ -74,7 +74,7 @@ const CreateNewZone = ({ zoneState, etages }) => { - return( + return (

    Ajout d'une zone

    @@ -84,19 +84,19 @@ const CreateNewZone = ({ zoneState, etages }) => {
    - +

    -
    diff --git a/src/app/zone/RowZone.jsx b/src/app/(dashboard)/zone/RowZone.jsx similarity index 95% rename from src/app/zone/RowZone.jsx rename to src/app/(dashboard)/zone/RowZone.jsx index baebd3a..bb1be7d 100644 --- a/src/app/zone/RowZone.jsx +++ b/src/app/(dashboard)/zone/RowZone.jsx @@ -1,6 +1,6 @@ "use client" import React, { useState, useEffect, useRef } from 'react'; -import fetchRequest from '../lib/fetchRequest' +import fetchRequest from '../../lib/fetchRequest' import Loader from '@/components/Loader/Loader' import DeleteIcon from "@/static/image/svg/delete.svg" import EditIcon from "@/static/image/svg/edit.svg" @@ -42,7 +42,7 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { setLoadingStatus(false) if (isSuccess) { console.log(data) - if(data.message === "NO_CHANGES"){ + if (data.message === "NO_CHANGES") { toggleNotification({ visible: true, message: "Aucun changement n'a été effectué.", @@ -52,7 +52,7 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { return } console.log(data.data) - zonesState((prevZonesValue) => prevZonesValue.map( (element) => element.id === id ? {...data.data, id_etage: etages.find(etage => etage.id === data.data.id_etage)} : element )) + zonesState((prevZonesValue) => prevZonesValue.map((element) => element.id === id ? { ...data.data, id_etage: etages.find(etage => etage.id === data.data.id_etage) } : element)) setIsUpdating(false) toggleNotification({ visible: true, @@ -67,7 +67,7 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { message: "Le nom de la zone existe déjà", visible: true, }) - }else if (errors.detail.non_field_errors) { + } else if (errors.detail.non_field_errors) { toggleNotification({ type: "warning", message: "Le nom de la zone saisie déjà existe.", @@ -174,15 +174,15 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { setModalOpen(false); }; - return( + return (
    setZoneName(event.target.value)} defaultValue={nom} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - + @@ -71,18 +71,18 @@ const Zone = ()=> { })}
    ZoneZone Etage Action
    - : -
    -

    Pas encore des zones

    -
    } + : +
    +

    Pas encore des zones

    +
    } :
    - } + } - + ) } diff --git a/src/app/globals.css b/src/app/globals.css index 83bb86b..20a7710 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -39,22 +39,27 @@ -o-transition: max-height .35s; transition: max-height .35s; } + /* :checked - resize to full height */ -.tab input:checked ~ .tab-content { +.tab input:checked~.tab-content { max-height: 100%; } + /* Label formatting when open */ -.tab input:checked + label{ +.tab input:checked+label { /*@apply text-xl p-5 border-l-2 border-indigo-500 bg-gray-100 text-indigo*/ - font-size: 1.25rem; /*.text-xl*/ - padding: 1.25rem; /*.p-5*/ + font-size: 1.25rem; + /*.text-xl*/ + padding: 1.25rem; + /*.p-5*/ /*border-left-width: 2px; !*.border-l-2*!*/ /*border-color: #93a84c; !*.border-indigo*!*/ /*color: #93a84c; !*.text-indigo*!*/ } + /* Icon */ .tab label::after { - float:right; + float: right; right: 0; top: 0; display: block; @@ -67,29 +72,133 @@ -o-transition: all .35s; transition: all .35s; } + /* Icon formatting - closed */ -.tab input[type=checkbox] + label::after { +.tab input[type=checkbox]+label::after { content: "+"; - font-weight:bold; /*.font-bold*/ - border-width: 1px; /*.border*/ - border-radius: 9999px; /*.rounded-full */ - border-color: #5c5c5c; /*.border-grey*/ + font-weight: bold; + /*.font-bold*/ + border-width: 1px; + /*.border*/ + border-radius: 9999px; + /*.rounded-full */ + border-color: #5c5c5c; + /*.border-grey*/ } -.tab input[type=radio] + label::after { + +.tab input[type=radio]+label::after { content: "\25BE"; - font-weight:bold; /*.font-bold*/ - border-width: 1px; /*.border*/ - border-radius: 9999px; /*.rounded-full */ - border-color: #5c5c5c; /*.border-grey*/ + font-weight: bold; + /*.font-bold*/ + border-width: 1px; + /*.border*/ + border-radius: 9999px; + /*.rounded-full */ + border-color: #5c5c5c; + /*.border-grey*/ } + /* Icon formatting - open */ -.tab input[type=checkbox]:checked + label::after { +.tab input[type=checkbox]:checked+label::after { transform: rotate(315deg); - background-color: #93a84c; /*.bg-indigo*/ - color: #f8fafc; /*.text-grey-lightest*/ + background-color: #93a84c; + /*.bg-indigo*/ + color: #f8fafc; + /*.text-grey-lightest*/ } -.tab input[type=radio]:checked + label::after { + +.tab input[type=radio]:checked+label::after { transform: rotateX(180deg); - background-color: #93a84c; /*.bg-indigo*/ - color: #f8fafc; /*.text-grey-lightest*/ + background-color: #93a84c; + /*.bg-indigo*/ + color: #f8fafc; + /*.text-grey-lightest*/ +} + +.burger.notActive { + height: 4px; + width: 17px; + border-radius: 5px; + background-color: white; + position: relative; + transition: all 0.2s; +} + +.burger.active { + height: 4px; + width: 17px; + border-radius: 5px; + position: relative; + transition: all 0.2s; + opacity: 1; +} + +.burger.active::before { + content: ""; + position: absolute; + top: 8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(-45deg) translate(6px, -6px); + transition: transform 0.2s; +} + +.burger.active::after { + content: ""; + position: absolute; + top: -8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(45deg) translate(5px, 6px); + transition: transform 0.2s; +} + +.burger.notActive::before { + content: ""; + position: absolute; + top: 8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(0) translate(0, 0); + transition: transform 0.2s; +} + +.burger.notActive::after { + content: ""; + position: absolute; + top: -8px; + right: 0; + height: 4px; + width: 25px; + border-radius: 5px; + background-color: white; + transform: rotate(0) translate(0, 0); + transition: transform 0.2s; +} + +#sideBar { + white-space: no-wrap; + height: calc(100dvh - 3.5rem); +} + +@media screen and (max-width:767px) { + #sideBar.active { + width: 100%; + transition-duration: 300ms; + } + + + #sideBar.notActive { + width: 0; + transition-duration: 300ms + } } \ No newline at end of file diff --git a/src/app/layout.js b/src/app/layout.js index 45c2161..52fe06b 100644 --- a/src/app/layout.js +++ b/src/app/layout.js @@ -14,7 +14,6 @@ export default function RootLayout({ children }) { return ( -
    {children} diff --git a/src/app/planning/layout.jsx b/src/app/planning/layout.jsx deleted file mode 100644 index 6386ecb..0000000 --- a/src/app/planning/layout.jsx +++ /dev/null @@ -1,23 +0,0 @@ -"use client"; -import SideBar from "@/app/ui/SideBar"; -import ProjectForm from "@/app/projects/ProjectForm"; -import ProjectList from "@/app/projects/ProjectList"; - -// layout for the planning page -export default function PlanningLayout ({ children }) { - return ( -
    -
    - -
    - -
    -
    - {children} -
    -
    -
    - ); - -} - diff --git a/src/app/privilege/page.jsx b/src/app/privilege/page.jsx deleted file mode 100644 index 46c9515..0000000 --- a/src/app/privilege/page.jsx +++ /dev/null @@ -1,55 +0,0 @@ -'use client' -import React, { useEffect, useState } from 'react' -import CreatePrivilegeForm from './CreatePrivilegeForm' -import fetchRequest from '../lib/fetchRequest' -import { isArray } from '../lib/TypesHelper' -import PrivilegeTableRows from './PrivilegeTableRow' -import Loader from '@/components/Loader/Loader' -const Privilege = () => { - const [privileges, setPrivileges] = useState([]) - const [isLoading, setIsLoading] = useState(true) - useEffect(() => { - const getPrivileges = async () => { - const { data, errors, isSuccess } = await fetchRequest("/privileges") - setIsLoading(false) - if (isSuccess) { - setPrivileges(data) - } else { - console.log(errors) - } - } - getPrivileges() - }, []) - const appendPrivilege = (privilege) => { - setPrivileges((data) => [privilege, ...data]) - } - return ( -
    -
    -
    - -

    List des habilitations

    - {isLoading &&
    } - {!isLoading && <> {(!isArray(privileges) || privileges?.length === 0) - ?
    -

    Pas encore des habilitations

    -
    - :
    - - - - - - {privileges.map((element) => { - return - })} -
    HabilitationAction
    -
    - }} -
    -
    -
    - ) -} - -export default Privilege \ No newline at end of file diff --git a/src/app/role/page.jsx b/src/app/role/page.jsx deleted file mode 100644 index 411c32f..0000000 --- a/src/app/role/page.jsx +++ /dev/null @@ -1,75 +0,0 @@ -'use client' -import React, { useState, useEffect } from 'react' -import CreateRoleForm from './CreateRoleForm' -import Loader from '@/components/Loader/Loader' -import RoleTableRows from './RoleTableRows' -import fetchRequest from '../lib/fetchRequest' -import { isArray } from '../lib/TypesHelper' -import AddIcon from "@/static/image/svg/add.svg" -import UpdateRoleForm from './UpdateRoleForm' -import { useNotification } from '@/context/NotificationContext' -const Role = () => { - const [roles, setRoles] = useState([]) - const [isLoading, setIsLoading] = useState(true) - const [openCreatePopup, setOpenCreatePopup] = useState(null) - const [roleToUpdate, setRoleToUpdate] = useState(null) - const { toggleNotification } = useNotification() - useEffect(() => { - const getRoles = async () => { - const { data, errors, isSuccess } = await fetchRequest("/roles") - setIsLoading(false) - if (isSuccess) { - console.log(data) - setRoles(data) - } else { - console.log(errors) - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - } - } - getRoles() - }, []) - const appendRole = (newRole) => { - setRoles([newRole, ...roles]) - } - return ( -
    -
    -
    - {openCreatePopup && } - {roleToUpdate && } -
    -

    List des Roles

    - -
    - {isLoading &&
    } - {!isLoading && <> {(!isArray(roles) || roles?.length === 0) - ?
    -

    Pas encore des roles

    -
    - :
    - - - - - - - {roles?.map((element) => { - return - })} -
    RôleHabilitationsAction
    -
    - }} -
    -
    -
    - ) -} - -export default Role \ No newline at end of file diff --git a/src/app/ui/Burger.jsx b/src/app/ui/Burger.jsx new file mode 100644 index 0000000..68fafff --- /dev/null +++ b/src/app/ui/Burger.jsx @@ -0,0 +1,25 @@ +'use client' +import React from 'react' + +const Burger = () => { + const dislaySideBar = (event) => { + const target = event.currentTarget.firstChild + const sideBar = document.getElementById("sideBar") + if (target && sideBar) + if (target.classList.contains("active")) { + target.classList.replace("active", "notActive") + sideBar.classList.replace("active", "notActive") + } + else { + target.classList.replace("notActive", "active") + sideBar.classList.replace("notActive", "active") + } + } + return ( +
    +
    +
    + ) +} + +export default Burger \ No newline at end of file diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx index bb048c4..4112e77 100644 --- a/src/app/ui/Header.jsx +++ b/src/app/ui/Header.jsx @@ -1,40 +1,35 @@ // 'use client'; -// import { useState, useEffect } from 'react'; import Link from 'next/link'; -// import isAuthenticated from "@/app/lib/isAuthenticated"; import isAuthenticatedSSR from "@/app/lib/isAuthenticatedSSR"; import LogoutButton from "@/app/ui/LogoutButton"; -// import fetchRequest from "@/app/lib/fetchRequest"; -// import Cookies from "js-cookie"; +import Burger from './Burger'; const Header = async () => { - const {isAuth, sessionData} = await isAuthenticatedSSR() + const { isAuth, sessionData } = await isAuthenticatedSSR() console.log('isAuth', isAuth) console.log('sessionData', sessionData) return ( -
    -
    +
    +

    TeamBook

    - ); } export default SideBar; \ No newline at end of file diff --git a/src/app/ui/SideBarLink.jsx b/src/app/ui/SideBarLink.jsx new file mode 100644 index 0000000..33595a7 --- /dev/null +++ b/src/app/ui/SideBarLink.jsx @@ -0,0 +1,22 @@ +'use client' +import Link from 'next/link' +import { usePathname } from 'next/navigation' +import React from 'react' +const SideBarLink = ({ link, label }) => { + const pathname = usePathname() + if (pathname && pathname.includes(link)) + return ( +
    + + {label} + +
    + ) + return
    + + {label} + +
    +} + +export default SideBarLink \ No newline at end of file diff --git a/src/app/user/page.jsx b/src/app/user/page.jsx deleted file mode 100644 index de82ec7..0000000 --- a/src/app/user/page.jsx +++ /dev/null @@ -1,119 +0,0 @@ -'use client'; -import React, { useEffect, useState, useDeferredValue } from 'react' -import CreateUserForm from './CreateUserForm' -import UpdateUserForm from './UpdateUserForm' -import AddIcon from "@/static/image/svg/add.svg" -import Loader from '@/components/Loader/Loader' -import fetchRequest from '../lib/fetchRequest'; -import { isArray } from '../lib/TypesHelper'; -import { useNotification } from '@/context/NotificationContext'; -import UserTableRow from './UserTableRow'; -import { PAGINATION_SIZE } from '../lib/constants'; -import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" -import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" -const UserPage = () => { - const [users, setUsers] = useState([]) - const [paginationData, setPaginationData] = useState(null) - const [isLoading, setIsLoading] = useState(true) - const [openCreatePopup, setOpenCreatePopup] = useState(null) - const [userToUpdate, setUserToUpdate] = useState(null) - const { toggleNotification } = useNotification() - const [query, setQuery] = useState(''); - const getUsers = async (pageNumber = 1, signal) => { - setIsLoading(true) - if (search) var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}&search=${query}`, { signal: signal }) - else var { data, errors, isSuccess } = await fetchRequest(`/users/?page=${pageNumber}`) - setIsLoading(false) - if (isSuccess) { - console.log(data) - setUsers(data.results) - setPaginationData({ pagesNumber: Math.ceil((data.count || 0) / PAGINATION_SIZE), currentPage: pageNumber }) - } else { - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - } - } - useEffect(() => { - const controller = new AbortController() - const signal = controller.signal - getUsers(1, signal) - return () => { - controller.abort("fetching another users") - } - }, [query]) - const appendUser = (newUser) => { - setUsers([newUser, ...users]) - } - - const handleSearchChange = (event) => { - setQuery(event.target.value) - } - return (
    -
    -
    - {openCreatePopup && } - {userToUpdate && } -
    -

    List des Utilisateurs

    - -
    -
    - -
    - {(isLoading) &&
    } - {(!isLoading) && <> {(!isArray(users) || users?.length === 0) - ?
    -

    Pas encore des utilisateurs

    -
    - : -
    - - - - - - - - - {users?.map((element) => { - return - })} -
    NomEmailRôleProjectsAction
    -
    } - - } -
    - {(paginationData) &&
    - {(paginationData.currentPage > 1) &&
    getUsers(paginationData.currentPage - 1)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> - -
    } - {paginationData && Array.from({ length: paginationData.pagesNumber }, (_, index) => index).map((element, index) => { - if (element + 1 === paginationData.currentPage + 3) return

    - ... -

    - else if (paginationData.currentPage === element + 1) return

    - {element + 1} -

    - else if (paginationData.currentPage < element + 3 && element - paginationData.currentPage < 3) return

    getUsers(element + 1)} key={element} className='h-8 w-9 hover:bg-neutral-200 cursor-pointer duration-150 delay-75 font-bold text-neutral-700 text-sm flex items-center justify-center' > - {element + 1} -

    - else return <> - })} - {(paginationData.currentPage !== paginationData.pagesNumber) &&
    getUsers(paginationData.currentPage + 1)} className='flex h-8 w-9 items-center hover:bg-neutral-200 duration-150 delay-75 cursor-pointer justify-center'> - -
    } -
    } -
    -
    -
    -
    - ) -} - -export default UserPage \ No newline at end of file diff --git a/src/static/image/svg/logout.svg b/src/static/image/svg/logout.svg new file mode 100644 index 0000000..932b461 --- /dev/null +++ b/src/static/image/svg/logout.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/role.svg b/src/static/image/svg/role.svg new file mode 100644 index 0000000..d8f1394 --- /dev/null +++ b/src/static/image/svg/role.svg @@ -0,0 +1 @@ + \ No newline at end of file -- GitLab From 58638d5e6163aba0e339ba68e0e7c053678d5cac Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Mon, 3 Jun 2024 14:54:55 +0100 Subject: [PATCH 34/79] added search pagination to projects --- src/static/image/svg/simple-table.svg | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 src/static/image/svg/simple-table.svg diff --git a/src/static/image/svg/simple-table.svg b/src/static/image/svg/simple-table.svg new file mode 100644 index 0000000..178f298 --- /dev/null +++ b/src/static/image/svg/simple-table.svg @@ -0,0 +1,11 @@ + + + + + + + + + \ No newline at end of file -- GitLab From 33fbb8308f910a60614ec00a4353834b449b4a54 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Tue, 4 Jun 2024 20:29:41 +0100 Subject: [PATCH 35/79] affecting project to zone progress v1 --- src/app/assign_zone_project/AssignProject.jsx | 366 +++++++++++++++--- src/app/assign_zone_project/page.jsx | 246 ++++++------ 2 files changed, 435 insertions(+), 177 deletions(-) diff --git a/src/app/assign_zone_project/AssignProject.jsx b/src/app/assign_zone_project/AssignProject.jsx index 8e9ac54..19eae4e 100644 --- a/src/app/assign_zone_project/AssignProject.jsx +++ b/src/app/assign_zone_project/AssignProject.jsx @@ -1,109 +1,351 @@ "use client" -import React, { useState, useEffect } from 'react' +import React, { useState, useEffect, useRef } from 'react' import Loader from '@/components/Loader/Loader' import fetchRequest from '../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" import UserIcon from "@/static/image/svg/user.svg" import DeskIcon from "@/static/image/svg/study-desk.svg" +import AddIcon from "@/static/image/svg/add.svg" -const AssignProject = ({ setIsOpen }) => { +const AssignProject = ({ setIsOpen, listProjects }) => { + const [loading, setLoading] = useState(false) + const [projects, setProjects] = useState([]) + const [zones, setZones] = useState([]) + const [ selectedDay, setSelectedDay ] = useState('') + const [ selectedWeek, setSelectedWeek ] = useState('') + const [ selectedZone, setSelectedZone ] = useState(null) + const [ selectedProject, setSelectedProject ] = useState(null) + const [ nbrPlaces , setNbrPlaces ] = useState(0) + const [ nbrCollabs, setNbrCollabs ] = useState(0) + const [ collabsAttributed, setCollabsAttributed ] = useState(0) + const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + const [ otherNbrPlaceState, setOtherNbrPlaceState ] = useState(0) + + const { toggleNotification } = useNotification() + + const attributedCollabsRef = useRef() + + useEffect(() => { + const fetchProjectsandZones = async () => { + setLoading(true) + try { + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${selectedWeek}/${selectedDay}`, {method: 'GET'}) + if(isSuccess){ + setCollabsAttributed(0) + setNbrCollabs(0) + setNbrPlaces(0) + if ( (data.projects && data.projects.length === 0) && (data.zones && data.zones.length === 0)){ + toggleNotification({ + visible: true, + message: "Il y'a pas de projets et de zones pour cette semaine et ce jour.", + type: "warning" + }) + setProjects([]) + setZones([]) + }else{ + if(data.projects && data.projects.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de projets pour cette semaine et ce jour.", + type: "warning" + }) + setProjects([]) + }else{ + setProjects(data.projects) + } + if(data.zones && data.zones.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de zones pour cette semaine et ce jour.", + type: "warning" + }) + setZones([]) + }else{ + setZones(data.zones) + } + } + }else{ + // handle error + setLoading(false) + } + } catch (error) { + console.log(error) + useNotification({ title: 'Erreur', content: error.message, type: 'error' }) + } finally { + setLoading(false) + } + } + if(selectedDay && selectedWeek) fetchProjectsandZones() + }, [selectedDay, selectedWeek]) + + const handleZoneSelection = async (e) => { + const zone_id = e.target.value + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces/${zone_id}`, {method: 'GET'}) + if(isSuccess){ + setSelectedZone(zone_id) + setNbrPlaces(data.place_count) + }else{ + // handle error + setNbrPlaces(0) + } + }catch(error){ + console.log(error) + } + } + + const handleProjectSelection = async (e) => { + const project_id = e.target.value + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingCollabs/${project_id}`, {method: 'GET'}) + if(isSuccess){ + setSelectedProject(project_id) + setNbrCollabs(data.user_count) + }else{ + // handle error + setNbrCollabs(0) + } + }catch(error){ + console.log(error) + } + } + + useEffect( () => { + if(nbrCollabs > 0 && nbrPlaces > 0){ + if( nbrCollabs <= nbrPlaces){ + console.log("collabs cahnged") + setCollabsAttributed(nbrCollabs) + attributedCollabsRef.current = nbrCollabs + }else{ + console.log("places cahnged") + setCollabsAttributed(nbrPlaces) + attributedCollabsRef.current = nbrPlaces + } + } + }, [nbrCollabs, nbrPlaces]) + + + const handleAddCollab = () =>{ + setCollabsAttributed(collabsAttributed + 1) + attributedCollabsRef.current = collabsAttributed + 1 + } + + const handleMinusCollab = () =>{ + setCollabsAttributed(collabsAttributed - 1) + attributedCollabsRef.current = collabsAttributed - 1 + } + + + + + const handleOtherZoneSelection = async (e) => { + const zone_id = e.target.value + setSelectedOtherZone(zone_id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces/${zone_id}`, {method: 'GET'}) + if(isSuccess){ + setOtherNbrPlaceState(data.place_count) + }else{ + // handle error + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + useEffect( () => { + if( (collabsAttributed - nbrPlaces) <= 0 && selectedOtherZone ) { + setSelectedOtherZone(null) + setOtherNbrPlaceState(0) + } + }, [collabsAttributed]) + + + const handleAssignProject = async () => { + const finalData = { + id_zone: selectedZone, + id_project: selectedProject, + jour: selectedDay, + semaine: selectedWeek, + nombre_personnes: collabsAttributed, + places_disponibles: (collabsAttributed > nbrPlaces) ? 0 : nbrPlaces - collabsAttributed, + places_occuper: (collabsAttributed > nbrPlaces) ? nbrPlaces : collabsAttributed, + } + if( selectedOtherZone && otherNbrPlaceState > 0){ + finalData.otherZone = { + id_zone: selectedOtherZone, + places_disponibles: ( (collabsAttributed - nbrPlaces ) > otherNbrPlaceState ) ? 0 : otherNbrPlaceState - (collabsAttributed - nbrPlaces), + places_occuper: ( (collabsAttributed - nbrPlaces ) > otherNbrPlaceState ) ? otherNbrPlaceState : collabsAttributed - nbrPlaces + } + } + console.log(finalData) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) + if(isSuccess){ + console.log(data) + listProjects( (prevListProjects) => [...prevListProjects, data]) + toggleNotification({ + visible: true, + message: "Projet affecté avec succès.", + type: "success" + }) + closePopup(false) + }else{ + console.log(errors) + toggleNotification({ + visible: true, + message: "Erreur lors de l'affectation du projet", + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + + } + + const closePopup = () => { + setIsOpen() + setSelectedWeek('') + setSelectedDay('') + setSelectedProject(null) + setSelectedZone(null) + setCollabsAttributed(0) + setNbrPlaces(0) + setNbrCollabs(0) + setSelectedOtherZone(null) + setOtherNbrPlaceState(0) + setProjects([]) + setZones([]) + setLoading(false) + + + } return (

    Attribuer un projet à une zone.

    {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> -
    +

    Veuillez sélectionner une date

    - setSelectedWeek(e.target.value)} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-500 outline-none'> - - - - - + + + + +
    - setSelectedDay(e.target.value)} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none'> - - - - - + + + + +
    - + {(!loading && (projects && projects.length)) ? + + : +
    + Aucun projet disponible +
    + }
    -

    : 50

    +

    : {nbrCollabs}

    - + {(!loading && (zones && zones.length)) ? + + : +
    + Aucune zone disponible +
    + }
    -

    : 50

    +

    : {nbrPlaces}

    -
    -
    - - -
    -
    -
    20
    - + {collabsAttributed > 0 && + <> +
    + Collaborateurs attribués +
    + {collabsAttributed} +
    + + +
    +
    +
    +
    +
    +
    0) ? "text-red-400" : "text-sushi-500"}`}>{nbrCollabs - collabsAttributed}
    + 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
    +
    +
    0) ? "text-red-400" : "text-sushi-500"}`}>{(nbrPlaces+ otherNbrPlaceState) - collabsAttributed}
    + 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
    +
    + restant
    - restant -
    -
    -
    -
    -

    Veuillez sélectionner une autre zonne (optionnel)

    -
    - -
    -

    Places: 50

    -
    -
    + {((collabsAttributed - nbrPlaces) > 0) &&
    +

    Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

    +
    + +
    +

    : {otherNbrPlaceState}

    +
    +
    +
    } + + }
    -
    - +
    ) diff --git a/src/app/assign_zone_project/page.jsx b/src/app/assign_zone_project/page.jsx index 0283164..2890835 100644 --- a/src/app/assign_zone_project/page.jsx +++ b/src/app/assign_zone_project/page.jsx @@ -1,12 +1,59 @@ "use client" -import React, { useState } from 'react'; +import React, { useEffect, useState } from 'react'; import AddIcon from "@/static/image/svg/add.svg"; import AssignProject from './AssignProject'; +import { useNotification } from '@/context/NotificationContext' +import fetchRequest from '../lib/fetchRequest'; +import Loader from '@/components/Loader/Loader' + const AffectingZoneProject = () => { const [isOpen, setIsOpen] = useState(false) + const [ listProjectsAffected, setListProjectsAffected ] = useState([]) + const [ isLoadingListProjects, setIsLoadingListProjects ] = useState(false) + const { toggleNotification } = useNotification() + const [ selectedWeek, setSelectedWeek ] = useState(null) + const [ selectedDay, setSelectedDay ] = useState(null) + + useEffect(() => { + const getListOfAffectedProjects = async () => { + setIsLoadingListProjects(true) + try{ + if(selectedDay && selectedWeek){ + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, {method: 'GET'}) + }else if (selectedWeek){ + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, {method: 'GET'}) + }else if (selectedDay){ + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, {method: 'GET'}) + }else{ + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, {method: 'GET'}) + } + if(isSuccess){ + setListProjectsAffected(data) + }else{ + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + setIsLoadingListProjects(false) + }catch(error){ + setIsLoadingListProjects(false) + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getListOfAffectedProjects() + }, [selectedDay, selectedWeek]) + + console.log(listProjectsAffected) const handleOpenAssignProject = () => { setIsOpen(!isOpen) @@ -14,129 +61,98 @@ const AffectingZoneProject = () => { return (
    - {isOpen && } + {isOpen && }

    List des Projets attribuer

    +
    + Il y a des projets qui ne sont pas complètement affecter. + +
    - setSelectedWeek(e.target.value)} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-500 outline-none'> - - - - - - - + + + + + + +
    + +
    - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - -
    - Date - - Plateau - - Projet - - Places occupées - - Nombre des personnes - - Places disponible - - Actions -
    - Apple MacBook Pro 17" - - Silver - - Laptop - - $2999 - - $2999 - - $2999 - - Edit -
    - Apple MacBook Pro 17" - - Silver - - Laptop - - $2999 - - $2999 - - $2999 - - Edit -
    - Apple MacBook Pro 17" - - Silver - - Laptop - - $2999 - - $2999 - - $2999 - - Edit -
    + { (!isLoadingListProjects && (listProjectsAffected && listProjectsAffected.length > 0)) ? + + + + + + + + + + + + + + { (listProjectsAffected.map( (element, index) => + + {/* */} + + + + + + + + + ))} + +
    + Date + + Plateau + + Projet + + Places occupées + + Nombre des personnes + + Places disponible + + Actions +
    + Semaine: {element.semaine} - Jour: {element.jour} + + {element.id_zone.nom}-{element.id_zone.id_etage.numero} + + {element.id_project.nom} + + {element.places_occuper} + + {element.nombre_personnes} + + {element.places_disponibles} + + Edit +
    + : +
    + }
    ); -- GitLab From e232dc9a363c49c83469853252d5e9b77d5654be Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 5 Jun 2024 09:39:58 +0100 Subject: [PATCH 36/79] added type de presence to sidebar --- src/app/ui/SideBar.jsx | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index 4236d7d..90f7df6 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -30,7 +30,11 @@ const SideBar = () => { link: "/planning" , icon: }, - , + { + label: "Type de Presence", + link: "/planning/type-presence" + , icon: + }, { label: "Tables", link: "/table" -- GitLab From dda021ca923b1e7d89d75f338e7d5008bedb8241 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Wed, 5 Jun 2024 13:01:18 +0100 Subject: [PATCH 37/79] before merging with layout --- src/app/assign_zone_project/AssignProject.jsx | 3 +- src/app/assign_zone_project/page.jsx | 116 ++++++++++-------- src/static/image/no-data.png | Bin 0 -> 266601 bytes 3 files changed, 63 insertions(+), 56 deletions(-) create mode 100644 src/static/image/no-data.png diff --git a/src/app/assign_zone_project/AssignProject.jsx b/src/app/assign_zone_project/AssignProject.jsx index 19eae4e..db74d0b 100644 --- a/src/app/assign_zone_project/AssignProject.jsx +++ b/src/app/assign_zone_project/AssignProject.jsx @@ -189,8 +189,7 @@ const AssignProject = ({ setIsOpen, listProjects }) => { try{ const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) if(isSuccess){ - console.log(data) - listProjects( (prevListProjects) => [...prevListProjects, data]) + listProjects( (prevListProjects) => [...prevListProjects, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])]) toggleNotification({ visible: true, message: "Projet affecté avec succès.", diff --git a/src/app/assign_zone_project/page.jsx b/src/app/assign_zone_project/page.jsx index 2890835..776da0c 100644 --- a/src/app/assign_zone_project/page.jsx +++ b/src/app/assign_zone_project/page.jsx @@ -5,6 +5,8 @@ import AssignProject from './AssignProject'; import { useNotification } from '@/context/NotificationContext' import fetchRequest from '../lib/fetchRequest'; import Loader from '@/components/Loader/Loader' +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/delete.svg"; @@ -94,62 +96,68 @@ const AffectingZoneProject = () => {

    Affecter Projet

    - { (!isLoadingListProjects && (listProjectsAffected && listProjectsAffected.length > 0)) ? - - - - - - - - - - - - - - { (listProjectsAffected.map( (element, index) => - - {/* */} - + { (listProjectsAffected.map( (element, index) => + + {/* */} + + + + + + + + + ))} + +
    - Date - - Plateau - - Projet - - Places occupées - - Nombre des personnes - - Places disponible - - Actions -
    - Semaine: {element.semaine} - Jour: {element.jour} + { (!isLoadingListProjects) ? + (listProjectsAffected && listProjectsAffected.length > 0) ? + + + + + + + + + + - - - - - - - ))} - -
    + Date + + Plateau + + Projet + + Places occupées + + Nombre des personnes + + Places disponible + + Actions - {element.id_zone.nom}-{element.id_zone.id_etage.numero} - - {element.id_project.nom} - - {element.places_occuper} - - {element.nombre_personnes} - - {element.places_disponibles} - - Edit -
    + +
    + Semaine: {element.semaine} - Jour: {element.jour} + + {element.id_zone.nom}-{element.id_zone.id_etage.numero} + + {element.id_project.nom} + + {element.places_occuper} + + {element.nombre_personnes} + + {element.places_disponibles} + +
    +
    +
    + : +
    + Aucun projet affecter +
    :
    } diff --git a/src/static/image/no-data.png b/src/static/image/no-data.png new file mode 100644 index 0000000000000000000000000000000000000000..90f0083b8a12e1f6b08f3dd3c2e301a81562afd4 GIT binary patch literal 266601 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

    -
    0) ? "text-red-400" : "text-sushi-500"}`}>{(nbrPlaces+ otherNbrPlaceState) - collabsAttributed}
    - 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
    0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
    + 0) ? "fill-red-400" : "fill-sushi-500"}`} />
    restant - {((collabsAttributed - nbrPlaces) > 0) &&
    + {((collabsAttributed - places.length) > 0) &&

    Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

    - handleOtherZoneSelection(e)} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none'> {zones.filter( (element) => element.id != selectedZone)?.map( (element, index) => )}
    -

    : {otherNbrPlaceState}

    +

    : {otherPlaces.length}

    } diff --git a/src/app/(dashboard)/place/RowPlace.jsx b/src/app/(dashboard)/place/RowPlace.jsx index 6e05792..1aad2e0 100644 --- a/src/app/(dashboard)/place/RowPlace.jsx +++ b/src/app/(dashboard)/place/RowPlace.jsx @@ -184,7 +184,7 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { {(tables && tables?.length) && tables.map((element, index) => ( )) } -- GitLab From 780d538f88e20ef03a86b8a1c4f6033275ae5566 Mon Sep 17 00:00:00 2001 From: "oussema.elbenney" Date: Thu, 6 Jun 2024 10:30:40 +0000 Subject: [PATCH 40/79] Update RowPlace.jsx --- src/app/(dashboard)/place/RowPlace.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/place/RowPlace.jsx b/src/app/(dashboard)/place/RowPlace.jsx index abb9bd7..3d22e48 100644 --- a/src/app/(dashboard)/place/RowPlace.jsx +++ b/src/app/(dashboard)/place/RowPlace.jsx @@ -184,7 +184,7 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { {(tables && tables?.length) && tables.map((element, index) => ( )) } @@ -223,4 +223,4 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { } -export default RowPlace \ No newline at end of file +export default RowPlace -- GitLab From 51c172f08f5b62cc8d31f27dd499e7193aae6651 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Thu, 6 Jun 2024 12:22:26 +0100 Subject: [PATCH 41/79] feature affecting project with delete --- .../(dashboard)/assign_zone_project/page.jsx | 63 ++++++++++++++++++- 1 file changed, 61 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/assign_zone_project/page.jsx index 685d01c..1c49160 100644 --- a/src/app/(dashboard)/assign_zone_project/page.jsx +++ b/src/app/(dashboard)/assign_zone_project/page.jsx @@ -8,6 +8,8 @@ import Loader from '@/components/Loader/Loader' import EditIcon from "@/static/image/svg/edit.svg"; import DeleteIcon from "@/static/image/svg/delete.svg"; import fetchRequest from "@/app/lib/fetchRequest"; +import ConfirmationModal from "@/app/ui/ConfirmationModal"; + @@ -18,6 +20,8 @@ const AffectingZoneProject = () => { const { toggleNotification } = useNotification() const [ selectedWeek, setSelectedWeek ] = useState(null) const [ selectedDay, setSelectedDay ] = useState(null) + const [ selectedAffectaionToDelete, setSelectedAffectationToDelete ] = useState(null) + const [isModalOpen, setModalOpen] = useState(false); useEffect(() => { @@ -56,11 +60,60 @@ const AffectingZoneProject = () => { getListOfAffectedProjects() }, [selectedDay, selectedWeek]) - console.log(listProjectsAffected) const handleOpenAssignProject = () => { setIsOpen(!isOpen) } + + const handleDeleteAffectation = async () => { + try{ + console.log("qsdsqdqsdsqdq") + var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, {method: 'DELETE'}) + if(isSuccess){ + toggleNotification({ + visible: true, + message: "Affectation supprimer avec succès", + type: "success" + }) + setListProjectsAffected(listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id)) + }else if(status === 404){ + toggleNotification({ + visible: true, + message: "Affectation introuvable", + type: "error" + }) + }else{ + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const handleDeleteClick = (element) => { + setSelectedAffectationToDelete(element) + setModalOpen(true); + } + + + const handleConfirmDelete = () => { + console.log("qsdsq") + handleDeleteAffectation(); + console.log("fdsfsd") + setModalOpen(false); + setSelectedAffectationToDelete(null); + + }; + return (
    @@ -149,7 +202,7 @@ const AffectingZoneProject = () => {
    -
    +
    handleDeleteClick(element)} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
    @@ -175,9 +175,9 @@ const AffectingZoneProject = () => { - { (listProjectsAffected.map( (element, index) => + {(listProjectsAffected.map((element, index) => - {/* */} + {/* */} diff --git a/src/app/(dashboard)/reservation/TableUI.jsx b/src/app/(dashboard)/reservation/TableUI.jsx index 6a22027..33a55c9 100644 --- a/src/app/(dashboard)/reservation/TableUI.jsx +++ b/src/app/(dashboard)/reservation/TableUI.jsx @@ -1,7 +1,11 @@ -import React from 'react' +import React, { useContext } from 'react' +import { ReservationContext } from './page'; +import fetchRequest from '@/app/lib/fetchRequest'; -const TableUI = ({ tableName, places }) => { +const TableUI = ({ id, numero, places, bookedPlaces }) => { function groupConsecutive(arr) { + + arr = arr.sort((a, b) => a.id - b.id) if (arr.length === 0) { return []; } @@ -21,20 +25,54 @@ const TableUI = ({ tableName, places }) => { return grouped; } const proccessedPlaces = groupConsecutive(places).reverse() + const { allPlaces, currentDateData } = useContext(ReservationContext) + + const reservePlace = async (id) => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/reservations/', { + method: "POST", body: JSON.stringify( + { + "presence": false, + "date": currentDateData.date, + "id_place": id + } + ) + }); + if (isSuccess) { + console.log(data); + toggleNotification({ + visible: true, + message: "La réservation a été enregistrer avec succés", + type: "success" + }) + } else { + console.log(errors) + } + } + + const findPlace = (id) => { + return allPlaces?.find((place) => place.id === id) + } + return (
    {proccessedPlaces.map((element, index) => { return
    -
    -
    - {element[0]} -
    +
    + {(allPlaces?.find((place) => place.id === element[0].id)) + ?
    reservePlace(element[0].id)} className='absolute text-white items-center flex justify-center h-full w-full px-[2px] text-sm bg-blue-500/80'> +

    {allPlaces.find((place) => place.id === element[0].id).project_name}

    +
    + :
    + }
    - {(element.length > 1) &&
    -
    - {element[1]} -
    + {(element.length > 1) &&
    + {findPlace(element[1].id) + ?
    reservePlace(element[1].id)} className='absolute text-white items-center flex justify-center h-full px-[2px] w-full text-sm bg-blue-500/80'> +

    {allPlaces.find((place) => place.id === element[1].id).project_name}

    +
    + :
    + }
    }
    })} diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx index f30e8b0..cd387ac 100644 --- a/src/app/(dashboard)/reservation/ZoneUI.jsx +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -1,13 +1,13 @@ import React from 'react' import TableUI from './TableUI' -const ZoneUI = ({ tables, zoneName }) => { +const ZoneUI = ({ id, tables, nom }) => { return (
    -

    {zoneName}

    +

    {nom}

    - {tables.map((table, index) => { - return + {tables.map((table) => { + return })}
    diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index 414ce0d..2fbab06 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -4,63 +4,129 @@ import React, { useEffect, useState } from 'react' import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' + +export const ReservationContext = React.createContext() const Reservation = () => { - const [selectedFloor, setSelectedFloor] = useState(5) - const [floors, setFloors] = useState([]) - const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) const [isLoadingData, setIsLoadingData] = useState(false) - + const { toggleNotification } = useNotification() + const [date, setDate] = useState({ day: null, week: null }) + const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) + const [floors, setFloors] = useState([]) + const [projectsData, setProjectsData] = useState([]) + const [currentDateData, setCurrentDateData] = useState(null) + const [datesData, setDatesData] = useState(null) + const [bookedPlaces, setBookedPlaces] = useState([]) useEffect(() => { - const getAllFloors = async () => { + const getPlan = async () => { try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etage-zone-table-place/') setIsLoadingSelectsData(false) if (isSuccess) { setFloors(data) } else { - setFloors([]) + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) } } catch (error) { - setIsLoadingSelectsData(false) console.log(error) } } - getAllFloors() + getPlan() }, []) - const [zones, setZones] = useState([ - { - zoneName: "A", - tables: [ - { - tableName: 1, - places: [1, 2, 3, 4, 5, 6, 7, 8] - }, - // { - // tableName: 2, - // places: [1, 2, 3, 4, 5, 6, 7, 8] - // }, - // { - // tableName: 3, - // places: [1, 2, 3, 4, 5, 6, 7] - // } - ] - }, - { - zoneName: "B", - tables: [ - { - tableName: 1, - places: [1, 2, 3, 4, 5, 6, 7, 8] - }, - // { - // tableName: 2, - // places: [1, 2, 3, 4, 5, 6, 8] - // } - ] + + + useEffect(() => { + const getSyncData = async () => { + const getUserPlan = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/user-projects-zones-places/${date.week}/${date.day}`) + if (isSuccess) { + setProjectsData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const getBookedPlaces = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${currentDateData.date}/`) + if (isSuccess) { + console.log("booked places : :", data) + setBookedPlaces(data.map((element) => ({ ...element, id_place: element.id_place.id, presence: element.presence, id_user: element.id_user.id }))) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + setIsLoadingData(true) + await Promise.all([getUserPlan(), getBookedPlaces()]) + setIsLoadingData(false) } - ]) + if (date.week && date.day) getSyncData() + else { + setProjectsData([]) + setBookedPlaces([]) + } + + }, [date.week, date.day]) + + useEffect(() => { + const getCurrentDateData = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/current-date/'); + if (isSuccess) { + setCurrentDateData(data) + } else { + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const YearCalendar = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/dates/'); + if (isSuccess) { + console.log("dates data", data) + setDatesData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const getSyncData = async () => { + await Promise.all([getCurrentDateData(), YearCalendar()]) + } + getSyncData() + }, []) + + const handleChangeDate = (event) => { + const target = event.target + setDate({ ...date, [target.name]: target.value }) + } + + + const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] + const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] + const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] + if (isLoadingSelectsData) return
    @@ -68,22 +134,24 @@ const Reservation = () => { return (
    - - + + + + + + - - + + + + + + +
    @@ -105,9 +173,16 @@ const Reservation = () => {
    {(!isLoadingData) ?
    - {zones.map((zone, index) => { - return - })} + + {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return
    +

    Etage {floor.numero}

    + {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
    + })} +
    :
    -- GitLab From cc94c788641ad5706f6c7c8c2c4b1013d4101cd5 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 10 Jun 2024 14:53:30 +0100 Subject: [PATCH 45/79] add & cancel reservation --- src/app/(dashboard)/reservation/PlaceUI.jsx | 118 ++++++++++++++++++++ src/app/(dashboard)/reservation/TableUI.jsx | 52 ++------- src/app/(dashboard)/reservation/page.jsx | 9 +- src/app/ui/ConfirmationModal.jsx | 29 ++++- 4 files changed, 158 insertions(+), 50 deletions(-) create mode 100644 src/app/(dashboard)/reservation/PlaceUI.jsx diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx new file mode 100644 index 0000000..17a18ab --- /dev/null +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -0,0 +1,118 @@ +"use client" +import React, { useContext, useMemo, useState } from 'react' +import { ReservationContext } from './page' +import fetchRequest from '@/app/lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +import Cookies from 'js-cookie'; +import { decrypt } from '@/app/lib/session'; +import ConfirmationModal from '@/app/ui/ConfirmationModal' +const PlaceUI = ({ id }) => { + + const { allPlaces, currentDateData, bookedPlaces, setBookedPlaces } = useContext(ReservationContext) + const { toggleNotification } = useNotification() + const [isOpenBooking, setIsOpenBooking] = useState(false) + const [isOpenCanceling, setIsOpenCanceling] = useState(false) + const [authenticatedUserData, setAuthenticatedUserData] = useState(null) + + const cookie = Cookies.get("session") + const getUserData = async () => { + try { + if (cookie) { + const data = await decrypt(cookie) + setAuthenticatedUserData(data) + } + } catch (error) { + console.log(error) + } + } + useMemo(getUserData, [cookie]) + const place = allPlaces?.find((place) => place.id === id) + const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) + const hasPlace = bookedPlaces?.some((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) + const handleBooking = (event) => { + event.stopPropagation() + if (hasPlace) { + toggleNotification({ + visible: true, + message: "Veuillez annuler votre réservation pour réserver une nouvelle place .", + type: "warning" + }) + } + else setIsOpenBooking(true) + } + const handleBookingConfirmation = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/reservations/', { + method: "POST", body: JSON.stringify( + { + "presence": false, + "date": currentDateData.date, + "id_place": id + } + ) + }); + if (isSuccess) { + console.log(data); + setBookedPlaces([...bookedPlaces, data]) + toggleNotification({ + visible: true, + message: "La réservation a été enregistrer avec succés", + type: "success" + }) + setIsOpenBooking(false) + } else { + console.log(errors) + } + } + const handleCancelingConfirmation = async (idReservation) => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/${idReservation}`, { + method: "DELETE" + }); + if (isSuccess) { + console.log(data); + setBookedPlaces(bookedPlaces.filter((element) => element.id !== idReservation)) + toggleNotification({ + visible: true, + message: "La réservation a été annuler avec succés", + type: "success" + }) + setIsOpenCanceling(false) + } else { + console.log(errors) + } + } + + const handleCanceling = (event) => { + event.stopPropagation() + setIsOpenCanceling(true) + } + const closeCancelingPopup = (event) => { + event.stopPropagation() + setIsOpenCanceling(false) + } + const closeConfirmationPopup = (event) => { + event.stopPropagation() + setIsOpenBooking(false) + } + + + if (authenticatedUserData) + if (place) + if (bookedPlace) + if (bookedPlace.id_user === authenticatedUserData.sessionData?.user_id) + return
    +

    {place.project_name || ""}

    + handleCancelingConfirmation(bookedPlace.id)} onClose={closeCancelingPopup} /> +
    + else return
    +

    {place.project_name || ""}

    +
    + else return
    +

    {place.project_name || ""}

    + +
    + else return
    + else return <> +} + + +export default PlaceUI \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/TableUI.jsx b/src/app/(dashboard)/reservation/TableUI.jsx index 33a55c9..8c77ad5 100644 --- a/src/app/(dashboard)/reservation/TableUI.jsx +++ b/src/app/(dashboard)/reservation/TableUI.jsx @@ -1,8 +1,7 @@ -import React, { useContext } from 'react' -import { ReservationContext } from './page'; -import fetchRequest from '@/app/lib/fetchRequest'; +import React from 'react' +import PlaceUI from './PlaceUI'; -const TableUI = ({ id, numero, places, bookedPlaces }) => { +const TableUI = ({ id, numero, places }) => { function groupConsecutive(arr) { arr = arr.sort((a, b) => a.id - b.id) @@ -24,55 +23,20 @@ const TableUI = ({ id, numero, places, bookedPlaces }) => { return grouped; } - const proccessedPlaces = groupConsecutive(places).reverse() - const { allPlaces, currentDateData } = useContext(ReservationContext) + const processedPlaces = groupConsecutive(places).reverse() - const reservePlace = async (id) => { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/reservations/', { - method: "POST", body: JSON.stringify( - { - "presence": false, - "date": currentDateData.date, - "id_place": id - } - ) - }); - if (isSuccess) { - console.log(data); - toggleNotification({ - visible: true, - message: "La réservation a été enregistrer avec succés", - type: "success" - }) - } else { - console.log(errors) - } - } - - const findPlace = (id) => { - return allPlaces?.find((place) => place.id === id) - } + if (!processedPlaces || processedPlaces.length === 0) return <> return (
    - {proccessedPlaces.map((element, index) => { + {processedPlaces.map((element, index) => { return
    - {(allPlaces?.find((place) => place.id === element[0].id)) - ?
    reservePlace(element[0].id)} className='absolute text-white items-center flex justify-center h-full w-full px-[2px] text-sm bg-blue-500/80'> -

    {allPlaces.find((place) => place.id === element[0].id).project_name}

    -
    - :
    - } +
    {(element.length > 1) &&
    - {findPlace(element[1].id) - ?
    reservePlace(element[1].id)} className='absolute text-white items-center flex justify-center h-full px-[2px] w-full text-sm bg-blue-500/80'> -

    {allPlaces.find((place) => place.id === element[1].id).project_name}

    -
    - :
    - } +
    }
    })} diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index 2fbab06..2ff4d3a 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -5,6 +5,7 @@ import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '@/app/ui/ConfirmationModal' export const ReservationContext = React.createContext() @@ -166,14 +167,18 @@ const Reservation = () => {

    Réservé par vous

    +
    +
    +

    Réservé par un collègue

    +
    -

    Confirmé

    +

    Présent

    {(!isLoadingData) ?
    - + {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { return

    Etage {floor.numero}

    diff --git a/src/app/ui/ConfirmationModal.jsx b/src/app/ui/ConfirmationModal.jsx index 1c667ac..ad5e6cd 100644 --- a/src/app/ui/ConfirmationModal.jsx +++ b/src/app/ui/ConfirmationModal.jsx @@ -1,8 +1,29 @@ +"use client" + import React from 'react'; -const ConfirmationModal = ({ isOpen, onClose, onConfirm, message }) => { +const ConfirmationModal = ({ isOpen, onClose, onConfirm, message, type }) => { if (!isOpen) return null; - + else if (type === "create") return
    +
    +

    Confirmation

    +

    {message}

    +
    + + +
    +
    +
    return (
    @@ -13,13 +34,13 @@ const ConfirmationModal = ({ isOpen, onClose, onConfirm, message }) => { onClick={onClose} className="px-4 py-2 bg-gray-300 text-gray-700 rounded-md" > - Cancel + Annuler
    -- GitLab From 555d08bee5c31b87d9a0a8c7d44c71c3ee62c0dc Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Mon, 10 Jun 2024 14:56:18 +0100 Subject: [PATCH 46/79] fixed reservation dates --- .../(dashboard)/planning/PlanningTable.jsx | 6 ++-- src/app/(dashboard)/reservation/page.jsx | 36 +++++++++---------- 2 files changed, 21 insertions(+), 21 deletions(-) diff --git a/src/app/(dashboard)/planning/PlanningTable.jsx b/src/app/(dashboard)/planning/PlanningTable.jsx index 2fbdf40..2bf1d00 100644 --- a/src/app/(dashboard)/planning/PlanningTable.jsx +++ b/src/app/(dashboard)/planning/PlanningTable.jsx @@ -30,12 +30,12 @@ const PlanningTable = ({ data, typePresences, onTypePresenceChange }) => { diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index 2fbab06..00608c3 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -118,40 +118,40 @@ const Reservation = () => { }, []) const handleChangeDate = (event) => { - const target = event.target - setDate({ ...date, [target.name]: target.value }) + const date = JSON.parse(event.target.value); + console.log("weekMonthly", date.weekMonthly); + console.log("day", date.day); + setDate({ day: date.day, week: date.weekMonthly }) } const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] - + console.log("all places", allPlaces) if (isLoadingSelectsData) return
    + + const currentMonth = new Date().getMonth() + 1 + // filter dates from now to 2 weeks later + const filteredDatesData = datesData?.filter((element) => { + const date = new Date(element.date) + const month = date.getMonth() + 1 + return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + }) return (
    - - -
    -- GitLab From 5a31baf40f202079ad4ebe9c7be7e4d5b2e83f49 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 10 Jun 2024 15:20:06 +0100 Subject: [PATCH 47/79] handle reservation exceptions --- src/app/(dashboard)/reservation/PlaceUI.jsx | 27 +++++++++++++++++++-- src/app/(dashboard)/reservation/ZoneUI.jsx | 2 +- src/app/(dashboard)/reservation/page.jsx | 1 - 3 files changed, 26 insertions(+), 4 deletions(-) diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx index 17a18ab..cf831dd 100644 --- a/src/app/(dashboard)/reservation/PlaceUI.jsx +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -7,7 +7,6 @@ import Cookies from 'js-cookie'; import { decrypt } from '@/app/lib/session'; import ConfirmationModal from '@/app/ui/ConfirmationModal' const PlaceUI = ({ id }) => { - const { allPlaces, currentDateData, bookedPlaces, setBookedPlaces } = useContext(ReservationContext) const { toggleNotification } = useNotification() const [isOpenBooking, setIsOpenBooking] = useState(false) @@ -29,6 +28,7 @@ const PlaceUI = ({ id }) => { const place = allPlaces?.find((place) => place.id === id) const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) const hasPlace = bookedPlaces?.some((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) + if (bookedPlace) console.log("place id", id) const handleBooking = (event) => { event.stopPropagation() if (hasPlace) { @@ -60,11 +60,23 @@ const PlaceUI = ({ id }) => { }) setIsOpenBooking(false) } else { + if (errors.type === "ValidationError" && errors.detail?.non_field_errors && errors.detail?.non_field_errors[0]?.indexOf("date, id_place must make a unique set") !== -1) { + toggleNotification({ + visible: true, + message: "La place a été déjà réservée par votre collègue", + type: "warning" + }) + } + else toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) console.log(errors) } } const handleCancelingConfirmation = async (idReservation) => { - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/${idReservation}`, { + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/reservations/${idReservation}`, { method: "DELETE" }); if (isSuccess) { @@ -77,6 +89,17 @@ const PlaceUI = ({ id }) => { }) setIsOpenCanceling(false) } else { + if (status === 404) + toggleNotification({ + visible: true, + message: "La réservation n'existe pas", + type: "warning" + }) + else toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) console.log(errors) } } diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx index cd387ac..5fb60ee 100644 --- a/src/app/(dashboard)/reservation/ZoneUI.jsx +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -3,7 +3,7 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return ( -
    +

    {nom}

    {tables.map((table) => { diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index 2ff4d3a..f1ddaf7 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -5,7 +5,6 @@ import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { useNotification } from '@/context/NotificationContext' -import ConfirmationModal from '@/app/ui/ConfirmationModal' export const ReservationContext = React.createContext() -- GitLab From af0babff47bd535570f908596725463055033224 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Mon, 10 Jun 2024 15:47:10 +0100 Subject: [PATCH 48/79] fixed reservation dates --- src/app/(dashboard)/reservation/page.jsx | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index f31f2cd..a9d0e7b 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -58,7 +58,7 @@ const Reservation = () => { } } const getBookedPlaces = async () => { - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${currentDateData.date}/`) + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${date.date}/`) if (isSuccess) { console.log("booked places : :", data) setBookedPlaces(data.map((element) => ({ ...element, id_place: element.id_place.id, presence: element.presence, id_user: element.id_user.id }))) @@ -118,10 +118,10 @@ const Reservation = () => { }, []) const handleChangeDate = (event) => { - const date = JSON.parse(event.target.value); - console.log("weekMonthly", date.weekMonthly); - console.log("day", date.day); - setDate({ day: date.day, week: date.weekMonthly }) + const dateSelected = JSON.parse(event.target.value); + console.log("weekMonthly", dateSelected.weekMonthly); + console.log("day", dateSelected.day); + setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date}) } -- GitLab From 1fc4f0bd95182af174534e3556b35519af6b8f23 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Mon, 10 Jun 2024 16:59:29 +0100 Subject: [PATCH 49/79] stable reservation UI --- src/app/(dashboard)/reservation/PlaceUI.jsx | 5 +- src/app/(dashboard)/reservation/ZoneUI.jsx | 2 +- src/app/(dashboard)/reservation/page.jsx | 56 +++++++++++++-------- src/app/ui/SideBar.jsx | 5 ++ 4 files changed, 44 insertions(+), 24 deletions(-) diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx index cf831dd..f347155 100644 --- a/src/app/(dashboard)/reservation/PlaceUI.jsx +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -7,7 +7,7 @@ import Cookies from 'js-cookie'; import { decrypt } from '@/app/lib/session'; import ConfirmationModal from '@/app/ui/ConfirmationModal' const PlaceUI = ({ id }) => { - const { allPlaces, currentDateData, bookedPlaces, setBookedPlaces } = useContext(ReservationContext) + const { allPlaces, selectedDate, bookedPlaces, setBookedPlaces } = useContext(ReservationContext) const { toggleNotification } = useNotification() const [isOpenBooking, setIsOpenBooking] = useState(false) const [isOpenCanceling, setIsOpenCanceling] = useState(false) @@ -28,7 +28,6 @@ const PlaceUI = ({ id }) => { const place = allPlaces?.find((place) => place.id === id) const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) const hasPlace = bookedPlaces?.some((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) - if (bookedPlace) console.log("place id", id) const handleBooking = (event) => { event.stopPropagation() if (hasPlace) { @@ -45,7 +44,7 @@ const PlaceUI = ({ id }) => { method: "POST", body: JSON.stringify( { "presence": false, - "date": currentDateData.date, + "date": selectedDate, "id_place": id } ) diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx index 5fb60ee..48ae7d1 100644 --- a/src/app/(dashboard)/reservation/ZoneUI.jsx +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -3,7 +3,7 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return ( -
    +

    {nom}

    {tables.map((table) => { diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index a9d0e7b..37921d1 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -118,10 +118,15 @@ const Reservation = () => { }, []) const handleChangeDate = (event) => { - const dateSelected = JSON.parse(event.target.value); - console.log("weekMonthly", dateSelected.weekMonthly); - console.log("day", dateSelected.day); - setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date}) + if (event.target.value) { + const dateSelected = JSON.parse(event.target.value); + console.log("weekMonthly", dateSelected.weekMonthly); + console.log("day", dateSelected.day); + setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) + } + else { + setDate({ day: null, week: null }) + } } @@ -137,9 +142,9 @@ const Reservation = () => { const currentMonth = new Date().getMonth() + 1 // filter dates from now to 2 weeks later const filteredDatesData = datesData?.filter((element) => { - const date = new Date(element.date) - const month = date.getMonth() + 1 - return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + const date = new Date(element.date) + const month = date.getMonth() + 1 + return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) }) return (
    @@ -156,7 +161,7 @@ const Reservation = () => {
    -

    Indisponible

    +

    Autres Projets

    @@ -176,18 +181,29 @@ const Reservation = () => {
    {(!isLoadingData) - ?
    - - {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { - return
    -

    Etage {floor.numero}

    - {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { - return - })} -
    - })} -
    -
    + ? <> + {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0) &&
    + +
    } +
    + + {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return
    +

    Etage {floor.numero}

    + {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
    + })} + {floors.filter((element) => concernedFloors.includes(element.id)).length === 0 + &&
    +

    Un exemple d'un message à l'utilisateur

    +
    } +
    +
    + :
    diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index 90f7df6..4bab50a 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -25,6 +25,11 @@ const SideBar = () => { link: "/projects" , icon: }, + { + label: "Réservation", + link: "/reservation", + icon: + }, { label: "Planning", link: "/planning" -- GitLab From ccb712c2586a45654a6e7893af5cd2cc6e9cac84 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Mon, 10 Jun 2024 18:15:22 +0100 Subject: [PATCH 50/79] version affectation all exceptions handled --- .../assign_zone_project/AssignProject.jsx | 13 +- .../CompleteAffectation.jsx | 303 ++++++++++++++++++ .../(dashboard)/assign_zone_project/page.jsx | 130 +++++++- 3 files changed, 428 insertions(+), 18 deletions(-) create mode 100644 src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx diff --git a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx index afafe64..7eb743d 100644 --- a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx +++ b/src/app/(dashboard)/assign_zone_project/AssignProject.jsx @@ -9,7 +9,7 @@ import AddIcon from "@/static/image/svg/add.svg" import fetchRequest from "@/app/lib/fetchRequest"; -const AssignProject = ({ setIsOpen, listProjects, affectations }) => { +const AssignProject = ({ setIsOpen, listProjects, affectations, mutateProjectAffection }) => { const [loading, setLoading] = useState(false) const [projects, setProjects] = useState([]) const [zones, setZones] = useState([]) @@ -22,12 +22,10 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { const [ collabsAttributed, setCollabsAttributed ] = useState(0) const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) const [ otherPlaces, setOtherPlaces ] = useState([]) - const [ affectaionsRelatedToZones, setAffectationsRelatedToZones ] = useState([]) const { toggleNotification } = useNotification() const attributedCollabsRef = useRef() - console.log(affectations) useEffect(() => { const fetchProjectsandZones = async () => { setLoading(true) @@ -92,7 +90,6 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { zone_id: zone_id })}) if(isSuccess){ - console.log(data.places) setSelectedZone(zone_id) setPlaces(data.places) setOtherPlaces([]) @@ -183,14 +180,13 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { } }, [collabsAttributed]) - const handleAssignProject = async () => { const finalData = { id_zone: selectedZone, id_project: selectedProject, jour: selectedDay, semaine: selectedWeek, - nombre_personnes: collabsAttributed, + nombre_personnes: nbrCollabs, places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) @@ -206,7 +202,9 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { try{ const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) if(isSuccess){ - listProjects( (prevListProjects) => [...prevListProjects, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])]) + const newAffectations = [...affectations, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])] + listProjects(newAffectations) + mutateProjectAffection(newAffectations) toggleNotification({ visible: true, message: "Projet affecté avec succès.", @@ -229,7 +227,6 @@ const AssignProject = ({ setIsOpen, listProjects, affectations }) => { type: "error" }) } - } const closePopup = () => { diff --git a/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx b/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx new file mode 100644 index 0000000..16d9adf --- /dev/null +++ b/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx @@ -0,0 +1,303 @@ +"use client" +import React, { useState, useEffect, useRef } from 'react' +import { useNotification } from '@/context/NotificationContext' +import CancelIcon from "@/static/image/svg/cancel.svg" +import UserIcon from "@/static/image/svg/user.svg" +import DeskIcon from "@/static/image/svg/study-desk.svg" +import AddIcon from "@/static/image/svg/add.svg" +import fetchRequest from "@/app/lib/fetchRequest"; +import Loader from '@/components/Loader/Loader' + + + +const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, fullAffectations, mutateProjectAffection }) => { + const [ selectedProject, setSelectedProject ] = useState(null) + const [ zones, setZones ] = useState([]) + const [ loading, setLoading ] = useState(false) + const [ places , setPlaces ] = useState([]) + const [ nbrCollabs, setNbrCollabs ] = useState(0) + const [ collabsAttributed, setCollabsAttributed ] = useState(0) + const { toggleNotification } = useNotification() + const [ selectedZone, setSelectedZone ] = useState(null) + const [ otherPlaces, setOtherPlaces ] = useState([]) + const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + + + const getZones = async (day, week) => { + console.log("day, week", day, week) + setLoading(true) + try { + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/affectingProject/${day}/${week}`, {method: 'GET'}) + if(isSuccess){ + setCollabsAttributed(0) + setPlaces([]) + if(data.zones && data.zones.length === 0){ + toggleNotification({ + visible: true, + message: "Il y'a pas de zones pour cette semaine et ce jour.", + type: "warning" + }) + setZones([]) + }else{ + console.log("dsqqqqqdsqdqs") + setZones(data.zones) + } + + }else{ + // handle error + setLoading(false) + } + } catch (error) { + console.log(error) + toggleNotification({ title: 'Erreur', content: error.message, type: 'error' }) + } finally { + setLoading(false) + } + } + + const handleProjectSelection = (project) => { + if(selectedProject && selectedProject.project.id === project.project.id){ + setSelectedProject(null) + setZones([]) + setPlaces([]) + setCollabsAttributed(0) + return + } + console.log(project) + setSelectedProject(project) + getZones(project.jour, project.semaine) + setNbrCollabs(project.nbr_personnes_restant) + } + + + useEffect( () => { + if(nbrCollabs > 0 && places.length > 0){ + if( nbrCollabs <= places.length){ + setCollabsAttributed(nbrCollabs) + }else{ + setCollabsAttributed(places.length) + } + } + }, [nbrCollabs, places]) + + const handleSelectionZone = async (e) => { + const zone_id = e.target.value + const related_affecations = fullAffectations.filter( (element) => element.jour == selectedProject.jour && element.semaine == selectedProject.week && element.id_zone.id == zone_id).map(element => element.id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })}) + if(isSuccess){ + console.log(data.places) + setSelectedZone(zone_id) + setPlaces(data.places) + setOtherPlaces([]) + setSelectedOtherZone(null) + }else{ + // handle error + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + + const handleOtherZoneSelection = async (e) => { + const zone_id = e.target.value + const related_affecations = fullAffectations.filter( (element) => element.jour == selectedProject.jour && element.semaine == selectedProject.semaine && element.id_zone.id == zone_id).map(element => element.id) + setSelectedOtherZone(zone_id) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/countingPlaces`, + {method: 'POST', body: JSON.stringify({ + related_affecations: related_affecations, + zone_id: zone_id + })} + ) + if(isSuccess){ + setOtherPlaces(data.places) + }else{ + // handle error + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const handleAddCollab = () =>{ + setCollabsAttributed(collabsAttributed + 1) + } + + const handleMinusCollab = () =>{ + setCollabsAttributed(collabsAttributed - 1) + } + + console.log("collabsAttributed", collabsAttributed) + console.log("nbrCollabs", nbrCollabs) + console.log("places", places) + + + + const handleAssignProject = async () => { + const finalData = { + id_zone: selectedZone, + id_project: selectedProject.project.id, + jour: selectedProject.jour, + semaine: selectedProject.semaine, + nombre_personnes: nbrCollabs, + places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, + places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, + places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) + } + if( selectedOtherZone && otherPlaces.length > 0){ + finalData.otherZone = { + id_zone: selectedOtherZone, + places_disponibles: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? 0 : otherPlaces.length - (collabsAttributed - places.length), + places_occuper: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.length : collabsAttributed - places.length, + places: ( (collabsAttributed - places.length ) > otherPlaces.length ) ? otherPlaces.map( (element) => element.id) : otherPlaces.map( (element, index) => (index < (collabsAttributed - places.length)) && element.id).filter(id => id !== false) + } + } + console.log(finalData) + try{ + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/affectingProject`, {method: 'POST', body: JSON.stringify(finalData)}) + if(isSuccess){ + const newAffectations = [...fullAffectations, data.main_zone, ...(data.second_zone ? [data.second_zone] : [])] + listAffectationsState(newAffectations) + mutateProjectAffection(newAffectations) + toggleNotification({ + visible: true, + message: "Projet affecté avec succès.", + type: "success" + }) + closePopup(false) + }else{ + console.log(errors) + toggleNotification({ + visible: true, + message: "Erreur lors de l'affectation du projet", + type: "error" + }) + } + }catch(error){ + console.log(error) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + + const closePopup = () => { + setIsOpen() + setSelectedProject(null) + setSelectedZone(null) + setCollabsAttributed(0) + setPlaces([]) + setNbrCollabs(0) + setSelectedOtherZone(null) + setOtherPlaces([]) + setZones([]) + setLoading(false) + } + + + + return ( +
    +
    +

    Compléter l'affectation

    + {setIsOpen(false)}} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> +
    +
    +

    Veuillez sélectionner un projet

    +
    + {(affectations) && + affectations.map((element, index) => +
    handleProjectSelection(element)} key={index} className='border-b border-gray-200'> + Projet: {element?.project?.nom} -- Collaborateurs: {element.nbr_personnes_restant} +
    + ) + } +
    +
    +
    +

    Veuillez sélectionner une zone

    +
    + {(!loading) ? + (zones && zones.length) ? + + : +
    + Aucune zone disponible +
    + : +
    + +
    + + } +
    +

    : {places.length}

    +
    +
    +
    + {(collabsAttributed > 0) &&
    +

    Collaborateurs affectées

    +
    +
    + {(collabsAttributed) ? collabsAttributed : 0} +
    + + +
    +
    +
    +
    +
    0) ? "text-red-400" : "text-sushi-500"}`}>{nbrCollabs - collabsAttributed}
    + 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
    +
    +
    0) ? "text-red-400" : "text-sushi-500"}`}>{(places.length+ otherPlaces.length) - collabsAttributed}
    + 0) ? "fill-red-400" : "fill-sushi-500"}`} /> +
    +
    +
    +
    } + {((collabsAttributed - places.length) > 0) &&
    +

    Veuillez sélectionner une autre zonne pour compléter l'affectation (optionnel)

    +
    + +
    +

    : {otherPlaces.length}

    +
    +
    +
    } +
    +
    + +
    +
    +
    + ) +} + +export default CompleteAffectation \ No newline at end of file diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/assign_zone_project/page.jsx index 85c5bee..758252e 100644 --- a/src/app/(dashboard)/assign_zone_project/page.jsx +++ b/src/app/(dashboard)/assign_zone_project/page.jsx @@ -9,12 +9,14 @@ import EditIcon from "@/static/image/svg/edit.svg"; import DeleteIcon from "@/static/image/svg/delete.svg"; import fetchRequest from "@/app/lib/fetchRequest"; import ConfirmationModal from "@/app/ui/ConfirmationModal"; +import CompleteAffectation from './CompleteAffectation'; const AffectingZoneProject = () => { const [isOpen, setIsOpen] = useState(false) + const [ isOpenCompleteAffectation, setIsOpenCompleteAffectation ] = useState(false) const [ listProjectsAffected, setListProjectsAffected ] = useState([]) const [ isLoadingListProjects, setIsLoadingListProjects ] = useState(false) const { toggleNotification } = useNotification() @@ -22,9 +24,55 @@ const AffectingZoneProject = () => { const [ selectedDay, setSelectedDay ] = useState(null) const [ selectedAffectaionToDelete, setSelectedAffectationToDelete ] = useState(null) const [isModalOpen, setModalOpen] = useState(false); + const [ listProjectsSemiAffected, setListProjectsSemiAffected ] = useState([]) useEffect(() => { + + // this function is to detect if there is a project that is not fully affected and return the corresponding data to use + function filterAndGroupProjects(data) { + // Step 1: Create an object to aggregate data by id_project, semaine, and jour + const aggregatedProjects = {}; + + data.forEach(project => { + const projectId = project.id_project.id; + const week = project.semaine; + const day = project.jour; + const key = `${projectId}-${week}-${day}`; + + if (!aggregatedProjects[key]) { + aggregatedProjects[key] = { + id_project: project.id_project, + semaine: week, + jour: day, + places_disponibles: 0, + places_occuper: 0, + nombre_personnes: project.nombre_personnes + }; + } + + aggregatedProjects[key].places_disponibles += project.places_disponibles; + aggregatedProjects[key].places_occuper += project.places_occuper; + }); + + // Step 2: Filter out projects that don't meet the condition + const filteredProjects = Object.values(aggregatedProjects).filter(project => { + return project.nombre_personnes - project.places_occuper > 0; + }); + + // Step 3: Prepare the final result with additional fields + const result = filteredProjects.map(project => { + return { + project: project.id_project, + semaine: project.semaine, + jour: project.jour, + nbr_personnes_restant: project.nombre_personnes - project.places_occuper + }; + }); + + return result; + } + const getListOfAffectedProjects = async () => { setIsLoadingListProjects(true) try{ @@ -39,6 +87,7 @@ const AffectingZoneProject = () => { } if(isSuccess){ setListProjectsAffected(data) + setListProjectsSemiAffected(filterAndGroupProjects(data)) }else{ toggleNotification({ visible: true, @@ -65,6 +114,10 @@ const AffectingZoneProject = () => { setIsOpen(!isOpen) } + const handleOpenCompleteAffectation = () => { + setIsOpenCompleteAffectation(!isOpenCompleteAffectation) + } + const handleDeleteAffectation = async () => { try{ var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, {method: 'DELETE'}) @@ -74,7 +127,9 @@ const AffectingZoneProject = () => { message: "Affectation supprimer avec succès", type: "success" }) - setListProjectsAffected(listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id)) + const filteredProjectsAffected = listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id) + setListProjectsAffected(filteredProjectsAffected) + mutateProjectsAffectaionCheck(filteredProjectsAffected) }else if(status === 404){ toggleNotification({ visible: true, @@ -110,17 +165,63 @@ const AffectingZoneProject = () => { setSelectedAffectationToDelete(null); }; + + // function to detect if there is a project not fully affected ( outside the use effect to mutate the changement of the state) + function filterAndGroupProjects(data) { + // Step 1: Create an object to aggregate data by id_project, semaine, and jour + const aggregatedProjects = {}; + + data.forEach(project => { + const projectId = project.id_project.id; + const week = project.semaine; + const day = project.jour; + const key = `${projectId}-${week}-${day}`; + + if (!aggregatedProjects[key]) { + aggregatedProjects[key] = { + id_project: project.id_project, + semaine: week, + jour: day, + places_disponibles: 0, + places_occuper: 0, + nombre_personnes: project.nombre_personnes + }; + } + + aggregatedProjects[key].places_disponibles += project.places_disponibles; + aggregatedProjects[key].places_occuper += project.places_occuper; + }); + + // Step 2: Filter out projects that don't meet the condition + const filteredProjects = Object.values(aggregatedProjects).filter(project => { + return project.nombre_personnes - project.places_occuper > 0; + }); + + // Step 3: Prepare the final result with additional fields + const result = filteredProjects.map(project => { + return { + project: project.id_project, + semaine: project.semaine, + jour: project.jour, + nbr_personnes_restant: project.nombre_personnes - project.places_occuper + }; + }); + + return result; + } + + const mutateProjectsAffectaionCheck = (passedData) => { + console.log("mutaion triggered") + setListProjectsSemiAffected(filterAndGroupProjects(passedData)) + } + + return ( -
    -
    - {isOpen && } +
    +
    + {isOpen && } + {(isOpenCompleteAffectation) && }

    List des Projets attribuer

    -
    - Il y a des projets qui ne sont pas complètement affecter. - -
    Semaine: {element.semaine} - Jour: {element.jour}
    - + -- GitLab From b0c1f2c8b6ed5ce1aa2a26415b46a1171e938c90 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Tue, 11 Jun 2024 09:30:39 +0100 Subject: [PATCH 52/79] feature affecting zone completed with all exceptions handeled --- src/app/(dashboard)/place/page.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/place/page.jsx b/src/app/(dashboard)/place/page.jsx index 95f1221..a7d3269 100644 --- a/src/app/(dashboard)/place/page.jsx +++ b/src/app/(dashboard)/place/page.jsx @@ -83,8 +83,8 @@ const Place = () => {
    TablePlace Table-Zone-Etage Action
    - - + + {places?.map((element) => { -- GitLab From a612a40fd39868e0369a054d78e0a3c6b03bd863 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Tue, 11 Jun 2024 16:44:29 +0100 Subject: [PATCH 53/79] fixed bug affectation delete and re affect in feature/affecting-zone --- .../assign_zone_project/CompleteAffectation.jsx | 11 ++++------- src/app/(dashboard)/assign_zone_project/page.jsx | 9 +++++++-- src/app/(dashboard)/place/page.jsx | 16 ++++++++++++++-- 3 files changed, 25 insertions(+), 11 deletions(-) diff --git a/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx b/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx index 8da3fc7..d59137a 100644 --- a/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx +++ b/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx @@ -136,11 +136,8 @@ const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, f const handleMinusCollab = () =>{ setCollabsAttributed(collabsAttributed - 1) } - - console.log("collabsAttributed", collabsAttributed) - console.log("nbrCollabs", nbrCollabs) - console.log("places", places) - + console.log("projectss", affectations) + console.log("selected project", selectedProject) const handleAssignProject = async () => { @@ -149,7 +146,7 @@ const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, f id_project: selectedProject.project.id, jour: selectedProject.jour, semaine: selectedProject.semaine, - nombre_personnes: nbrCollabs, + nombre_personnes: selectedProject.nombre_personnes, places_disponibles: (collabsAttributed > places.length) ? 0 : places.length - collabsAttributed, places_occuper: (collabsAttributed > places.length) ? places.length : collabsAttributed, places: (collabsAttributed > places.length) ? places.map( (element) => element.id) : places.map( (element, index) => index < collabsAttributed && element.id).filter(id => id !== false) @@ -207,7 +204,7 @@ const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, f setLoading(false) } - + console.log(nbrCollabs) return (
    diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/assign_zone_project/page.jsx index 57a58ec..c9a2606 100644 --- a/src/app/(dashboard)/assign_zone_project/page.jsx +++ b/src/app/(dashboard)/assign_zone_project/page.jsx @@ -66,7 +66,8 @@ const AffectingZoneProject = () => { project: project.id_project, semaine: project.semaine, jour: project.jour, - nbr_personnes_restant: project.nombre_personnes - project.places_occuper + nbr_personnes_restant: project.nombre_personnes - project.places_occuper, + nombre_personnes: project.nombre_personnes }; }); @@ -203,7 +204,8 @@ const AffectingZoneProject = () => { project: project.id_project, semaine: project.semaine, jour: project.jour, - nbr_personnes_restant: project.nombre_personnes - project.places_occuper + nbr_personnes_restant: project.nombre_personnes - project.places_occuper, + nombre_personnes: project.nombre_personnes }; }); @@ -215,6 +217,9 @@ const AffectingZoneProject = () => { setListProjectsSemiAffected(filterAndGroupProjects(passedData)) } + console.log("project semi affected", listProjectsSemiAffected) + console.log("project fully associated", listProjectsAffected) + return (
    diff --git a/src/app/(dashboard)/place/page.jsx b/src/app/(dashboard)/place/page.jsx index a7d3269..074d6e8 100644 --- a/src/app/(dashboard)/place/page.jsx +++ b/src/app/(dashboard)/place/page.jsx @@ -9,12 +9,24 @@ import RowPlace from './RowPlace' import PlaceIcon from "@/static/image/svg/place.svg" - const Place = () => { const [places, setPlaces] = useState([]) const [isLoadingData, setIsLoadingData] = useState(true) const [tables, setTables] = useState([]) + + useEffect(() => { + const fetchIP = async () => { + try { + const response = await fetch("https://api.ipify.org?format=json") + const data = await response.json() + console.log(data) + } catch (error) { + console.log(error) + } + } + fetchIP() + }, []) // Fetch data from external API useEffect(() => { @@ -72,7 +84,7 @@ const Place = () => {

    List des Places

    -
    +
    -- GitLab From 3bcac3d0bd9ccead12dfe36af1c86a370f7eefdc Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 12 Jun 2024 09:15:37 +0100 Subject: [PATCH 54/79] added consultaions des reservations --- .../consultation-reservations/PlaceUI.jsx | 53 +++++ .../consultation-reservations/TableUI.jsx | 47 +++++ .../consultation-reservations/ZoneUI.jsx | 17 ++ .../consultation-reservations/page.jsx | 188 ++++++++++++++++++ 4 files changed, 305 insertions(+) create mode 100644 src/app/(dashboard)/consultation-reservations/PlaceUI.jsx create mode 100644 src/app/(dashboard)/consultation-reservations/TableUI.jsx create mode 100644 src/app/(dashboard)/consultation-reservations/ZoneUI.jsx create mode 100644 src/app/(dashboard)/consultation-reservations/page.jsx diff --git a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx new file mode 100644 index 0000000..b0b7ce2 --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx @@ -0,0 +1,53 @@ +import React, { useContext, useState } from 'react'; +import { ReservationContext } from './page'; + +const colors = [ + '#FF5733', '#50cc65', '#3357FF', '#c9ce41', '#FF33A8', + '#24c4b8', '#FF8333', '#8333FF', '#3383FF', '#83FF33', + '#FF3383', '#2ac567', '#FF33F3', '#4cb7be', '#F333FF', + '#cdd927', '#FF33A8', '#80d3cd', '#FF8333', '#8333FF', + '#3383FF', '#92d965', '#FF3383', '#4ebb78', '#FF33F3', +]; + +const getColorForProject = (projectId) => { + const index = projectId % colors.length; + return colors[index]; +}; + +const PlaceUI = ({ id }) => { + const { allPlaces, bookedPlaces } = useContext(ReservationContext); + const [showTooltip, setShowTooltip] = useState(false); + const place = allPlaces?.find((place) => place.id === id); + const bookedPlace = bookedPlaces?.find((place) => place.id_place === id); + + if (place) { + const backgroundColor = getColorForProject(place.project_id); // Assuming place object has a project_id field + return ( +
    setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)} + > +

    + {place?.project_name || ""} +

    + {!showTooltip && bookedPlace && ( +
    +

    First Name: {bookedPlace.first_name}

    +

    Last Name: {bookedPlace.last_name}

    +

    Role: {bookedPlace.role}

    +

    Presence: {bookedPlace.presence}

    +

    Created At: {new Date(bookedPlace.created_at).toLocaleString()}

    +
    + )} +
    + ); + } else { + return ( +
    + ); + } +}; + +export default PlaceUI; diff --git a/src/app/(dashboard)/consultation-reservations/TableUI.jsx b/src/app/(dashboard)/consultation-reservations/TableUI.jsx new file mode 100644 index 0000000..8c77ad5 --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/TableUI.jsx @@ -0,0 +1,47 @@ +import React from 'react' +import PlaceUI from './PlaceUI'; + +const TableUI = ({ id, numero, places }) => { + function groupConsecutive(arr) { + + arr = arr.sort((a, b) => a.id - b.id) + if (arr.length === 0) { + return []; + } + + const grouped = []; + var counter = 0 + while (counter < arr.length) { + if (counter + 1 < arr.length) { + grouped.push([arr[counter], arr[counter + 1]]) + } + else { + grouped.push([arr[counter]]) + } + counter += 2; + } + + return grouped; + } + const processedPlaces = groupConsecutive(places).reverse() + + + if (!processedPlaces || processedPlaces.length === 0) return <> + return ( +
    + {processedPlaces.map((element, index) => { + return
    +
    + +
    +
    + {(element.length > 1) &&
    + +
    } +
    + })} +
    + ) +} + +export default TableUI \ No newline at end of file diff --git a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx new file mode 100644 index 0000000..48ae7d1 --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx @@ -0,0 +1,17 @@ +import React from 'react' +import TableUI from './TableUI' + +const ZoneUI = ({ id, tables, nom }) => { + return ( +
    +

    {nom}

    +
    + {tables.map((table) => { + return + })} +
    +
    + ) +} + +export default ZoneUI \ No newline at end of file diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx new file mode 100644 index 0000000..8b71f45 --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -0,0 +1,188 @@ +'use client' + +import React, { useEffect, useState } from 'react' +import ZoneUI from './ZoneUI' +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' + +export const ReservationContext = React.createContext() + +const Reservation = () => { + const [isLoadingData, setIsLoadingData] = useState(false) + const { toggleNotification } = useNotification() + const [date, setDate] = useState({ day: null, week: null }) + const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) + const [floors, setFloors] = useState([]) + const [projectsData, setProjectsData] = useState([]) + const [currentDateData, setCurrentDateData] = useState(null) + const [datesData, setDatesData] = useState(null) + const [bookedPlaces, setBookedPlaces] = useState([]) + useEffect(() => { + const getPlan = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etage-zone-table-place/') + setIsLoadingSelectsData(false) + if (isSuccess) { + setFloors(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } catch (error) { + console.log(error) + } + } + getPlan() + }, []) + + + + useEffect(() => { + const getSyncData = async () => { + const getUserPlan = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/projects-zones-places/${date.week}/${date.day}`) + if (isSuccess) { + setProjectsData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + const getBookedPlaces = async () => { + const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${date.date}/`) + if (isSuccess) { + console.log("booked places : :", data) + setBookedPlaces(data.map((element) => ({ ...element, id_place: element.id_place.id, presence: element.presence, id_user: element.id_user.id, created_at: element.created_at, first_name: element.id_user.first_name, last_name: element.id_user.last_name, role: element.id_user.role.name }))) + } + else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + setIsLoadingData(true) + await Promise.all([getUserPlan(), getBookedPlaces()]) + setIsLoadingData(false) + } + if (date.week && date.day) getSyncData() + else { + setProjectsData([]) + setBookedPlaces([]) + } + + }, [date.week, date.day]) + + useEffect(() => { + const getCurrentDateData = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/current-date/'); + if (isSuccess) { + setCurrentDateData(data) + } else { + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const YearCalendar = async () => { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/dates/'); + if (isSuccess) { + console.log("dates data", data) + setDatesData(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Failed to fetch current date", + type: "error" + }) + } + } + const getSyncData = async () => { + await Promise.all([getCurrentDateData(), YearCalendar()]) + } + getSyncData() + }, []) + + const handleChangeDate = (event) => { + if (event.target.value) { + const dateSelected = JSON.parse(event.target.value); + console.log("weekMonthly", dateSelected.weekMonthly); + console.log("day", dateSelected.day); + setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) + } + else { + setDate({ day: null, week: null }) + } + } + + + const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] + const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] + const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] + console.log("all places", allPlaces) + if (isLoadingSelectsData) + return
    + +
    + + const currentMonth = new Date().getMonth() + 1 + // filter dates from now to 2 weeks later + const filteredDatesData = datesData?.filter((element) => { + const date = new Date(element.date) + const month = date.getMonth() + 1 + return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + }) + return ( +
    +
    + +
    + {(!isLoadingData) + ? <> +
    + + {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return
    +

    Etage {floor.numero}

    + {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
    + })} + {floors.filter((element) => concernedFloors.includes(element.id)).length === 0 + &&
    +

    Un exemple d'un message à l'utilisateur

    +
    } +
    +
    + + :
    + +
    + } +
    + ) +} + +export default Reservation \ No newline at end of file -- GitLab From d702bfe1676d2ac52698f5bd6e2e67861d6445de Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Wed, 12 Jun 2024 12:06:50 +0100 Subject: [PATCH 55/79] feature consultation with etage filter --- .../consultation-reservations/page.jsx | 70 ++++++++++++++++--- .../(dashboard)/etage/AddEtageComponent.jsx | 13 ++-- src/app/(dashboard)/etage/page.jsx | 8 ++- 3 files changed, 71 insertions(+), 20 deletions(-) diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx index 8b71f45..5e0b8ae 100644 --- a/src/app/(dashboard)/consultation-reservations/page.jsx +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -18,6 +18,9 @@ const Reservation = () => { const [currentDateData, setCurrentDateData] = useState(null) const [datesData, setDatesData] = useState(null) const [bookedPlaces, setBookedPlaces] = useState([]) + const [ etages, setEtages ] = useState([]) + const [ isLoadingEtages, setIsLoadingEtages ] = useState(true) + const [ selectedEtage, setSelectedEtage ] = useState(null) useEffect(() => { const getPlan = async () => { try { @@ -40,6 +43,23 @@ const Reservation = () => { getPlan() }, []) + useEffect(() => { + const getAllEtages = async () => { + try { + const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) + setIsLoadingEtages(false) + if (isSuccess) { + setEtages(data) + } else { + setEtages([]) + } + } catch (error) { + setIsLoadingEtages(false) + console.log(error) + } + } + getAllEtages() + }, []) useEffect(() => { @@ -123,17 +143,24 @@ const Reservation = () => { console.log("weekMonthly", dateSelected.weekMonthly); console.log("day", dateSelected.day); setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) + setSelectedEtage(null) } else { setDate({ day: null, week: null }) + setSelectedEtage(null) } } + const handleChangeEtage = (event) =>{ + const etage_id = event.target.value + if(selectedEtage !== etage_id){ + setSelectedEtage(etage_id) + } + } const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] - console.log("all places", allPlaces) if (isLoadingSelectsData) return
    @@ -146,6 +173,9 @@ const Reservation = () => { const month = date.getMonth() + 1 return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) }) + + console.log(floors) + console.log(selectedEtage) return (
    @@ -157,19 +187,41 @@ const Reservation = () => { ))} +
    {(!isLoadingData) ? <>
    - {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { - return
    -

    Etage {floor.numero}

    - {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { - return - })} -
    - })} + { + (selectedEtage) ? + floors.filter((element) => + {if (concernedFloors.includes(element.id) && element.id == selectedEtage) {return element}} + ).map((floor) => { + return
    +

    Etage {floor.numero}

    + {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
    + }) + : + floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return
    +

    Etage {floor.numero}

    + {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + })} +
    + }) + } {floors.filter((element) => concernedFloors.includes(element.id)).length === 0 &&

    Un exemple d'un message à l'utilisateur

    diff --git a/src/app/(dashboard)/etage/AddEtageComponent.jsx b/src/app/(dashboard)/etage/AddEtageComponent.jsx index 42cfdca..aaf361f 100644 --- a/src/app/(dashboard)/etage/AddEtageComponent.jsx +++ b/src/app/(dashboard)/etage/AddEtageComponent.jsx @@ -56,16 +56,13 @@ const AddEtageComponent = ({ etagesState }) => { } return ( -
    -
    - Nouveau étage: -
    - -
    - +
    + Nouveau étage: +
    +
    +
    - ) } diff --git a/src/app/(dashboard)/etage/page.jsx b/src/app/(dashboard)/etage/page.jsx index ca6ba13..d969987 100644 --- a/src/app/(dashboard)/etage/page.jsx +++ b/src/app/(dashboard)/etage/page.jsx @@ -80,8 +80,8 @@ const Etage = () => { }; return ( -
    -
    +
    +

    Liste des étages

    @@ -108,7 +108,9 @@ const Etage = () => { }
    - +
    + +
    Date: Wed, 12 Jun 2024 12:18:57 +0100 Subject: [PATCH 56/79] finished adding consultation reservations --- .../consultation-reservations/PlaceUI.jsx | 12 +++++++---- .../consultation-reservations/TableUI.jsx | 21 ++++++++++++------- src/app/globals.css | 8 +++++++ src/app/ui/LogoutButton.js | 18 ++++++++-------- 4 files changed, 38 insertions(+), 21 deletions(-) diff --git a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx index b0b7ce2..7e294fa 100644 --- a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx @@ -14,7 +14,7 @@ const getColorForProject = (projectId) => { return colors[index]; }; -const PlaceUI = ({ id }) => { +const PlaceUI = ({ id, isTop }) => { const { allPlaces, bookedPlaces } = useContext(ReservationContext); const [showTooltip, setShowTooltip] = useState(false); const place = allPlaces?.find((place) => place.id === id); @@ -24,7 +24,8 @@ const PlaceUI = ({ id }) => { const backgroundColor = getColorForProject(place.project_id); // Assuming place object has a project_id field return (
    setShowTooltip(true)} onMouseLeave={() => setShowTooltip(false)} @@ -32,8 +33,11 @@ const PlaceUI = ({ id }) => {

    {place?.project_name || ""}

    - {!showTooltip && bookedPlace && ( -
    + {bookedPlace && ( +
    + )} + {showTooltip && bookedPlace && ( +

    First Name: {bookedPlace.first_name}

    Last Name: {bookedPlace.last_name}

    Role: {bookedPlace.role}

    diff --git a/src/app/(dashboard)/consultation-reservations/TableUI.jsx b/src/app/(dashboard)/consultation-reservations/TableUI.jsx index 8c77ad5..ce702a2 100644 --- a/src/app/(dashboard)/consultation-reservations/TableUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/TableUI.jsx @@ -1,7 +1,7 @@ import React from 'react' import PlaceUI from './PlaceUI'; -const TableUI = ({ id, numero, places }) => { +const TableUI = ({id, numero, places}) => { function groupConsecutive(arr) { arr = arr.sort((a, b) => a.id - b.id) @@ -14,8 +14,7 @@ const TableUI = ({ id, numero, places }) => { while (counter < arr.length) { if (counter + 1 < arr.length) { grouped.push([arr[counter], arr[counter + 1]]) - } - else { + } else { grouped.push([arr[counter]]) } counter += 2; @@ -23,6 +22,7 @@ const TableUI = ({ id, numero, places }) => { return grouped; } + const processedPlaces = groupConsecutive(places).reverse() @@ -31,12 +31,17 @@ const TableUI = ({ id, numero, places }) => {
    {processedPlaces.map((element, index) => { return
    -
    - +
    +
    -
    - {(element.length > 1) &&
    - +
    + {(element.length > 1) &&
    +
    }
    })} diff --git a/src/app/globals.css b/src/app/globals.css index 20a7710..771e450 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -201,4 +201,12 @@ width: 0; transition-duration: 300ms } +} + +.tooltip { + @apply invisible absolute; +} + +.has-tooltip:hover .tooltip { + @apply visible z-50; } \ No newline at end of file diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.js index 1ffc0e4..5dbae38 100644 --- a/src/app/ui/LogoutButton.js +++ b/src/app/ui/LogoutButton.js @@ -3,30 +3,30 @@ import Cookies from 'js-cookie'; import fetchRequest from "@/app/lib/fetchRequest"; import LogoutIcon from "@/static/image/svg/logout.svg" -const LogoutButton = ({ isButton = false }) => { + +const LogoutButton = ({isButton = false}) => { const logout = async () => { const response = await fetchRequest(`/logout`, { method: 'GET' }); console.log(response); - if (response.isSuccess) { - console.log('logout successful'); - Cookies.remove('session'); - window.location.href = '/'; - } + console.log('logout successful'); + Cookies.remove('session'); + window.location.href = '/auth/login'; }; if (isButton) return
    -
    -

    +

    +

    Se déconnecter

    return ( ); }; -- GitLab From fac22b5d1ae80bd9c311584dad2a76b4271463fb Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Wed, 12 Jun 2024 13:11:28 +0100 Subject: [PATCH 57/79] add presence with geolocation conditions --- src/app/(dashboard)/reservation/PlaceUI.jsx | 24 +-- .../reservation/PresenceButton.jsx | 105 +++++++++++++ src/app/(dashboard)/reservation/page.jsx | 140 ++++++++++++++++-- src/app/(dashboard)/role/RoleTableRows.jsx | 1 + 4 files changed, 237 insertions(+), 33 deletions(-) create mode 100644 src/app/(dashboard)/reservation/PresenceButton.jsx diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx index f347155..976bc4b 100644 --- a/src/app/(dashboard)/reservation/PlaceUI.jsx +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -1,35 +1,21 @@ "use client" -import React, { useContext, useMemo, useState } from 'react' +import React, { useContext, useState } from 'react' import { ReservationContext } from './page' import fetchRequest from '@/app/lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' -import Cookies from 'js-cookie'; -import { decrypt } from '@/app/lib/session'; import ConfirmationModal from '@/app/ui/ConfirmationModal' const PlaceUI = ({ id }) => { - const { allPlaces, selectedDate, bookedPlaces, setBookedPlaces } = useContext(ReservationContext) + const { allPlaces, selectedDate, bookedPlaces, setBookedPlaces, authenticatedUserData, hasPlace } = useContext(ReservationContext) const { toggleNotification } = useNotification() const [isOpenBooking, setIsOpenBooking] = useState(false) const [isOpenCanceling, setIsOpenCanceling] = useState(false) - const [authenticatedUserData, setAuthenticatedUserData] = useState(null) - const cookie = Cookies.get("session") - const getUserData = async () => { - try { - if (cookie) { - const data = await decrypt(cookie) - setAuthenticatedUserData(data) - } - } catch (error) { - console.log(error) - } - } - useMemo(getUserData, [cookie]) const place = allPlaces?.find((place) => place.id === id) const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) - const hasPlace = bookedPlaces?.some((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) + console.log(hasPlace); const handleBooking = (event) => { event.stopPropagation() + if (hasPlace && hasPlace.presence) return; if (hasPlace) { toggleNotification({ visible: true, @@ -121,7 +107,7 @@ const PlaceUI = ({ id }) => { if (place) if (bookedPlace) if (bookedPlace.id_user === authenticatedUserData.sessionData?.user_id) - return
    + return
    { }} className={`${bookedPlace.presence ? "bg-green-500/80" : "bg-amber-500/80 cursor-pointer"} absolute items-center flex justify-center h-full px-[2px] w-full text-sm`}>

    {place.project_name || ""}

    handleCancelingConfirmation(bookedPlace.id)} onClose={closeCancelingPopup} />
    diff --git a/src/app/(dashboard)/reservation/PresenceButton.jsx b/src/app/(dashboard)/reservation/PresenceButton.jsx new file mode 100644 index 0000000..ae58677 --- /dev/null +++ b/src/app/(dashboard)/reservation/PresenceButton.jsx @@ -0,0 +1,105 @@ +"use client" +import fetchRequest from '@/app/lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import { useNotification } from '@/context/NotificationContext' +import React, { useContext, useEffect, useState } from 'react' +import { ReservationContext } from './page' + +const PresenceButton = ({ processUserLocation, geolocationError, isInside }) => { + const { toggleNotification } = useNotification() + const [isLoading, setIsLoading] = useState(false) + const { setBookedPlaces, hasPlace } = useContext(ReservationContext) + const handlePresenceSave = async () => { + if (hasPlace) { + setIsLoading(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/reservations/${hasPlace.id}/`, { + method: "PATCH", + body: JSON.stringify({ presence: true }) + }) + setIsLoading(false) + if (isSuccess) { + toggleNotification({ + visible: true, + message: "Votre présence a été enregistrée avec succès.", + type: "success" + }) + console.log(data) + setBookedPlaces((elements) => elements.map((element) => element.id === data.data?.id ? data.data : element)) + } + else { + console.log(errors) + if (status === 404) + toggleNotification({ + visible: true, + message: "Aucune réservation existe dans cette date.", + type: "warning" + }) + else + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + } + useEffect(() => { + if ("User denied the request for Geolocation." === geolocationError) { + toggleNotification({ + visible: true, + message: "Vous devez activer la géolocalisation pour enregistrer la présence", + type: "warning" + }) + } + else if (geolocationError === "The request to get user location timed out.") { + toggleNotification({ + visible: true, + message: "Vous devez activer la géolocalisation pour enregistrer la présence", + type: "warning" + }) + } + else if (geolocationError === "Location information is unavailable.") { + toggleNotification({ + visible: true, + message: "Erreur de géolocation", + type: "error" + }) + } + }, [geolocationError]) + + + if ("User denied the request for Geolocation." === geolocationError) { + return
    +

    Autorisez la géolocalisation pour enregistrer votre présence

    + +
    + } + else if (geolocationError === "The request to get user location timed out.") { + return
    +

    Autorisez la géolocalisation pour enregistrer votre présence

    + +
    + } + else if (geolocationError === "Location information is unavailable.") { + return + } + else if (isInside) + return + else return
    +

    Vous n'êtes pas géolocalisé à l'intérieur de Teamwill.

    + +
    +} + + +export default PresenceButton \ No newline at end of file diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index 37921d1..e899e40 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -1,10 +1,13 @@ 'use client' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useMemo } from 'react' import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' import { useNotification } from '@/context/NotificationContext' +import PresenceButton from './PresenceButton' +import Cookies from 'js-cookie'; +import { decrypt } from '@/app/lib/session'; export const ReservationContext = React.createContext() @@ -18,6 +21,115 @@ const Reservation = () => { const [currentDateData, setCurrentDateData] = useState(null) const [datesData, setDatesData] = useState(null) const [bookedPlaces, setBookedPlaces] = useState([]) + const [isInside, setIsInside] = useState(null) + const [geolocationError, setGeolocationError] = useState(null) + + const [authenticatedUserData, setAuthenticatedUserData] = useState(null) + + const cookie = Cookies.get("session") + const getUserData = async () => { + try { + if (cookie) { + const data = await decrypt(cookie) + setAuthenticatedUserData(data) + } + } catch (error) { + console.log(error) + } + } + useMemo(getUserData, [cookie]) + + const processUserLocation = () => { + const companyLatitude = 36.8402141; // Example: Company's latitude + const companyLongitude = 10.2432573; // Example: Company's longitude + const allowedRadius = 400; // Example: Radius in meters + + + function getDistanceFromLatLonInMeters(lat1, lon1, lat2, lon2) { + const R = 6371000; // Radius of the Earth in meters + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; // Distance in meters + return distance; + } + + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + function (position) { + const userLatitude = position.coords.latitude; + const userLongitude = position.coords.longitude; + + const distance = getDistanceFromLatLonInMeters( + userLatitude, + userLongitude, + companyLatitude, + companyLongitude + ); + + if (distance <= allowedRadius) { + setIsInside(true) + } else { + setIsInside(false) + } + setGeolocationError(null) + }, + function (error) { + switch (error.code) { + case error.PERMISSION_DENIED: + setGeolocationError("User denied the request for Geolocation."); + break; + case error.POSITION_UNAVAILABLE: + setGeolocationError("Location information is unavailable."); + break; + case error.TIMEOUT: + setGeolocationError("The request to get user location timed out."); + break; + case error.UNKNOWN_ERROR: + setGeolocationError("An unknown error occurred."); + break; + } + } + ); + } else { + toggleNotification({ + message: "La géolocalisation n'est pas supporté par votre navigateur.", + type: "warning", + visible: true + }) + } + } + + + useEffect(() => { + const handlePermissionChange = (permissionStatus) => { + if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') { + processUserLocation(); + } else { + setGeolocationError("User denied the request for Geolocation."); + } + }; + + navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { + handlePermissionChange(permissionStatus); + + permissionStatus.onchange = () => { + handlePermissionChange(permissionStatus); + }; + }).catch((error) => { + console.error('Failed to query geolocation permission:', error); + }); + return () => { + navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { + permissionStatus.onchange = null; + }); + }; + }, []) + useEffect(() => { const getPlan = async () => { try { @@ -129,11 +241,12 @@ const Reservation = () => { } } - + const currentDate = new Date().toJSON()?.split("T")[0] || null const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] - console.log("all places", allPlaces) + const hasPlace = bookedPlaces?.find((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) + if (isLoadingSelectsData) return
    @@ -181,14 +294,13 @@ const Reservation = () => {
    {(!isLoadingData) - ? <> - {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0) &&
    - -
    } -
    - + ? + + <> + {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0 && hasPlace && currentDate && currentDate === date?.date) &&
    + +
    } +
    {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { return

    Etage {floor.numero}

    @@ -201,9 +313,9 @@ const Reservation = () => { &&

    Un exemple d'un message à l'utilisateur

    } - -
    - + +
    +
    :
    diff --git a/src/app/(dashboard)/role/RoleTableRows.jsx b/src/app/(dashboard)/role/RoleTableRows.jsx index 89fcaa4..ccdcad9 100644 --- a/src/app/(dashboard)/role/RoleTableRows.jsx +++ b/src/app/(dashboard)/role/RoleTableRows.jsx @@ -57,6 +57,7 @@ const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { {privileges?.map((element, index) => { return
    {element.name}
    })} + {!privileges || privileges.length == 0 &&

    -

    }
    TableTable-Zone-EtagePlacePlace-Zone-Etage Action
    -- GitLab From 802176bcfeb322b1dc694dade70518f1b75d1991 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 12 Jun 2024 14:10:15 +0100 Subject: [PATCH 58/79] finished adding consultation reservations --- .env.local | 1 + .gitignore | 1 + .../consultation-reservations/PlaceUI.jsx | 2 +- .../consultation-reservations/page.jsx | 2 +- src/app/ui/SideBar.jsx | 29 +++++++++++-------- 5 files changed, 21 insertions(+), 14 deletions(-) create mode 100644 .env.local diff --git a/.env.local b/.env.local new file mode 100644 index 0000000..8b13789 --- /dev/null +++ b/.env.local @@ -0,0 +1 @@ + diff --git a/.gitignore b/.gitignore index 10e85c1..fe4d87f 100644 --- a/.gitignore +++ b/.gitignore @@ -27,6 +27,7 @@ yarn-error.log* # local env files .env +.env.local # vercel .vercel diff --git a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx index 7e294fa..f60e74f 100644 --- a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx @@ -49,7 +49,7 @@ const PlaceUI = ({ id, isTop }) => { ); } else { return ( -
    +
    ); } }; diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx index 5e0b8ae..4edaac4 100644 --- a/src/app/(dashboard)/consultation-reservations/page.jsx +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -180,7 +180,7 @@ const Reservation = () => {
    - {etages?.map((etage, index) => ( ))} +
    {(!isLoadingData) ? <> @@ -207,9 +239,17 @@ const Reservation = () => { ).map((floor) => { return

    Etage {floor.numero}

    - {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { - return - })} + {(selectedZone)? + floor.zones.filter((element) => + {if (concernedZones.includes(element.id) && element.id == selectedZone) {return element}} + ).map((zone, index) => { + return + }) + : + floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { + return + }) + }
    }) : diff --git a/src/app/(dashboard)/etage/page.jsx b/src/app/(dashboard)/etage/page.jsx index d969987..9d69312 100644 --- a/src/app/(dashboard)/etage/page.jsx +++ b/src/app/(dashboard)/etage/page.jsx @@ -81,7 +81,7 @@ const Etage = () => { return (
    -
    +

    Liste des étages

    -- GitLab From 36b4fef6e43898e8054e34c6d64047c8d2e16081 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 12 Jun 2024 14:30:25 +0100 Subject: [PATCH 60/79] added consultaions des reservations-finished --- src/app/(dashboard)/consultation-reservations/page.jsx | 2 ++ src/app/auth/login/page.jsx | 2 +- 2 files changed, 3 insertions(+), 1 deletion(-) diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx index 5e1ed3e..dad6282 100644 --- a/src/app/(dashboard)/consultation-reservations/page.jsx +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -213,6 +213,7 @@ const Reservation = () => {
    }} diff --git a/src/app/(dashboard)/role/CreateRoleForm.jsx b/src/app/(dashboard)/role/CreateRoleForm.jsx index 223840e..3ba4a85 100644 --- a/src/app/(dashboard)/role/CreateRoleForm.jsx +++ b/src/app/(dashboard)/role/CreateRoleForm.jsx @@ -135,7 +135,7 @@ const CreateRoleForm = ({ appendRole, setIsOpen }) => { {!isLoading && "Créer" || }
    - :
    } + :
    }
    ) diff --git a/src/app/(dashboard)/role/UpdateRoleForm.jsx b/src/app/(dashboard)/role/UpdateRoleForm.jsx index 685b2ed..4b35ead 100644 --- a/src/app/(dashboard)/role/UpdateRoleForm.jsx +++ b/src/app/(dashboard)/role/UpdateRoleForm.jsx @@ -134,7 +134,7 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv {!loadingStatus && "Modifier" || }
    - :
    } + :
    } ) diff --git a/src/app/(dashboard)/user/CreateUserForm.jsx b/src/app/(dashboard)/user/CreateUserForm.jsx index 90b1799..bb631dd 100644 --- a/src/app/(dashboard)/user/CreateUserForm.jsx +++ b/src/app/(dashboard)/user/CreateUserForm.jsx @@ -4,8 +4,8 @@ import fetchRequest from '../../lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" import { EMAIL_REGEX } from '../../lib/constants' - - +import { removeWhiteSpace } from '@/app/lib/StringHelper' +import Select from "react-select"; function generateRandomPassword() { @@ -71,13 +71,19 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { getRoles() getProjects() }, []) + + const [errors, setErrors] = useState({ first_name: "", last_name: "", email: "", role: "" }) + const [userData, setUserData] = useState({ email: "", password: generateRandomPassword(), first_name: "", last_name: "" }) const handleFieldChange = (event) => { setUserData({ ...userData, [event.target.name]: event.target.value }) setErrors({ ...errors, [event.target.name]: "" }) } - const [errors, setErrors] = useState({ first_name: "", last_name: "", email: "", role: "" }) - const [userData, setUserData] = useState({ email: "", password: generateRandomPassword(), first_name: "", last_name: "" }) - + useEffect(() => { + setUserData({ + ...userData, email: (userData.last_name || userData.first_name) ? removeWhiteSpace(userData.first_name + "." + userData.last_name + "@teamwillgroup.com") : "" + }) + setErrors({ ...errors, email: "" }) + }, [userData.first_name, userData.last_name]) const isValidFields = () => { const localErrors = { first_name: "", last_name: "", email: "", role: "" } if (userData.first_name === "") localErrors.first_name = "Le prénom doit être spécifier." @@ -147,10 +153,8 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { setSelectedRole(event.target.value) else setSelectedRole(null) } - const handleProjectChange = (event) => { - if (event.target.value) - setSelectedProject([event.target.value]) - else setSelectedProject([]) + const handleProjectChange = (selectedOptions) => { + setSelectedProject(selectedOptions.map((element) => element.value)) } return (
    @@ -171,19 +175,72 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
    - - + +

    {errors.email}

    - + +
    +
    +
    + +
    +
    + {projects.length !== 0 ? +
    + @@ -201,27 +258,6 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
    }
    -
    -
    - -
    -
    - {projects.length !== 0 ? -
    - -
    - :
    -

    Pas encore des projets

    -
    } -
    -
    return ( ); }; diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index ea86517..6c3d331 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -65,85 +65,83 @@ const SideBar = () => { link: "/assign_zone_project" , icon: }, - { + { label: "Consulter les réservations", link: "/consultation-reservations" , icon: }, ] return ( -
    - +
      + {AdminLinks.map((element, index) => { + return + })} +
    + + +
  • + +
  • +
    ); } diff --git a/src/app/ui/SideBarLink.jsx b/src/app/ui/SideBarLink.jsx index 33595a7..ca8d176 100644 --- a/src/app/ui/SideBarLink.jsx +++ b/src/app/ui/SideBarLink.jsx @@ -4,17 +4,26 @@ import { usePathname } from 'next/navigation' import React from 'react' const SideBarLink = ({ link, label }) => { const pathname = usePathname() - if (pathname && pathname.includes(link)) + const hideSideBar = () => { + const target = document.querySelector(".burger") + const sideBar = document.getElementById("sideBar") + if (target && sideBar) { + target.classList.replace("active", "notActive") + sideBar.classList.replace("active", "notActive") + } + console.log(target, sideBar); + } + if (pathname && pathname === link) return ( -
    +
    - {label} + {label} -
    +
    ) - return
    + return
    - {label} + {label}
    } -- GitLab From 56043bce5040c199be869394f26d22779cdb8849 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 18 Jun 2024 14:34:05 +0100 Subject: [PATCH 64/79] reservation UI dropdown autoselected in the current date --- src/app/(dashboard)/reservation/ZoneUI.jsx | 2 +- src/app/(dashboard)/reservation/page.jsx | 23 +++++++++++++--------- 2 files changed, 15 insertions(+), 10 deletions(-) diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx index 48ae7d1..4228c50 100644 --- a/src/app/(dashboard)/reservation/ZoneUI.jsx +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -4,7 +4,7 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return (
    -

    {nom}

    +

    Zone {nom}

    {tables.map((table) => { return diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index e899e40..e4ba0d5 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -1,6 +1,6 @@ 'use client' -import React, { useEffect, useState, useMemo } from 'react' +import React, { useEffect, useState, useMemo, useRef } from 'react' import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' @@ -232,8 +232,7 @@ const Reservation = () => { const handleChangeDate = (event) => { if (event.target.value) { const dateSelected = JSON.parse(event.target.value); - console.log("weekMonthly", dateSelected.weekMonthly); - console.log("day", dateSelected.day); + setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) } else { @@ -247,6 +246,13 @@ const Reservation = () => { const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] const hasPlace = bookedPlaces?.find((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) + const dateRef = useRef(null) + useEffect(() => { + if (dateRef.current && currentDateData?.date) { + dateRef.current.value = JSON.stringify(currentDateData) + setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) + } + }, [dateRef.current, currentDateData?.date]) if (isLoadingSelectsData) return
    @@ -262,14 +268,13 @@ const Reservation = () => { return (
    - {filteredDatesData.map((date, index) => ( ))} - + }
    @@ -311,11 +316,11 @@ const Reservation = () => { })} {floors.filter((element) => concernedFloors.includes(element.id)).length === 0 &&
    -

    Un exemple d'un message à l'utilisateur

    +

    Vous êtes en télétravail aujourd'hui.

    } -
    - + + :
    -- GitLab From 78d088c36953bbb2f430a4dd2e8c44770d4f26d3 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 18 Jun 2024 15:03:09 +0100 Subject: [PATCH 65/79] fix reservation UI --- src/app/(dashboard)/reservation/TableUI.jsx | 2 +- src/app/(dashboard)/reservation/ZoneUI.jsx | 6 +++--- src/app/(dashboard)/reservation/page.jsx | 2 +- 3 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/app/(dashboard)/reservation/TableUI.jsx b/src/app/(dashboard)/reservation/TableUI.jsx index 8c77ad5..bf4e5e6 100644 --- a/src/app/(dashboard)/reservation/TableUI.jsx +++ b/src/app/(dashboard)/reservation/TableUI.jsx @@ -1,7 +1,7 @@ import React from 'react' import PlaceUI from './PlaceUI'; -const TableUI = ({ id, numero, places }) => { +const TableUI = ({ places }) => { function groupConsecutive(arr) { arr = arr.sort((a, b) => a.id - b.id) diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx index 4228c50..42538fd 100644 --- a/src/app/(dashboard)/reservation/ZoneUI.jsx +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -3,9 +3,9 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return ( -
    -

    Zone {nom}

    -
    +
    +

    Zone {nom}

    +
    {tables.map((table) => { return })} diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index e4ba0d5..2ab35c3 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -42,7 +42,7 @@ const Reservation = () => { const processUserLocation = () => { const companyLatitude = 36.8402141; // Example: Company's latitude const companyLongitude = 10.2432573; // Example: Company's longitude - const allowedRadius = 400; // Example: Radius in meters + const allowedRadius = 300; // Example: Radius in meters function getDistanceFromLatLonInMeters(lat1, lon1, lat2, lon2) { -- GitLab From fac775df8cc72d9a8b42be27fbad4f521f972a4b Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 18 Jun 2024 15:12:55 +0100 Subject: [PATCH 66/79] fix presence button conditions --- src/app/(dashboard)/reservation/PlaceUI.jsx | 1 - src/app/(dashboard)/reservation/page.jsx | 4 ++-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx index 976bc4b..43d2f8f 100644 --- a/src/app/(dashboard)/reservation/PlaceUI.jsx +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -12,7 +12,6 @@ const PlaceUI = ({ id }) => { const place = allPlaces?.find((place) => place.id === id) const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) - console.log(hasPlace); const handleBooking = (event) => { event.stopPropagation() if (hasPlace && hasPlace.presence) return; diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index 2ab35c3..a10267f 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -271,7 +271,7 @@ const Reservation = () => { {(filteredDatesData && filteredDatesData.length) && } @@ -302,7 +302,7 @@ const Reservation = () => { ? <> - {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0 && hasPlace && currentDate && currentDate === date?.date) &&
    + {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0 && hasPlace && !hasPlace.presence && currentDate && currentDate === date?.date) &&
    }
    -- GitLab From cefe157de7520ebfaaa4a42406805a3de2f924df Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 18 Jun 2024 15:46:44 +0100 Subject: [PATCH 67/79] add loader ,optimize reload and fix login submit function --- src/app/auth/login/page.jsx | 18 +++++++++++++----- 1 file changed, 13 insertions(+), 5 deletions(-) diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index 1bacbee..2bdb9a0 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -4,11 +4,15 @@ import { useState } from 'react'; import Link from 'next/link'; import Cookies from 'js-cookie'; import { createSession } from "@/app/lib/session"; +import { useRouter } from 'next/navigation' +import Loader from '@/components/Loader/Loader'; const LoginPage = () => { const [username, setUsername] = useState(''); const [password, setPassword] = useState(''); const [messages, setMessages] = useState(''); + const [isLoading, setIsLoading] = useState(false) + const router = useRouter(); const handleSubmit = (event) => { event.preventDefault(); @@ -21,6 +25,7 @@ const LoginPage = () => { const login = async (event) => { event.preventDefault(); try { + setIsLoading(true) const response = await fetch(`${process.env.NEXT_PUBLIC_API_URL}/login/`, { method: 'POST', headers: { @@ -29,11 +34,13 @@ const LoginPage = () => { body: JSON.stringify({ username, password }), }); const data = await response.json(); - if (data.non_field_errors) { - setMessages(data.non_field_errors[0]); + console.log(data) + setIsLoading(false) + if (data.detail?.non_field_errors) { + setMessages("Email ou mot de passe incorrect. Veuillez réessayer."); } else { await createSession(data); - window.location.href = '/reservation'; + router.push('/'); } } catch (error) { setMessages('An error occurred'); @@ -83,10 +90,11 @@ const LoginPage = () => {

    )}
    -- GitLab From 989db0a8d406e5f9c42393f684a370f6e2e8c6c5 Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Tue, 18 Jun 2024 16:10:32 +0100 Subject: [PATCH 68/79] fix loader in reservation UI --- src/app/(dashboard)/reservation/page.jsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index a10267f..e77c657 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -12,7 +12,7 @@ import { decrypt } from '@/app/lib/session'; export const ReservationContext = React.createContext() const Reservation = () => { - const [isLoadingData, setIsLoadingData] = useState(false) + const [isLoadingData, setIsLoadingData] = useState(true) const { toggleNotification } = useNotification() const [date, setDate] = useState({ day: null, week: null }) const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) @@ -314,7 +314,7 @@ const Reservation = () => { })}
    })} - {floors.filter((element) => concernedFloors.includes(element.id)).length === 0 + {!isLoadingData && floors.filter((element) => concernedFloors.includes(element.id)).length === 0 &&

    Vous êtes en télétravail aujourd'hui.

    } -- GitLab From 9382d0070c30c11b31b30929d0d7f8a48219323a Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Wed, 19 Jun 2024 09:51:56 +0100 Subject: [PATCH 69/79] version fixing pages place/table/zone/etage --- src/app/(dashboard)/place/CreateNewPlace.jsx | 108 ------- src/app/(dashboard)/place/RowPlace.jsx | 13 +- src/app/(dashboard)/place/page.jsx | 293 +++++++++++++++++-- 3 files changed, 273 insertions(+), 141 deletions(-) delete mode 100644 src/app/(dashboard)/place/CreateNewPlace.jsx diff --git a/src/app/(dashboard)/place/CreateNewPlace.jsx b/src/app/(dashboard)/place/CreateNewPlace.jsx deleted file mode 100644 index 0d9a017..0000000 --- a/src/app/(dashboard)/place/CreateNewPlace.jsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client" -import React, { useState, useEffect, useRef } from 'react' -import fetchRequest from '../../lib/fetchRequest' -import Loader from '@/components/Loader/Loader' -import { Island_Moments } from 'next/font/google' -import { useNotification } from '@/context/NotificationContext' - - -const CreateNewPlace = ({ placesState, tables }) => { - const [error, setError] = useState(null) - const [isLoadingAction, setIsLoadingAction] = useState(false) - const [numPlace, setNumPlace] = useState(null) - const [selectedTable, setSelectedTable] = useState(null) - - const inputRef = useRef(null) - const selectRef = useRef(null) - - const { toggleNotification } = useNotification() - - - const handleSubmit = async (event) => { - event.preventDefault() - setIsLoadingAction(true) - const { data, errors, isSuccess } = await fetchRequest("/zoaning/places/", { - method: "POST", - body: JSON.stringify({ numero: numPlace, id_table: selectedTable }) - }) - if (isSuccess) { - setIsLoadingAction(false) - placesState((prevPlaceState) => [...prevPlaceState, { ...data, id_table: tables.find(table => table.id === data.id_table) }]); - inputRef.current.value = "" - selectRef.current.value = "" - setNumPlace(null) - setSelectedTable(null) - toggleNotification({ - visible: true, - message: "La table a été créer avec succès.", - type: "success" - }) - } else { - setIsLoadingAction(false) - if (errors.type === "ValidationError") { - if (errors.detail.non_field_errors) { - toggleNotification({ - type: "warning", - message: "Le numéro de la table saisie déjà existe.", - visible: true, - }) - } - } else { - toggleNotification({ - type: "error", - message: "Une erreur s'est produite lors de la création de la table.", - visible: true, - }) - } - console.log(errors) - } - } - - // Handle the name of zone change - const handleChangeZone = (event) => { - setError("") - setNumPlace(event.target.value) - } - - const handleChangeEtage = (event) => { - setError("") - setSelectedTable(event.target.value) - } - - console.log(selectedTable) - console.log(numPlace) - - - - return ( -
    -

    Ajout d'une place

    -
    -
    - - -
    -
    - - -
    -
    -

    -
    - -
    -
    - ) -} - - -export default CreateNewPlace \ No newline at end of file diff --git a/src/app/(dashboard)/place/RowPlace.jsx b/src/app/(dashboard)/place/RowPlace.jsx index 3d22e48..32ffc3f 100644 --- a/src/app/(dashboard)/place/RowPlace.jsx +++ b/src/app/(dashboard)/place/RowPlace.jsx @@ -12,7 +12,8 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; -const RowPlace = ({ id, numero, table, placesState, tables }) => { +const RowPlace = ({ id, numero, table, placesState, tables, filteredPlacesState }) => { + console.log(table) //states const [isUpdating, setIsUpdating] = useState(false) const [numPlace, setNumPlace] = useState(numero) @@ -41,7 +42,6 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { }) setLoadingStatus(false) if (isSuccess) { - console.log(data) if (data.message === "NO_CHANGES") { toggleNotification({ visible: true, @@ -51,8 +51,8 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { setIsUpdating(false) return } - console.log(data.data) placesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) + filteredPlacesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) setIsUpdating(false) toggleNotification({ visible: true, @@ -96,13 +96,14 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { } console.log(errors) } - + } const handleDelete = async () => { const { isSuccess, errors, status } = await fetchRequest(`/zoaning/places/${id}/`, { method: "DELETE" }) if (isSuccess) { placesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) + filteredPlacesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) toggleNotification({ visible: true, message: "La place a été supprimée avec succès", @@ -179,12 +180,12 @@ const RowPlace = ({ id, numero, table, placesState, tables }) => { setNumPlace(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - + + + {(etages && etages?.length) && + etages.map((etage, index) => ( + + )) + } + +
    +
    + + +
    +
    + + +
    +
    + + +
    +
    +

    +
    + +
    +
    -

    List des Places

    +

    Liste des Places

    @@ -91,16 +330,16 @@ const Place = () => { { handleSearchingPlace(e) }} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des places..." required />
    - {isArray(places) && places?.length !== 0 && isArray(tables) && tables?.length !== 0 ? -
    - - + {isArray(filteredPlaces) && filteredPlaces?.length != 0 && isArray(tables) && tables?.length != 0 ? +
    +
    + - + - {places?.map((element) => { - return + {filteredPlaces?.map((element) => { + return })}
    PlacePlace-Zone-EtageTable-Zone-Etage Action
    -- GitLab From 936122d37d5b06df3f4bc60782b0c45823f9afaa Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 19 Jun 2024 15:25:13 +0100 Subject: [PATCH 70/79] fixed design issues - fix language to fr --- .../consultation-reservations/PlaceUI.jsx | 6 +-- .../consultation-reservations/ZoneUI.jsx | 2 +- .../(dashboard)/planning/PlanningTable.jsx | 31 ++++++++++- src/app/(dashboard)/planning/page.jsx | 52 +++++++++++-------- .../planning/type-presence/EntityForm.jsx | 2 +- .../planning/type-presence/page.jsx | 2 +- src/app/(dashboard)/projects/ProjectForm.jsx | 6 +-- src/app/(dashboard)/projects/page.jsx | 7 ++- 8 files changed, 73 insertions(+), 35 deletions(-) diff --git a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx index f60e74f..0f8bedf 100644 --- a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx @@ -38,11 +38,11 @@ const PlaceUI = ({ id, isTop }) => { )} {showTooltip && bookedPlace && (
    -

    First Name: {bookedPlace.first_name}

    -

    Last Name: {bookedPlace.last_name}

    +

    Nom: {bookedPlace.first_name}

    +

    Prénom: {bookedPlace.last_name}

    Role: {bookedPlace.role}

    Presence: {bookedPlace.presence}

    -

    Created At: {new Date(bookedPlace.created_at).toLocaleString()}

    +

    Crée à: {new Date(bookedPlace.created_at).toLocaleString()}

    )}
    diff --git a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx index 48ae7d1..cd378c1 100644 --- a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx @@ -4,7 +4,7 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return (
    -

    {nom}

    +

    Zone {nom}

    {tables.map((table) => { return diff --git a/src/app/(dashboard)/planning/PlanningTable.jsx b/src/app/(dashboard)/planning/PlanningTable.jsx index 2bf1d00..34228d7 100644 --- a/src/app/(dashboard)/planning/PlanningTable.jsx +++ b/src/app/(dashboard)/planning/PlanningTable.jsx @@ -2,14 +2,41 @@ import React, {useEffect, useState} from 'react'; import fetchRequest from "@/app/lib/fetchRequest"; import {useNotification} from "@/context/NotificationContext"; -const PlanningTable = ({ data, typePresences, onTypePresenceChange }) => { +const PlanningTable = ({ data, typePresences, onTypePresenceChange, selectedProject }) => { // fetch type presence const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); const {toggleNotification} = useNotification() + const [selectedPresence, setSelectedPresence] = useState(''); + + const handleRadioChange = (e) => { + const newPresence = e.target.value; + setSelectedPresence(newPresence); + data.forEach((row, rowIndex) => { + row.days.forEach((day, dayIndex) => { + onTypePresenceChange(rowIndex, dayIndex, newPresence); + }); + }); + }; return ( -
    +
    +
    + Réglez Tout sur : + {typePresences.map((typePresence) => ( + + ))} +
    diff --git a/src/app/(dashboard)/planning/page.jsx b/src/app/(dashboard)/planning/page.jsx index b9bdd26..f6dc6d3 100644 --- a/src/app/(dashboard)/planning/page.jsx +++ b/src/app/(dashboard)/planning/page.jsx @@ -9,11 +9,11 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; const PlanningPage = () => { const blankData = { planning_data: [ - { week: 1, days: [{}, {}, {}, {}, {}] }, - { week: 2, days: [{}, {}, {}, {}, {}] }, - { week: 3, days: [{}, {}, {}, {}, {}] }, - { week: 4, days: [{}, {}, {}, {}, {}] }, - { week: 5, days: [{}, {}, {}, {}, {}] }, + { week: 1, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 2, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 3, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 4, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, + { week: 5, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, ] } const [projects, setProjects] = useState([]); @@ -99,7 +99,17 @@ const PlanningPage = () => { if (!selectedProject) { toggleNotification({ visible: true, - message: 'Please select a project', + message: 'Veuillez sélectionner un projet', + type: 'error' + }); + return; + } + // verify if planing data has an empty value + const emptyValue = planningData.planning_data.some(week => week.days.some(day => day.nom === '')); + if (emptyValue) { + toggleNotification({ + visible: true, + message: 'Veuillez remplir toutes les valeurs de planification', type: 'error' }); return; @@ -115,7 +125,7 @@ const PlanningPage = () => { if (isSuccess) { toggleNotification({ visible: true, - message: 'Planning data saved successfully', + message: 'Données de planification enregistrées avec succès', type: 'success' }); fetchPlanningData() @@ -123,14 +133,12 @@ const PlanningPage = () => { setErrors(errors); toggleNotification({ visible: true, - message: 'Failed to save planning data', + message: 'Échec de lenregistrement des données de planification', type: 'error' }); } }; - - const handleUpdate = async () => { setLoading(true); const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; @@ -142,14 +150,14 @@ const PlanningPage = () => { if (isSuccess) { toggleNotification({ visible: true, - message: 'Planning data updated successfully', + message: 'Données de planification mises à jour avec succès', type: 'success' }); } else { setErrors(errors); toggleNotification({ visible: true, - message: 'Failed to update planning data', + message: 'Échec de la mise à jour des données de planification', type: 'error' }); } @@ -166,14 +174,14 @@ const PlanningPage = () => { setPlanningData(blankData); toggleNotification({ visible: true, - message: 'Planning data deleted successfully', + message: 'Données de planification supprimées avec succès', type: 'success' }); } else { setErrors(errors); toggleNotification({ visible: true, - message: 'Failed to delete planning data', + message: 'Échec de la suppression des données de planification', type: 'error' }); } @@ -195,14 +203,14 @@ const PlanningPage = () => { onChange={handleProjectChange} className="mt-4 block w-full px-3 py-2 bg-white border border-gray-300 rounded-md" > - + {projects.map((project) => ( ))} -
    +
    + onTypePresenceChange={handleTypePresenceChange} selectedProject={selectedProject} />
    {/* crud buttons*/}
    @@ -211,22 +219,22 @@ const PlanningPage = () => { setSelectedProject(''); setPlanningData(blankData); }} - >Cancel + >Annuler {planningData.id ? <> : + >Enregistrer }
    @@ -234,7 +242,7 @@ const PlanningPage = () => { isOpen={isModalOpen} onClose={() => setModalOpen(false)} onConfirm={handleConfirmDelete} - message={`Are you sure you want to delete this planning data?`} + message={`Etes-vous sûr de vouloir supprimer ces données de planification ?`} />
    ); diff --git a/src/app/(dashboard)/planning/type-presence/EntityForm.jsx b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx index 83f4dfc..408e3a4 100644 --- a/src/app/(dashboard)/planning/type-presence/EntityForm.jsx +++ b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx @@ -68,7 +68,7 @@ const EntityForm = ({ entity, id, onSaved, onCancel }) => { className="px-3 py-2 bg-sushi-500 text-white rounded-md" disabled={isLoading} > - {isLoading ? 'Saving...' : id ? 'Update' : 'Create'} + {isLoading ? 'Enregistrement...' : id ? 'Mettre à jour' : 'Créer'} diff --git a/src/app/(dashboard)/planning/type-presence/page.jsx b/src/app/(dashboard)/planning/type-presence/page.jsx index 697ba16..e3902ba 100644 --- a/src/app/(dashboard)/planning/type-presence/page.jsx +++ b/src/app/(dashboard)/planning/type-presence/page.jsx @@ -39,7 +39,7 @@ const ManagePage = () => {

    Gérer Les Entités

    -

    Type Presence

    +

    Type de Presence

    +
    setSearchQuery(e.target.value)} - placeholder="Type to search users..." + placeholder="Tapez pour rechercher des utilisateurs..." className="mt-1 block w-full px-3 py-2 bg-white border border-chicago-300 rounded-md" /> {userSuggestions.length > 0 && ( @@ -179,7 +179,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr {errors.userIds &&

    {errors.userIds}

    }
    {/* cancel*/} {editingProject && ( diff --git a/src/app/(dashboard)/projects/page.jsx b/src/app/(dashboard)/projects/page.jsx index d15db9d..230000e 100644 --- a/src/app/(dashboard)/projects/page.jsx +++ b/src/app/(dashboard)/projects/page.jsx @@ -84,6 +84,7 @@ const Projects = () => { setErrors(errors); } setLoading(false); + setIsFormOpen(false) }; const handleEditProject = async (id, updatedProject) => { @@ -167,7 +168,7 @@ const Projects = () => { className={`text-sm md:text-base p-2 ${isFormOpen ? 'bg-chicago-300' : 'bg-sushi-500'} text-white rounded-md`} onClick={toggleForm} > - {isFormOpen ? 'Cancel' : 'Add Project'} + {isFormOpen ? 'Annuler' : 'Ajouter un projet'}
    @@ -175,7 +176,7 @@ const Projects = () => { @@ -217,6 +218,8 @@ const Projects = () => { onHandlePageUrl={handlePageChange} /> + +
    -- GitLab From 759b3f173071886714ab9913192917ef6b781c07 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Wed, 19 Jun 2024 15:29:02 +0100 Subject: [PATCH 71/79] version fixed corrections place-table-zone --- src/app/(dashboard)/place/page.jsx | 29 +-- src/app/(dashboard)/table/CreateNewTable.jsx | 106 -------- src/app/(dashboard)/table/RowTable.jsx | 6 +- src/app/(dashboard)/table/page.jsx | 174 +++++++++++-- src/app/(dashboard)/zone/CreateNewZone.jsx | 108 --------- src/app/(dashboard)/zone/RowZone.jsx | 2 +- src/app/(dashboard)/zone/page.jsx | 111 ++++++++- src/static/image/svg/no-data-ill.svg | 243 +++++++++++++++++++ 8 files changed, 523 insertions(+), 256 deletions(-) delete mode 100644 src/app/(dashboard)/table/CreateNewTable.jsx delete mode 100644 src/app/(dashboard)/zone/CreateNewZone.jsx create mode 100644 src/static/image/svg/no-data-ill.svg diff --git a/src/app/(dashboard)/place/page.jsx b/src/app/(dashboard)/place/page.jsx index 5b7d4ed..68f65d6 100644 --- a/src/app/(dashboard)/place/page.jsx +++ b/src/app/(dashboard)/place/page.jsx @@ -86,6 +86,8 @@ const Place = () => { setZones(zones) } else { setTables([]) + setEtages([]) + setZones([]) } } catch (error) { console.log(error) @@ -269,7 +271,7 @@ const Place = () => { return (
    -
    +
    {!isLoadingData ? <> @@ -289,7 +291,7 @@ const Place = () => {
    - {(zones && zones?.length) && zones.map((zone, index) => ( @@ -300,7 +302,7 @@ const Place = () => {
    - {(tables && tables.length) && tables.map((table, index) => ( @@ -311,17 +313,16 @@ const Place = () => {
    - + +
    +
    +
    -
    -

    -
    -
    -
    +

    Liste des Places

    @@ -331,9 +332,9 @@ const Place = () => {
    {isArray(filteredPlaces) && filteredPlaces?.length != 0 && isArray(tables) && tables?.length != 0 ? -
    -
    - +
    +
    + diff --git a/src/app/(dashboard)/table/CreateNewTable.jsx b/src/app/(dashboard)/table/CreateNewTable.jsx deleted file mode 100644 index 50c0f90..0000000 --- a/src/app/(dashboard)/table/CreateNewTable.jsx +++ /dev/null @@ -1,106 +0,0 @@ -"use client" -import React, { useState, useEffect, useRef } from 'react' -import fetchRequest from '../../lib/fetchRequest' -import Loader from '@/components/Loader/Loader' -import { Island_Moments } from 'next/font/google' -import { useNotification } from '@/context/NotificationContext' - - -const CreateNewTable = ({ tablesState, zones }) => { - const [error, setError] = useState(null) - const [isLoadingAction, setIsLoadingAction] = useState(false) - const [numeroTable, setNumeroTable] = useState(null) - const [selectedZone, setSelectedZone] = useState(null) - - const inputRef = useRef(null) - const selectRef = useRef(null) - - const { toggleNotification } = useNotification() - - - const handleSubmit = async (event) => { - event.preventDefault() - setIsLoadingAction(true) - const { data, errors, isSuccess } = await fetchRequest("/zoaning/tables/", { - method: "POST", - body: JSON.stringify({ numero: numeroTable, id_zone: selectedZone }) - }) - if (isSuccess) { - setIsLoadingAction(false) - tablesState((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); - inputRef.current.value = "" - selectRef.current.value = "" - setNumeroTable(null) - setSelectedZone(null) - toggleNotification({ - visible: true, - message: "La table a été créer avec succès.", - type: "success" - }) - } else { - setIsLoadingAction(false) - if (errors.type === "ValidationError") { - if (errors.detail.non_field_errors) { - toggleNotification({ - type: "warning", - message: "Le numéro de la table saisie déjà existe.", - visible: true, - }) - } - } else { - toggleNotification({ - type: "error", - message: "Une erreur s'est produite lors de la création de la table.", - visible: true, - }) - } - console.log(errors) - } - } - - // Handle the name of zone change - const handleChangeTable = (event) => { - setError("") - setNumeroTable(event.target.value) - } - - const handleChangeZone = (event) => { - setError("") - setSelectedZone(event.target.value) - } - - - - - return ( - -

    Ajout d'une table

    -
    -
    - - -
    -
    - - -
    -
    -

    -
    - -
    - - ) -} - - -export default CreateNewTable \ No newline at end of file diff --git a/src/app/(dashboard)/table/RowTable.jsx b/src/app/(dashboard)/table/RowTable.jsx index 7e16dd2..30acd78 100644 --- a/src/app/(dashboard)/table/RowTable.jsx +++ b/src/app/(dashboard)/table/RowTable.jsx @@ -12,7 +12,7 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; -const RowZone = ({ id, numero, zone, tablesState, zones }) => { +const RowZone = ({ id, numero, zone, tablesState, zones, filteredPlacesState }) => { //states const [isUpdating, setIsUpdating] = useState(false) @@ -52,6 +52,7 @@ const RowZone = ({ id, numero, zone, tablesState, zones }) => { return } tablesState((prevTableState) => prevTableState.map((element) => element.id === id ? { ...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone) } : element)) + filteredPlacesState((prevTableState) => prevTableState.map((element) => element.id === id ? { ...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone) } : element)) setIsUpdating(false) toggleNotification({ visible: true, @@ -102,6 +103,7 @@ const RowZone = ({ id, numero, zone, tablesState, zones }) => { const { isSuccess, errors, status } = await fetchRequest(`/zoaning/tables/${id}/`, { method: "DELETE" }) if (isSuccess) { tablesState((prevTableState) => prevTableState.filter((element) => element.id !== id)) + filteredPlacesState((prevTableState) => prevTableState.filter((element) => element.id !== id)) toggleNotification({ visible: true, message: "La table a été supprimée avec succès", @@ -178,7 +180,7 @@ const RowZone = ({ id, numero, zone, tablesState, zones }) => { - - diff --git a/src/app/(dashboard)/projects/ProjectList.jsx b/src/app/(dashboard)/projects/ProjectList.jsx index c311a3b..67721cf 100644 --- a/src/app/(dashboard)/projects/ProjectList.jsx +++ b/src/app/(dashboard)/projects/ProjectList.jsx @@ -1,5 +1,7 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; -import { useState } from "react"; +import React, { useState } from "react"; +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/delete.svg"; @@ -38,15 +40,15 @@ const ProjectList = ({ projects, onEdit, onDelete, onHandlePageUrl }) => { -- GitLab From 72f98bb1e56e8419d158682ebea097826869f604 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Thu, 20 Jun 2024 11:23:06 +0100 Subject: [PATCH 74/79] version before pull from dev and start reporting --- .../AssignProject.jsx | 0 .../CompleteAffectation.jsx | 0 .../{assign_zone_project => assign-zone-project}/page.jsx | 0 src/app/ui/SideBar.jsx | 2 +- 4 files changed, 1 insertion(+), 1 deletion(-) rename src/app/(dashboard)/{assign_zone_project => assign-zone-project}/AssignProject.jsx (100%) rename src/app/(dashboard)/{assign_zone_project => assign-zone-project}/CompleteAffectation.jsx (100%) rename src/app/(dashboard)/{assign_zone_project => assign-zone-project}/page.jsx (100%) diff --git a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx b/src/app/(dashboard)/assign-zone-project/AssignProject.jsx similarity index 100% rename from src/app/(dashboard)/assign_zone_project/AssignProject.jsx rename to src/app/(dashboard)/assign-zone-project/AssignProject.jsx diff --git a/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx similarity index 100% rename from src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx rename to src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/assign-zone-project/page.jsx similarity index 100% rename from src/app/(dashboard)/assign_zone_project/page.jsx rename to src/app/(dashboard)/assign-zone-project/page.jsx diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index ea86517..8e39737 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -62,7 +62,7 @@ const SideBar = () => { }, { label: "Gestion des zones", - link: "/assign_zone_project" + link: "/assign-zone-project" , icon: }, { -- GitLab From e682071a2bd3417902f6fc5d08efa06f69b498bb Mon Sep 17 00:00:00 2001 From: Baligh ZOGHLAMI Date: Fri, 21 Jun 2024 11:19:29 +0100 Subject: [PATCH 75/79] feature: add bubble statistics presence for project and zone --- src/app/(dashboard)/privilege/page.jsx | 2 +- .../(dashboard)/reporting/BubbleStatistic.jsx | 91 ++++++++++ src/app/(dashboard)/reporting/page.jsx | 139 +++++++++++++++ src/app/(dashboard)/role/page.jsx | 4 +- .../AssignProject.jsx | 0 .../CompleteAffectation.jsx | 0 .../page.jsx | 163 +++++++++--------- src/app/lib/DateHelper.js | 32 ++++ src/app/lib/colorsGenerator.js | 5 + src/app/ui/SideBar.jsx | 32 ++-- 10 files changed, 371 insertions(+), 97 deletions(-) create mode 100644 src/app/(dashboard)/reporting/BubbleStatistic.jsx create mode 100644 src/app/(dashboard)/reporting/page.jsx rename src/app/(dashboard)/{assign_zone_project => zone-project}/AssignProject.jsx (100%) rename src/app/(dashboard)/{assign_zone_project => zone-project}/CompleteAffectation.jsx (100%) rename src/app/(dashboard)/{assign_zone_project => zone-project}/page.jsx (72%) create mode 100644 src/app/lib/DateHelper.js create mode 100644 src/app/lib/colorsGenerator.js diff --git a/src/app/(dashboard)/privilege/page.jsx b/src/app/(dashboard)/privilege/page.jsx index 3cea73f..f10039b 100644 --- a/src/app/(dashboard)/privilege/page.jsx +++ b/src/app/(dashboard)/privilege/page.jsx @@ -26,7 +26,7 @@ const Privilege = () => { return (
    -

    List des habilitations

    +

    Liste des habilitations

    {isLoading &&
    } {!isLoading && <> {(!isArray(privileges) || privileges?.length === 0) ?
    diff --git a/src/app/(dashboard)/reporting/BubbleStatistic.jsx b/src/app/(dashboard)/reporting/BubbleStatistic.jsx new file mode 100644 index 0000000..a3d82a4 --- /dev/null +++ b/src/app/(dashboard)/reporting/BubbleStatistic.jsx @@ -0,0 +1,91 @@ +"use client" +import React, { memo, useMemo, useState } from 'react' +import { Bubble } from 'react-chartjs-2'; +import { + Chart as ChartJS, + CategoryScale, + LinearScale, + PointElement, + Tooltip, + Legend +} from 'chart.js'; +import { generateColors } from '@/app/lib/colorsGenerator'; + +ChartJS.register( + CategoryScale, + LinearScale, + PointElement, + Tooltip, + Legend +); + + +const BubbleStatistic = React.memo(function BubbleStatistic({ axisX, data, title }) { + + const colors = useMemo(() => data ? generateColors(data.length) : [], [data]) + const options = { + responsive: true, + maintainAspectRatio: false, + scales: { + x: { + type: 'category', + labels: axisX, + }, + y: { + type: 'linear', + min: 0, + max: 125, + ticks: { + stepSize: 25, + callback: function (value) { + return value.toFixed(2).toString() + '%'; + } + }, + title: { + display: true, + text: 'Présence', + color: '#333', + font: { + size: 14, + weight: 'bold' + } + } + } + }, + plugins: { + tooltip: { + callbacks: { + label: function (context) { + const data = context.dataset.data[context.dataIndex]; + const label = context.dataset.label || ''; + return `${label}: (${data.x}, ${data.y.toFixed(2)}%)`; + } + } + } + } + }; + + const chartData = useMemo(() => { + if (!data) return { + datasets: [] + } + return { + datasets: data.map((element, index) => ({ + ...element, + backgroundColor: colors[index].backgroundColor, + borderColor: colors[index].borderColor, + borderWidth: 1 + })) + }; + }, [data, colors]) + return ( +
    +

    {title}

    +
    + +
    +
    + ); +}) + +export default BubbleStatistic; \ No newline at end of file diff --git a/src/app/(dashboard)/reporting/page.jsx b/src/app/(dashboard)/reporting/page.jsx new file mode 100644 index 0000000..524f1e8 --- /dev/null +++ b/src/app/(dashboard)/reporting/page.jsx @@ -0,0 +1,139 @@ +"use client" + + +import React, { useState, useEffect, useRef, useMemo } from 'react' +import BubbleStatistic from './BubbleStatistic'; +import fetchRequest from '@/app/lib/fetchRequest'; +import Loader from '@/components/Loader/Loader'; +import { useNotification } from '@/context/NotificationContext'; +import { extractDate, getDateRange, subtractDays } from '@/app/lib/DateHelper'; + + +const Reporting = () => { + const [chartDataZone, setChartDataZone] = useState(null) + const [chartDataProject, setChartDataProject] = useState(null) + const [isLoadingZone, setIsLoadingZone] = useState(false) + const [isLoadingProject, setIsLoadingProject] = useState(false) + const { toggleNotification } = useNotification() + const [dates, setDates] = useState({ fromDate: extractDate(subtractDays(new Date(), 4)), toDate: extractDate(new Date()) }) + useEffect(() => { + const getZonesPresenceStatistic = async () => { + setIsLoadingZone(true) + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/zone-presence/`, { + method: "POST", + body: JSON.stringify({ + from_date: dates.fromDate, + to_date: dates.toDate + }) + }) + setIsLoadingZone(false) + if (isSuccess) { + setChartDataZone(data); + } else { + console.log(errors); + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + getZonesPresenceStatistic() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dates.fromDate, dates.toDate]) + + + useEffect(() => { + const getProjectsPresenceStatistic = async () => { + setIsLoadingProject(true) + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/project-presence/`, { + method: "POST", + body: JSON.stringify({ + from_date: dates.fromDate, + to_date: dates.toDate + }) + }) + setIsLoadingProject(false) + if (isSuccess) { + console.log(data); + setChartDataProject(data); + } else { + console.log(errors); + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + getProjectsPresenceStatistic() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [dates.fromDate, dates.toDate]) + const handleDateChange = (event) => { + const name = event.target.name + const value = event.target.value + if (value) { + setDates({ ...dates, [name]: value }) + return; + } + else if (!value && name === "fromDate") setDates({ ...dates, "fromDate": extractDate(subtractDays(dates.toDate, 5)) }) + else if (!value && name === "toDate") setDates({ ...dates, "toDate": extractDate(new Date()) }) + } + const axisX = useMemo(() => getDateRange(dates.fromDate, dates.toDate), [dates.fromDate, dates.toDate]) + const processedProjectData = useMemo(() => { + return chartDataProject?.map((project, index) => ( + { + label: project.label, + data: [...project.data.map((element) => ( + { + x: element.date.split("-").reverse().join("-"), + y: element.pourcentage, r: (index * 2) + 6 + } + )), ...axisX.filter((element) => !project.data.find((elm) => elm.date.split("-").reverse().join("-") === element)).map((element) => ({ + x: element, + y: 0, + r: (index * 2) + 6 + }))] + })) + }, [chartDataProject, axisX]) + + const processedZoneData = useMemo(() => { + return chartDataZone?.map((zone, index) => ( + { + label: "Zone " + zone.label, + data: [...zone.data.map((element) => ( + { + x: element.date.split("-").reverse().join("-"), + y: element.pourcentage, r: (index * 2) + 6 + } + )), ...axisX.filter((element) => !zone.data.find((elm) => elm.date.split("-").reverse().join("-") === element)).map((element) => ({ + x: element, + y: 0, + r: (index * 2) + 6 + }))] + })) + }, [chartDataZone, axisX]) + return ( +
    +
    +
    + + +
    +
    + + + {/* */} +
    +
    + {(isLoadingZone || isLoadingProject) ?
    + +
    :
    + + +
    } +
    + ) +} + +export default Reporting \ No newline at end of file diff --git a/src/app/(dashboard)/role/page.jsx b/src/app/(dashboard)/role/page.jsx index 7e682d2..dfca5e0 100644 --- a/src/app/(dashboard)/role/page.jsx +++ b/src/app/(dashboard)/role/page.jsx @@ -40,7 +40,7 @@ const Role = () => { {openCreatePopup && } {roleToUpdate && }
    -

    List des Roles

    +

    Liste des Rôles

    Place Table-Zone-Etage Action
    setTableNum(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + + {(etages && etages?.length) && + etages.map((etage, index) => ( + + )) + } + + +
    + + +
    +
    + + +
    +
    + +
    + + +

    List des Tables

    @@ -77,17 +222,16 @@ const Table = () => { { handleSearchingTable(e) }} id="simple-search" class=" text-gray-900 text-sm block w-full ps-10 p-2.5 rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none " placeholder="Chercher des tables..." required />
    - - {isArray(tables) && tables?.length !== 0 && isArray(zones) && zones?.length !== 0 ? -
    - + {isArray(filteredTables) && filteredTables?.length !== 0 && isArray(zones) && zones?.length !== 0 ? +
    +
    - + - {tables?.map((element) => { - return + {filteredTables?.map((element) => { + return })}
    TableZone-EtageZone-Etage Action
    diff --git a/src/app/(dashboard)/zone/CreateNewZone.jsx b/src/app/(dashboard)/zone/CreateNewZone.jsx deleted file mode 100644 index ccd197d..0000000 --- a/src/app/(dashboard)/zone/CreateNewZone.jsx +++ /dev/null @@ -1,108 +0,0 @@ -"use client" -import React, { useState, useEffect, useRef } from 'react' -import fetchRequest from '../../lib/fetchRequest' -import Loader from '@/components/Loader/Loader' -import { Island_Moments } from 'next/font/google' -import { useNotification } from '@/context/NotificationContext' - - -const CreateNewZone = ({ zoneState, etages }) => { - const [error, setError] = useState(null) - const [isLoadingAction, setIsLoadingAction] = useState(false) - const [nomZone, setNomZone] = useState(null) - const [selectedEtage, setSelectedEtage] = useState(null) - - const inputRef = useRef(null) - const selectRef = useRef(null) - - const { toggleNotification } = useNotification() - - - const handleSubmit = async (event) => { - event.preventDefault() - setIsLoadingAction(true) - const { data, errors, isSuccess } = await fetchRequest("/zoaning/zones/", { - method: "POST", - body: JSON.stringify({ nom: nomZone, id_etage: selectedEtage }) - }) - if (isSuccess) { - setIsLoadingAction(false) - zoneState((prevZoneValue) => [...prevZoneValue, { ...data, id_etage: etages.find(etage => etage.id === data.id_etage) }]); - inputRef.current.value = "" - selectRef.current.value = "" - setNomZone(null) - setSelectedEtage(null) - toggleNotification({ - visible: true, - message: "La zone a été créer avec succès.", - type: "success" - }) - } else { - setIsLoadingAction(false) - if (errors.type === "ValidationError") { - if (errors.detail.non_field_errors) { - toggleNotification({ - type: "warning", - message: "Le nom de la zone saisie déjà existe.", - visible: true, - }) - } - } else { - toggleNotification({ - type: "error", - message: "Une erreur s'est produite lors de la création de la zone.", - visible: true, - }) - } - console.log(errors) - } - } - - // Handle the name of zone change - const handleChangeZone = (event) => { - setError("") - setNomZone(event.target.value) - } - - const handleChangeEtage = (event) => { - setError("") - setSelectedEtage(event.target.value) - } - - console.log(selectedEtage) - console.log(nomZone) - - - - return ( -
    -

    Ajout d'une zone

    -
    -
    - - -
    -
    - - -
    -
    -

    -
    - -
    -
    - ) -} - - -export default CreateNewZone \ No newline at end of file diff --git a/src/app/(dashboard)/zone/RowZone.jsx b/src/app/(dashboard)/zone/RowZone.jsx index bb1be7d..eac3a4c 100644 --- a/src/app/(dashboard)/zone/RowZone.jsx +++ b/src/app/(dashboard)/zone/RowZone.jsx @@ -179,7 +179,7 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => {
    setZoneName(event.target.value)} defaultValue={nom} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> + + +
    + + +
    +
    + +
    + + +

    List des Zones

    {isArray(zones) && zones?.length !== 0 && isArray(etages) && etages?.length !== 0 ? -
    - +
    +
    - + - {zones?.map((element) => { + {(selectedEtage) ? + zones?.filter(zone => zone.id_etage.id == selectedEtage).map((element) => {return }) + : + zones?.map((element) => { return })}
    ZoneEtageEtage Action
    diff --git a/src/static/image/svg/no-data-ill.svg b/src/static/image/svg/no-data-ill.svg new file mode 100644 index 0000000..992a01c --- /dev/null +++ b/src/static/image/svg/no-data-ill.svg @@ -0,0 +1,243 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + -- GitLab From c66de95eb2790d0dae7cc6ac410777111f8a8213 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Wed, 19 Jun 2024 15:58:58 +0100 Subject: [PATCH 72/79] fixed consulattion des reservation --- package-lock.json | 14 ++++++------- .../consultation-reservations/ZoneUI.jsx | 8 +++---- .../consultation-reservations/page.jsx | 21 +++++++++++++------ 3 files changed, 26 insertions(+), 17 deletions(-) diff --git a/package-lock.json b/package-lock.json index 583ab6c..4c126e3 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3401,11 +3401,11 @@ } }, "node_modules/braces": { - "version": "3.0.2", - "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", - "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.3.tgz", + "integrity": "sha512-yQbXgO/OSZVD2IsiLlro+7Hf6Q18EJrKSEsdoMzKePKXct3gvD8oLcOQdIzGupr5Fj+EDe8gO/lxc1BzfMpxvA==", "dependencies": { - "fill-range": "^7.0.1" + "fill-range": "^7.1.1" }, "engines": { "node": ">=8" @@ -4697,9 +4697,9 @@ } }, "node_modules/fill-range": { - "version": "7.0.1", - "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", - "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "version": "7.1.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.1.1.tgz", + "integrity": "sha512-YsGpe3WHLK8ZYi4tWDg2Jy3ebRz2rXowDxnld4bkQB00cc/1Zw9AWnC0i9ztDJitivtQvaI9KaLyKrc+hBW0yg==", "dependencies": { "to-regex-range": "^5.0.1" }, diff --git a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx index cd378c1..3ea1b5c 100644 --- a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx @@ -3,10 +3,10 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return ( -
    -

    Zone {nom}

    -
    - {tables.map((table) => { +
    +

    Zone {nom}

    +
    + {tables.map((table) => { return })}
    diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx index dad6282..8ac1cab 100644 --- a/src/app/(dashboard)/consultation-reservations/page.jsx +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -1,6 +1,6 @@ 'use client' -import React, { useEffect, useState } from 'react' +import React, { useEffect, useState, useRef } from 'react' import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' @@ -187,6 +187,16 @@ const Reservation = () => { const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] + + const dateRef = useRef(null) + useEffect(() => { + if (dateRef.current && currentDateData?.date) { + dateRef.current.value = JSON.stringify(currentDateData) + setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) + } + }, [dateRef.current, currentDateData?.date]) + + if (isLoadingSelectsData) return
    @@ -203,14 +213,13 @@ const Reservation = () => { return (
    - {filteredDatesData?.map((date, index) => ( ))} - + }
    diff --git a/src/app/(dashboard)/assign_zone_project/AssignProject.jsx b/src/app/(dashboard)/zone-project/AssignProject.jsx similarity index 100% rename from src/app/(dashboard)/assign_zone_project/AssignProject.jsx rename to src/app/(dashboard)/zone-project/AssignProject.jsx diff --git a/src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx b/src/app/(dashboard)/zone-project/CompleteAffectation.jsx similarity index 100% rename from src/app/(dashboard)/assign_zone_project/CompleteAffectation.jsx rename to src/app/(dashboard)/zone-project/CompleteAffectation.jsx diff --git a/src/app/(dashboard)/assign_zone_project/page.jsx b/src/app/(dashboard)/zone-project/page.jsx similarity index 72% rename from src/app/(dashboard)/assign_zone_project/page.jsx rename to src/app/(dashboard)/zone-project/page.jsx index c9a2606..c6de000 100644 --- a/src/app/(dashboard)/assign_zone_project/page.jsx +++ b/src/app/(dashboard)/zone-project/page.jsx @@ -1,6 +1,6 @@ "use client" import React, { useEffect, useState } from 'react'; -import AddIcon from "@/static/image/svg/add.svg"; +import AddIcon from "@/static/image/svg/add.svg"; import AssignProject from './AssignProject'; import { useNotification } from '@/context/NotificationContext' @@ -16,15 +16,15 @@ import CompleteAffectation from './CompleteAffectation'; const AffectingZoneProject = () => { const [isOpen, setIsOpen] = useState(false) - const [ isOpenCompleteAffectation, setIsOpenCompleteAffectation ] = useState(false) - const [ listProjectsAffected, setListProjectsAffected ] = useState([]) - const [ isLoadingListProjects, setIsLoadingListProjects ] = useState(false) + const [isOpenCompleteAffectation, setIsOpenCompleteAffectation] = useState(false) + const [listProjectsAffected, setListProjectsAffected] = useState([]) + const [isLoadingListProjects, setIsLoadingListProjects] = useState(false) const { toggleNotification } = useNotification() - const [ selectedWeek, setSelectedWeek ] = useState(null) - const [ selectedDay, setSelectedDay ] = useState(null) - const [ selectedAffectaionToDelete, setSelectedAffectationToDelete ] = useState(null) + const [selectedWeek, setSelectedWeek] = useState(null) + const [selectedDay, setSelectedDay] = useState(null) + const [selectedAffectaionToDelete, setSelectedAffectationToDelete] = useState(null) const [isModalOpen, setModalOpen] = useState(false); - const [ listProjectsSemiAffected, setListProjectsSemiAffected ] = useState([]) + const [listProjectsSemiAffected, setListProjectsSemiAffected] = useState([]) useEffect(() => { @@ -76,20 +76,21 @@ const AffectingZoneProject = () => { const getListOfAffectedProjects = async () => { setIsLoadingListProjects(true) - try{ - if(selectedDay && selectedWeek){ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, {method: 'GET'}) - }else if (selectedWeek){ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, {method: 'GET'}) - }else if (selectedDay){ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, {method: 'GET'}) - }else{ - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, {method: 'GET'}) + try { + if (selectedDay && selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedWeek) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, { method: 'GET' }) + } else if (selectedDay) { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, { method: 'GET' }) + } else { + var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, { method: 'GET' }) } - if(isSuccess){ + if (isSuccess) { setListProjectsAffected(data) + console.log("this is our ", data); setListProjectsSemiAffected(filterAndGroupProjects(data)) - }else{ + } else { toggleNotification({ visible: true, message: errors[0].message, @@ -97,7 +98,7 @@ const AffectingZoneProject = () => { }) } setIsLoadingListProjects(false) - }catch(error){ + } catch (error) { setIsLoadingListProjects(false) console.log(error) toggleNotification({ @@ -120,9 +121,9 @@ const AffectingZoneProject = () => { } const handleDeleteAffectation = async () => { - try{ - var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, {method: 'DELETE'}) - if(isSuccess){ + try { + var { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/deteleAffectedProject/${selectedAffectaionToDelete.id}`, { method: 'DELETE' }) + if (isSuccess) { toggleNotification({ visible: true, message: "Affectation supprimer avec succès", @@ -131,20 +132,20 @@ const AffectingZoneProject = () => { const filteredProjectsAffected = listProjectsAffected.filter(affected => affected.id !== selectedAffectaionToDelete.id) setListProjectsAffected(filteredProjectsAffected) mutateProjectsAffectaionCheck(filteredProjectsAffected) - }else if(status === 404){ + } else if (status === 404) { toggleNotification({ visible: true, message: "Affectation introuvable", type: "error" }) - }else{ + } else { toggleNotification({ visible: true, message: errors[0].message, type: "error" }) } - }catch(error){ + } catch (error) { console.log(error) toggleNotification({ visible: true, @@ -252,61 +253,61 @@ const AffectingZoneProject = () => {

    Affecter Projet

    - { (!isLoadingListProjects) ? - (listProjectsAffected && listProjectsAffected.length > 0) ? + {(!isLoadingListProjects) ? + (listProjectsAffected && listProjectsAffected.length > 0) ?
    - - - - - - - - - - - - { (listProjectsAffected.map( (element, index) => - - {/* */} - + + + + + + + - - - - - - - ))} + + + {(listProjectsAffected.map((element, index) => + + {/* */} + + + + + + + + + ))}
    - Date - - Plateau - - Projet - - Places occupées - - Nombre des personnes - - Places disponible - - Actions -
    - Semaine: {element.semaine} - Jour: {element.jour} +
    + Date + + Plateau + + Projet + + Places occupées + + Nombre des personnes + + Places disponible + + Actions - {element.id_zone.nom}-{element.id_zone.id_etage.numero} - - {element.id_project.nom} - - {element.places_occuper} - - {element.nombre_personnes} - - {element.places_disponibles} - -
    handleDeleteClick(element)} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
    -
    + Semaine: {element.semaine} - Jour: {element.jour} + + {element.id_zone.nom}-{element.id_zone.id_etage.numero} + + {element.id_project.nom} + + {element.places_occuper} + + {element.nombre_personnes} + + {element.places_disponibles} + +
    handleDeleteClick(element)} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
    +
    : @@ -318,10 +319,10 @@ const AffectingZoneProject = () => { } { (listProjectsSemiAffected && listProjectsSemiAffected.length) ?
    - -
    + +
    : "" } diff --git a/src/app/lib/DateHelper.js b/src/app/lib/DateHelper.js new file mode 100644 index 0000000..f4aac76 --- /dev/null +++ b/src/app/lib/DateHelper.js @@ -0,0 +1,32 @@ +export const subtractDays = (date, numberOfDays) => { + const result = new Date(date); + result.setDate(result.getDate() - numberOfDays); + return result; +}; + +/** + * date {Date} + * @return 2024-10-12 + */ +export const extractDate = (date) => { + if (date instanceof Date) + return date.toJSON().split("T")[0] + else throw new Error("date isn't instance of Date in extractDate Util function") +} + + +export const getDateRange = (fromDate, toDate) => { + const startDate = new Date(fromDate); + const endDate = new Date(toDate); + + const dateArray = []; + + let currentDate = startDate; + while (currentDate <= endDate) { + dateArray.push(currentDate.toISOString().split('T')[0].split("-").reverse().join("-")); + + currentDate.setDate(currentDate.getDate() + 1); + } + + return dateArray; +} \ No newline at end of file diff --git a/src/app/lib/colorsGenerator.js b/src/app/lib/colorsGenerator.js new file mode 100644 index 0000000..ba53b5c --- /dev/null +++ b/src/app/lib/colorsGenerator.js @@ -0,0 +1,5 @@ +import chroma from 'chroma-js'; + +export const generateColors = (numColors) => { + return chroma.scale(["#6574cd", "#f6ad55", "#e53e3e", "#cbd5e0", "#68D391"]).mode('lch').colors(numColors).map(color => ({ backgroundColor: chroma(color).alpha(0.3).css(), borderColor: chroma(color).alpha(0.6).css() })); +}; \ No newline at end of file diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index 6c3d331..304e3d6 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -1,7 +1,9 @@ +"use client" import SideBarLink from "./SideBarLink"; import RoleIcon from "@/static/image/svg/role.svg" import UserIcon from "@/static/image/svg/user.svg" import LogoutButton from "./LogoutButton"; +import { usePathname } from "next/navigation"; const SideBar = () => { const AdminLinks = [ @@ -42,13 +44,13 @@ const SideBar = () => { }, { label: "Etage", - link: "/etage" - , icon: + link: "/etage", + icon: }, { label: "Zone", - link: "/zone" - , icon: + link: "/zone", + icon: }, { label: "Tables", @@ -62,25 +64,28 @@ const SideBar = () => { }, { label: "Gestion des zones", - link: "/assign_zone_project" + link: "/zone-project" , icon: }, + ] + const ConsultationLinks = [ { label: "Consulter les réservations", - link: "/consultation-reservations" - , icon: - }, + link: "/consultation-reservations", + icon: + } ] + const pathname = usePathname() return ( diff --git a/src/middleware.js b/src/middleware.js index 9c10ccd..817153c 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -3,8 +3,8 @@ import { decrypt } from '@/app/lib/session' import { cookies } from 'next/headers' // 1. Specify protected and public routes -const protectedRoutes = ['/dashboard', '/auth/verif'] -const publicRoutes = ['/auth/login', '/auth/signup'] +const protectedRoutes = ['/dashboard', '/auth/verif', '/users', '/privilege', '/projects', '/role', '/etage', '/place', '/zone', '/table', '/reservation', '/planning', '/planning/type-presence', '/assign-zone-project', '/consultation-reservations'] +const publicRoutes = ['/auth/login', '/auth/signup', 'auth/forgot-password', '/auth/change-password'] export default async function middleware(req) { // 2. Check if the current route is protected or public @@ -15,13 +15,25 @@ export default async function middleware(req) { // 3. Decrypt the session from the cookie const cookie = cookies().get('session')?.value const session = await decrypt(cookie) - // console.log('session dfdf', session) + // 5. Redirect to /login if the user is not authenticated if (isProtectedRoute && !session?.sessionData.token) { return NextResponse.redirect(new URL('/auth/login', req.nextUrl)) } - // 6. Redirect to /dashboard if the user is authenticated + // 6. Check if the user has the necessary privileges + const userPrivileges = session?.sessionData.privileges || [] + console.log('userPrivileges', userPrivileges) + // check if the user the necessary privileges to access the current route + const hasPrivileges = userPrivileges.some(privilege => path.startsWith(`/${privilege}`)); + console.log('hasPrivileges', hasPrivileges) + + if (isProtectedRoute && !hasPrivileges) { + console.log('.') + return NextResponse.redirect(new URL('/no-access', req.nextUrl)); + } + + // 7. Redirect to /dashboard if the user is authenticated if ( isPublicRoute && session?.sessionData.token && -- GitLab From 5678e83f8226f1ce7c408b88c9adc92859c14827 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Fri, 21 Jun 2024 14:25:51 +0100 Subject: [PATCH 77/79] version fixed project update in affecting projects-zone --- .../CompleteAffectation.jsx | 8 +++-- .../(dashboard)/assign-zone-project/page.jsx | 31 +++++++++---------- 2 files changed, 20 insertions(+), 19 deletions(-) diff --git a/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx index d59137a..76045fe 100644 --- a/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx +++ b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx @@ -22,6 +22,9 @@ const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, f const [ otherPlaces, setOtherPlaces ] = useState([]) const [ selectedOtherZone, setSelectedOtherZone ] = useState(null) + console.log("fullAffectations", fullAffectations) + console.log("affectations", affectations) + const getZones = async (day, week) => { console.log("day, week", day, week) @@ -217,8 +220,9 @@ const CompleteAffectation = ({ setIsOpen, listAffectationsState, affectations, f
    {(affectations) && affectations.map((element, index) => -
    handleProjectSelection(element)} key={index} className='border-b border-gray-200'> - Projet: {element?.project?.nom} -- Collaborateurs: {element.nbr_personnes_restant} +
    handleProjectSelection(element)} key={index} className={`cursor-pointer mx-auto will-change-contents min-h-8 text-sm break-words flex flex-col items-center justify-center duration-150 delay-75 border-2 py-0.5 rounded-md w-fit px-2 text-sushi-600 ${ selectedProject?.project?.id === element.project.id ? "font-semibold border-sushi-400 bg-sushi-100" : "font-medium hover:border-sushi-400 hover:bg-sushi-100 border-sushi-400 bg-white"}`}> +

    Projet: {element?.project?.nom} -- Collaborateurs: {element.nbr_personnes_restant}

    +

    Semaine: {element.semaine} -- Jour: {element.jour}

    ) } diff --git a/src/app/(dashboard)/assign-zone-project/page.jsx b/src/app/(dashboard)/assign-zone-project/page.jsx index c9a2606..f02288e 100644 --- a/src/app/(dashboard)/assign-zone-project/page.jsx +++ b/src/app/(dashboard)/assign-zone-project/page.jsx @@ -33,13 +33,13 @@ const AffectingZoneProject = () => { function filterAndGroupProjects(data) { // Step 1: Create an object to aggregate data by id_project, semaine, and jour const aggregatedProjects = {}; - + data.forEach(project => { const projectId = project.id_project.id; const week = project.semaine; const day = project.jour; const key = `${projectId}-${week}-${day}`; - + if (!aggregatedProjects[key]) { aggregatedProjects[key] = { id_project: project.id_project, @@ -47,30 +47,30 @@ const AffectingZoneProject = () => { jour: day, places_disponibles: 0, places_occuper: 0, - nombre_personnes: project.nombre_personnes + nombre_personnes: project.id_project.users.length }; } - + aggregatedProjects[key].places_disponibles += project.places_disponibles; aggregatedProjects[key].places_occuper += project.places_occuper; }); - + // Step 2: Filter out projects that don't meet the condition const filteredProjects = Object.values(aggregatedProjects).filter(project => { - return project.nombre_personnes - project.places_occuper > 0; + return project.id_project.users.length - project.places_occuper > 0; }); - + // Step 3: Prepare the final result with additional fields const result = filteredProjects.map(project => { return { project: project.id_project, semaine: project.semaine, jour: project.jour, - nbr_personnes_restant: project.nombre_personnes - project.places_occuper, - nombre_personnes: project.nombre_personnes + nbr_personnes_restant: project.id_project.users.length - project.places_occuper, + nombre_personnes: project.id_project.users.length }; }); - + return result; } @@ -185,7 +185,7 @@ const AffectingZoneProject = () => { jour: day, places_disponibles: 0, places_occuper: 0, - nombre_personnes: project.nombre_personnes + nombre_personnes: project.id_project.users.length }; } @@ -195,7 +195,7 @@ const AffectingZoneProject = () => { // Step 2: Filter out projects that don't meet the condition const filteredProjects = Object.values(aggregatedProjects).filter(project => { - return project.nombre_personnes - project.places_occuper > 0; + return project.id_project.users.length - project.places_occuper > 0; }); // Step 3: Prepare the final result with additional fields @@ -204,8 +204,8 @@ const AffectingZoneProject = () => { project: project.id_project, semaine: project.semaine, jour: project.jour, - nbr_personnes_restant: project.nombre_personnes - project.places_occuper, - nombre_personnes: project.nombre_personnes + nbr_personnes_restant: project.id_project.users.length - project.places_occuper, + nombre_personnes: project.id_project.users.length }; }); @@ -213,12 +213,9 @@ const AffectingZoneProject = () => { } const mutateProjectsAffectaionCheck = (passedData) => { - console.log("mutaion triggered") setListProjectsSemiAffected(filterAndGroupProjects(passedData)) } - console.log("project semi affected", listProjectsSemiAffected) - console.log("project fully associated", listProjectsAffected) return ( -- GitLab From 69b9fcb5649b49f0e02c302bcfdcf3aa43bc2d1b Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Mon, 24 Jun 2024 16:33:29 +0100 Subject: [PATCH 78/79] reporting new design v_1 --- package-lock.json | 34 ++++++++ package.json | 2 + .../assign-zone-project/AssignProject.jsx | 2 +- .../CompleteAffectation.jsx | 2 +- .../(dashboard)/planning/PlanningTable.jsx | 2 +- src/app/(dashboard)/reporting/page.jsx | 80 +++++++++++++++++-- src/app/ui/SideBar.jsx | 1 + src/middleware.js | 2 +- src/static/image/svg/desk-1.svg | 14 ++++ src/static/image/svg/project.svg | 1 + src/static/image/svg/statistics.svg | 23 ++++++ 11 files changed, 151 insertions(+), 12 deletions(-) create mode 100644 src/static/image/svg/desk-1.svg create mode 100644 src/static/image/svg/project.svg create mode 100644 src/static/image/svg/statistics.svg diff --git a/package-lock.json b/package-lock.json index 4c126e3..459b222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "0.1.0", "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "chroma-js": "^2.4.2", "jose": "^5.3.0", "js-cookie": "^3.0.5", "next": "14.2.3", "react": "^18", + "react-chartjs-2": "^5.2.0", "react-dom": "^18", "react-select": "^5.8.0" }, @@ -2330,6 +2332,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "peer": true + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -3536,6 +3544,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3570,6 +3590,11 @@ "node": ">= 6" } }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -6501,6 +6526,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/package.json b/package.json index 7cf3ab1..f9e94e3 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ }, "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "chroma-js": "^2.4.2", "jose": "^5.3.0", "js-cookie": "^3.0.5", "next": "14.2.3", "react": "^18", + "react-chartjs-2": "^5.2.0", "react-dom": "^18", "react-select": "^5.8.0" }, diff --git a/src/app/(dashboard)/assign-zone-project/AssignProject.jsx b/src/app/(dashboard)/assign-zone-project/AssignProject.jsx index 3c923a8..5566068 100644 --- a/src/app/(dashboard)/assign-zone-project/AssignProject.jsx +++ b/src/app/(dashboard)/assign-zone-project/AssignProject.jsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from 'react' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" import UserIcon from "@/static/image/svg/user.svg" -import DeskIcon from "@/static/image/svg/study-desk.svg" +import DeskIcon from "@/static/image/svg/desk-1.svg" import fetchRequest from "@/app/lib/fetchRequest"; diff --git a/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx index 76045fe..649f94f 100644 --- a/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx +++ b/src/app/(dashboard)/assign-zone-project/CompleteAffectation.jsx @@ -3,7 +3,7 @@ import React, { useState, useEffect, useRef } from 'react' import { useNotification } from '@/context/NotificationContext' import CancelIcon from "@/static/image/svg/cancel.svg" import UserIcon from "@/static/image/svg/user.svg" -import DeskIcon from "@/static/image/svg/study-desk.svg" +import DeskIcon from "@/static/image/svg/desk-1.svg" import AddIcon from "@/static/image/svg/add.svg" import fetchRequest from "@/app/lib/fetchRequest"; import Loader from '@/components/Loader/Loader' diff --git a/src/app/(dashboard)/planning/PlanningTable.jsx b/src/app/(dashboard)/planning/PlanningTable.jsx index 34228d7..0db1822 100644 --- a/src/app/(dashboard)/planning/PlanningTable.jsx +++ b/src/app/(dashboard)/planning/PlanningTable.jsx @@ -20,7 +20,7 @@ const PlanningTable = ({ data, typePresences, onTypePresenceChange, selectedProj }; return ( -
    +
    Réglez Tout sur : {typePresences.map((typePresence) => ( diff --git a/src/app/(dashboard)/reporting/page.jsx b/src/app/(dashboard)/reporting/page.jsx index 524f1e8..47058e4 100644 --- a/src/app/(dashboard)/reporting/page.jsx +++ b/src/app/(dashboard)/reporting/page.jsx @@ -7,6 +7,10 @@ import fetchRequest from '@/app/lib/fetchRequest'; import Loader from '@/components/Loader/Loader'; import { useNotification } from '@/context/NotificationContext'; import { extractDate, getDateRange, subtractDays } from '@/app/lib/DateHelper'; +import UserIcon from "@/static/image/svg/user.svg" +import ProjectIcon from "@/static/image/svg/project.svg" +import StatisticsIcon from "@/static/image/svg/statistics.svg" +import DeskIcon from "@/static/image/svg/desk-1.svg" const Reporting = () => { @@ -14,8 +18,36 @@ const Reporting = () => { const [chartDataProject, setChartDataProject] = useState(null) const [isLoadingZone, setIsLoadingZone] = useState(false) const [isLoadingProject, setIsLoadingProject] = useState(false) + const [ countUsers, setCountUsers ] = useState(0) + const [ countProjects, setCountProjects ] = useState(0) + const [ countPlaces, setCountPlaces ] = useState(0) const { toggleNotification } = useNotification() const [dates, setDates] = useState({ fromDate: extractDate(subtractDays(new Date(), 4)), toDate: extractDate(new Date()) }) + + useEffect(() => { + const getStats= async () => { + const { data, errors, isSuccess } = await fetchRequest(`/statistics/`, { + method: "GET", + }) + if (isSuccess) { + setCountUsers(data.user_count) + setCountProjects(data.project_count) + setCountPlaces(data.place_count) + } else { + console.log(errors); + toggleNotification({ + type: "error", + message: "Internal Server Error", + visible: true + }) + } + } + getStats() + }, []) + console.log(countPlaces) + console.log(countProjects) + console.log(countUsers) + useEffect(() => { const getZonesPresenceStatistic = async () => { setIsLoadingZone(true) @@ -115,15 +147,47 @@ const Reporting = () => { }, [chartDataZone, axisX]) return (
    -
    -
    - - +
    +
    +

    Collaborateurs

    +
    + {countUsers} +
    + + +
    +
    +
    +
    +

    Projets

    +
    + {countProjects} +
    + + +
    +
    +
    +
    +

    Places

    +
    + {countPlaces} +
    + + +
    +
    -
    - - - {/* */} +
    +
    + + +
    +
    + + + {/* */} +
    {(isLoadingZone || isLoadingProject) ?
    diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index 04c5c04..965352b 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -45,6 +45,7 @@ const SideBar = () => { { label: "Places", link: "/place", icon: , privilege: "place" }, { label: "Gestion des zones", link: "/assign-zone-project", icon: , privilege: "assign-zone-project" }, { label: "Consulter les réservations", link: "/consultation-reservations", icon: , privilege: "consultation-reservations" }, + { label: "Reporting", link: "/reporting", icon: , privilege: "reporting" }, ]; console.log('sessionDataSideBar', sessionData) diff --git a/src/middleware.js b/src/middleware.js index 817153c..4b0c14d 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -3,7 +3,7 @@ import { decrypt } from '@/app/lib/session' import { cookies } from 'next/headers' // 1. Specify protected and public routes -const protectedRoutes = ['/dashboard', '/auth/verif', '/users', '/privilege', '/projects', '/role', '/etage', '/place', '/zone', '/table', '/reservation', '/planning', '/planning/type-presence', '/assign-zone-project', '/consultation-reservations'] +const protectedRoutes = ['/reporting', '/dashboard', '/auth/verif', '/users', '/privilege', '/projects', '/role', '/etage', '/place', '/zone', '/table', '/reservation', '/planning', '/planning/type-presence', '/assign-zone-project', '/consultation-reservations'] const publicRoutes = ['/auth/login', '/auth/signup', 'auth/forgot-password', '/auth/change-password'] export default async function middleware(req) { diff --git a/src/static/image/svg/desk-1.svg b/src/static/image/svg/desk-1.svg new file mode 100644 index 0000000..3bfcdbd --- /dev/null +++ b/src/static/image/svg/desk-1.svg @@ -0,0 +1,14 @@ + + + + + + \ No newline at end of file diff --git a/src/static/image/svg/project.svg b/src/static/image/svg/project.svg new file mode 100644 index 0000000..db8fcbc --- /dev/null +++ b/src/static/image/svg/project.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/statistics.svg b/src/static/image/svg/statistics.svg new file mode 100644 index 0000000..a84c165 --- /dev/null +++ b/src/static/image/svg/statistics.svg @@ -0,0 +1,23 @@ + + + + + + + + + + + + + \ No newline at end of file -- GitLab From a06338a82cf4d9bbca6e98b77b4f37eff92983b9 Mon Sep 17 00:00:00 2001 From: Oussama El Benney Date: Mon, 24 Jun 2024 16:35:15 +0100 Subject: [PATCH 79/79] fixed header + sidebar + design --- package-lock.json | 34 +++++++++++++++++ package.json | 2 + src/app/no-access/page.jsx | 2 + src/app/ui/Header.jsx | 15 +++++--- src/app/ui/LogoutButton.js | 2 +- src/app/ui/SideBar.css | 47 +++++++++++++++++++++++ src/app/ui/SideBar.jsx | 18 +++++---- src/app/ui/SideBarLink.jsx | 59 ++++++++++++++++++----------- src/static/image/teamwill_logo.png | Bin 0 -> 2669 bytes tailwind.config.js | 2 +- 10 files changed, 142 insertions(+), 39 deletions(-) create mode 100644 src/app/ui/SideBar.css create mode 100644 src/static/image/teamwill_logo.png diff --git a/package-lock.json b/package-lock.json index 4c126e3..459b222 100644 --- a/package-lock.json +++ b/package-lock.json @@ -9,10 +9,12 @@ "version": "0.1.0", "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "chroma-js": "^2.4.2", "jose": "^5.3.0", "js-cookie": "^3.0.5", "next": "14.2.3", "react": "^18", + "react-chartjs-2": "^5.2.0", "react-dom": "^18", "react-select": "^5.8.0" }, @@ -2330,6 +2332,12 @@ "@jridgewell/sourcemap-codec": "^1.4.14" } }, + "node_modules/@kurkle/color": { + "version": "0.3.2", + "resolved": "https://registry.npmjs.org/@kurkle/color/-/color-0.3.2.tgz", + "integrity": "sha512-fuscdXJ9G1qb7W8VdHi+IwRqij3lBkosAm4ydQtEmbY58OzHXqQhvlxqEkoz0yssNVn38bcpRWgA9PP+OGoisw==", + "peer": true + }, "node_modules/@next/env": { "version": "14.2.3", "resolved": "https://registry.npmjs.org/@next/env/-/env-14.2.3.tgz", @@ -3536,6 +3544,18 @@ "url": "https://github.com/chalk/chalk?sponsor=1" } }, + "node_modules/chart.js": { + "version": "4.4.3", + "resolved": "https://registry.npmjs.org/chart.js/-/chart.js-4.4.3.tgz", + "integrity": "sha512-qK1gkGSRYcJzqrrzdR6a+I0vQ4/R+SoODXyAjscQ/4mzuNzySaMCd+hyVxitSY1+L2fjPD1Gbn+ibNqRmwQeLw==", + "peer": true, + "dependencies": { + "@kurkle/color": "^0.3.0" + }, + "engines": { + "pnpm": ">=8" + } + }, "node_modules/chokidar": { "version": "3.6.0", "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.6.0.tgz", @@ -3570,6 +3590,11 @@ "node": ">= 6" } }, + "node_modules/chroma-js": { + "version": "2.4.2", + "resolved": "https://registry.npmjs.org/chroma-js/-/chroma-js-2.4.2.tgz", + "integrity": "sha512-U9eDw6+wt7V8z5NncY2jJfZa+hUH8XEj8FQHgFJTrUFnJfXYf4Ml4adI2vXZOjqRDpFWtYVWypDfZwnJ+HIR4A==" + }, "node_modules/client-only": { "version": "0.0.1", "resolved": "https://registry.npmjs.org/client-only/-/client-only-0.0.1.tgz", @@ -6501,6 +6526,15 @@ "node": ">=0.10.0" } }, + "node_modules/react-chartjs-2": { + "version": "5.2.0", + "resolved": "https://registry.npmjs.org/react-chartjs-2/-/react-chartjs-2-5.2.0.tgz", + "integrity": "sha512-98iN5aguJyVSxp5U3CblRLH67J8gkfyGNbiK3c+l1QI/G4irHMPQw44aEPmjVag+YKTyQ260NcF82GTQ3bdscA==", + "peerDependencies": { + "chart.js": "^4.1.1", + "react": "^16.8.0 || ^17.0.0 || ^18.0.0" + } + }, "node_modules/react-dom": { "version": "18.3.1", "resolved": "https://registry.npmjs.org/react-dom/-/react-dom-18.3.1.tgz", diff --git a/package.json b/package.json index 7cf3ab1..f9e94e3 100644 --- a/package.json +++ b/package.json @@ -10,10 +10,12 @@ }, "dependencies": { "@tailwindcss/aspect-ratio": "^0.4.2", + "chroma-js": "^2.4.2", "jose": "^5.3.0", "js-cookie": "^3.0.5", "next": "14.2.3", "react": "^18", + "react-chartjs-2": "^5.2.0", "react-dom": "^18", "react-select": "^5.8.0" }, diff --git a/src/app/no-access/page.jsx b/src/app/no-access/page.jsx index df86866..62db665 100644 --- a/src/app/no-access/page.jsx +++ b/src/app/no-access/page.jsx @@ -8,6 +8,8 @@ export default function NoAccess() { useEffect(() => { const interval = setInterval(() => { + // never allow countdown to go below 0 + if (countdown === 0) return; setCountdown(prevCountdown => prevCountdown - 1); }, 1000); diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx index 4112e77..79c3de1 100644 --- a/src/app/ui/Header.jsx +++ b/src/app/ui/Header.jsx @@ -4,6 +4,8 @@ import Link from 'next/link'; import isAuthenticatedSSR from "@/app/lib/isAuthenticatedSSR"; import LogoutButton from "@/app/ui/LogoutButton"; import Burger from './Burger'; +import Image from "next/image"; +import logo from "@/static/image/teamwill_logo.png"; const Header = async () => { @@ -11,20 +13,21 @@ const Header = async () => { console.log('isAuth', isAuth) console.log('sessionData', sessionData) return ( -
    +
    - -

    TeamBook

    + + logo +

    TeamBook

    return ( ); }; diff --git a/src/app/ui/SideBar.css b/src/app/ui/SideBar.css new file mode 100644 index 0000000..b471219 --- /dev/null +++ b/src/app/ui/SideBar.css @@ -0,0 +1,47 @@ +/*.relative::before {*/ +/* content: '';*/ +/* position: absolute;*/ +/* top: 50%;*/ +/* left: 0;*/ +/* height: 100%;*/ +/* width: 2px;*/ +/* background-color: #a6b764; !* Tailwind class 'sushi-400' color *!*/ +/* transform: translateY(-50%);*/ +/*}*/ + +/*.relative::after {*/ +/* content: '';*/ +/* position: absolute;*/ +/* top: 50%;*/ +/* left: -1px;*/ +/* width: 8px;*/ +/* height: 8px;*/ +/* background-color: #a6b764; !* Tailwind class 'sushi-400' color *!*/ +/* border-radius: 50%;*/ +/* transform: translate(-50%, -50%);*/ +/*}*/ + +input[type="radio"] { + appearance: none; + width: 16px; + height: 16px; + border: 2px solid #888888; /* Tailwind class 'sushi-400' color */ + border-radius: 50%; + position: relative; +} + +input[type="radio"]:checked { + border-color: #a6b764; /* Tailwind class 'sushi-400' color */ +} + +input[type="radio"]:checked::before { + content: ''; + position: absolute; + top: 50%; + left: 50%; + width: 9px; + height: 9px; + background-color: #a6b764; /* Tailwind class 'sushi-400' color */ + border-radius: 50%; + transform: translate(-50%, -50%); +} \ No newline at end of file diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index 04c5c04..659c856 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -6,6 +6,8 @@ import LogoutButton from "./LogoutButton"; import isAuthenticated from "@/app/lib/isAuthenticated"; import {useEffect, useState} from "react"; import { usePathname } from "next/navigation"; +import './SideBar.css'; + const SideBar = () => { const [isAuth, setIsAuth] = useState(false); const [sessionData, setSessionData] = useState(null); @@ -24,7 +26,7 @@ const SideBar = () => { if (!isAuth || !sessionData) { return (