diff --git a/.idea/misc.xml b/.idea/misc.xml index 639900d13c6182e452e33a3bd638e70a0146c785..6e86672130aac0e4918a028246f39139ae5a99c5 100644 --- a/.idea/misc.xml +++ b/.idea/misc.xml @@ -1,4 +1,3 @@ - diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index a47d118550b459a157d8b8878de6a0cb8a859cd5..71714c0ee4cc878749e3f6bfed9db93c2337e971 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/place/CreateNewPlace.jsx b/src/app/place/CreateNewPlace.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8036b0a0a4ee1a6020c82522f941ac346eb23e74 --- /dev/null +++ b/src/app/place/CreateNewPlace.jsx @@ -0,0 +1,108 @@ +"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/place/RowPlace.jsx b/src/app/place/RowPlace.jsx new file mode 100644 index 0000000000000000000000000000000000000000..acb7a5beb3aab54b3c3c5e840ae8aa5207719ce8 --- /dev/null +++ b/src/app/place/RowPlace.jsx @@ -0,0 +1,226 @@ +"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 RowPlace = ({ id, numero, table, placesState, tables }) => { + //states + const [isUpdating, setIsUpdating] = useState(false) + const [numPlace, setNumPlace] = useState(numero) + const [selectedTable, setSelectedTable] = useState(table) + 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(() => { + setNumPlace(numero) + setSelectedTable(table?.id) + selectRef.current.value = table?.id + inputRef.current.value = numero + }, [numero, table]) + + const handleUpdatePlace = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/places/${id}/`, { + method: "PATCH", + body: JSON.stringify({ numero: numPlace, id_table: selectedTable }) + }) + 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) + 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, + message: "La place a été modifiée avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le numéro de la place déjà existe.", + visible: true, + }) + }else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le numéro de la place saisie existe déjà.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la place", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "La place 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/places/${id}/`, { method: "DELETE" }) + if (isSuccess) { + placesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "La place a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "La place n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + + const cancelUpdate = () => { + setIsUpdating(false) + setNumPlace(numero) + setSelectedTable(table.id) + selectRef.current.value = table.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("placeRowSVG")) 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( + + + 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' /> + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer la place "${numero}"?`} + /> + + ) +} + + +export default RowPlace \ No newline at end of file diff --git a/src/app/place/page.jsx b/src/app/place/page.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9c7b955558dfa27217bd75209b12b7843346d23a --- /dev/null +++ b/src/app/place/page.jsx @@ -0,0 +1,110 @@ +"use client" +import React from 'react' +import fetchRequest from '../lib/fetchRequest' +import { useState, useEffect } from 'react'; +import Loader from '@/components/Loader/Loader' +import CreateNewPlace from './CreateNewPlace' +import { isArray } from '../lib/TypesHelper' +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([]) + + + // 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 getAllPlaces = async () => { + try{ + const {isSuccess, errors, data} = await fetchRequest('/zoaning/places/', {method: 'GET'}) + if(isSuccess){ + setPlaces(data) + }else{ + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + getAllPlaces() + getAllTables() + setIsLoadingData(false) + }, []) + + + const handleSearchingPlace = async (e) => { + const numero= e.target.value + try{ + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/search/place/${numero}`, {method: 'GET'}) + console.log(data) + if(isSuccess){ + setPlaces(data) + }else{ + setPlaces([]) + } + }catch(error){ + console.log(error) + } + } + + return( +
+
+
+ {!isLoadingData ? + <> + +
+

List des Places

+
+
+ +
+ {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 ? +
+ + + + + + + {places?.map((element) => { + return + })} +
TableTable-Zone-EtageAction
+
+ : +
+

Pas encore des places

+
} + + : +
+ } +
+
+
+ + ) +} + +export default Place \ No newline at end of file diff --git a/src/app/planning/PlanningTable.jsx b/src/app/planning/PlanningTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4ddc99b741dcfdbafb870ec1d7a94ed81f1c2d3e --- /dev/null +++ b/src/app/planning/PlanningTable.jsx @@ -0,0 +1,66 @@ +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() + }, []); + + return ( +
+ + + + + + + + + + + + + {data.map((row, rowIndex) => ( + + + {row.days.map((day, dayIndex) => ( + + ))} + + ))} + +
Semaine / JourJ1J2J3J4J5
{row.week} + +
+
+ ); +}; + +export default PlanningTable; diff --git a/src/app/planning/layout.jsx b/src/app/planning/layout.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6386ecbf10b56086248866048fab55809866386e --- /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 0000000000000000000000000000000000000000..42a6408e254349ea6db8a7494b09c90699758185 --- /dev/null +++ b/src/app/planning/page.jsx @@ -0,0 +1,96 @@ +'use client'; +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 [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 [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 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); + }; + + const handleTypePresenceChange = (weekIndex, dayIndex, value) => { + const updatedData = [...planningData]; + updatedData[weekIndex].days[dayIndex] = value; + setPlanningData(updatedData); + }; + + return ( +
+

Planning

+
+ +
+ {/*project using select*/} + + +
+ +
+
+ ); +}; + +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 new file mode 100644 index 0000000000000000000000000000000000000000..83f4dfcc51e1e13352713e98f6c86c8bddfa5b51 --- /dev/null +++ b/src/app/planning/type-presence-semaines-jours/EntityForm.jsx @@ -0,0 +1,78 @@ +import { useState, useEffect } from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import {useNotification} from "@/context/NotificationContext"; + +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) { + fetchData(); + } else { + setNom(''); + } + }, [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) { + toggleNotification({ + visible: true, + message: `${nom} a été ${id ? 'modifié' : 'créé'} avec succès`, + type: "success" + }) + setNom(''); + onSaved(); + onCancel(); + } + }; + + 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 0000000000000000000000000000000000000000..a829627bd3e8d27b61e7ec801211939711058be8 --- /dev/null +++ b/src/app/planning/type-presence-semaines-jours/EntityList.jsx @@ -0,0 +1,61 @@ +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); + }; + + return ( +
+ + + + + + + + + {items.map(item => ( + + + + + ))} + +
NomActions
{item.nom} + + +
+ + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Are you sure you want to delete this ${title.toLowerCase()} "${entityToDelete?.nom}"?`} + /> +
+ ); +}; + +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 new file mode 100644 index 0000000000000000000000000000000000000000..c29923c376a4f4b8b348331bd333698179f01078 --- /dev/null +++ b/src/app/planning/type-presence-semaines-jours/page.jsx @@ -0,0 +1,100 @@ +"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/privilege/PrivilegeTableRow.jsx b/src/app/privilege/PrivilegeTableRow.jsx index 96f1d1db285eaef898daecd57aaf7905de03eaab..7537e45773778a694de9fbe0655fdb37d9a04980 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 ( - - - 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 + ?
+ + +
+ :
+ + +
+ } + + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer l'habilitation ?`} + /> + ) } diff --git a/src/app/projects/ProjectForm.jsx b/src/app/projects/ProjectForm.jsx index 350d3d03c1aaa1542658222e432d4e8f98facebb..0c4f1d1c4d52d89123f26d592f4f6dbfc749fd24 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 adba7fe07afe8325f9fd5c364a35def57f378807..98cf39773810e75c7a18d3903f26cb6a859d5da8 100644 --- a/src/app/projects/page.jsx +++ b/src/app/projects/page.jsx @@ -1,10 +1,11 @@ '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"; 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/role/CreateRoleForm.jsx b/src/app/role/CreateRoleForm.jsx index 29e177c4b4c3acca9e4b7a9f24036564ddd9883f..546c0c0a863e3f773d39a9c338bddcba18f559b1 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 b6c4ea834dea08ad27505cbad054be5ae92908a0..5c44906d31bcc24da8220a392a1eb4b50e4dd520 100644 --- a/src/app/role/RoleTableRows.jsx +++ b/src/app/role/RoleTableRows.jsx @@ -1,67 +1,16 @@ 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() - 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 [isModalOpen, setModalOpen] = useState(false); + const showDeletePopup = () => { + setModalOpen(true); } const handleDelete = async () => { const { isSuccess, errors, status } = await fetchRequest(`/roles/${id}/`, { method: "DELETE" }) @@ -78,7 +27,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 +42,42 @@ 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 ( - - - 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 - ?
    - - + <> + + +

    {name}

    + + +
    + {privileges?.map((element, index) => { + return
    {element.name}
    + })}
    - :
    - -
    - } - - + + + 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 0000000000000000000000000000000000000000..e980dbf25c93bfccfd131e934585909e7ee45a26 --- /dev/null +++ b/src/app/role/UpdateRoleForm.jsx @@ -0,0 +1,143 @@ +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", + 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) + 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 78c99edfd1f13ef7552c2c5ecf729868dcc3cbd3..411c32f224b7080f45d8f933a48805edd67ee899 100644 --- a/src/app/role/page.jsx +++ b/src/app/role/page.jsx @@ -5,10 +5,15 @@ 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") @@ -34,21 +39,29 @@ const Role = () => {
    - -

    List des Roles

    - {isLoading &&
    } + {openCreatePopup && } + {roleToUpdate && } +
    +

    List des Roles

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

    Pas encore des roles

    - :
    + :
    + - {roles.map((element) => { - return + {roles?.map((element) => { + return })}
    RôleHabilitations Action
    diff --git a/src/app/table/CreateNewTable.jsx b/src/app/table/CreateNewTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..fc2f3cf647b5ae1d91d7ef0550b455bc98169476 --- /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 0000000000000000000000000000000000000000..939d879766957433e271007c4277397f96a944cf --- /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 0000000000000000000000000000000000000000..23ad849e1fc4e9d4af2c669d1976bc3f8b5c0a86 --- /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/ui/Dropdown.js b/src/app/ui/Dropdown.js new file mode 100644 index 0000000000000000000000000000000000000000..53c7dad6fe0e21fba29535fd2bb25be6263b09af --- /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 b219fc4381e545d6cd4e3facfe37c20696fbe65b..3f9f7e0b300c4f399bd3cf96da8480112a6f8780 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 ( - ); 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 diff --git a/src/app/user/CreateUserForm.jsx b/src/app/user/CreateUserForm.jsx new file mode 100644 index 0000000000000000000000000000000000000000..f06c5ca134640ced18395b607c20c204dee24ca6 --- /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 0000000000000000000000000000000000000000..d214f1851c494a1c39ed8432decfdc39e102c944 --- /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 0000000000000000000000000000000000000000..1860ee8ae70d4c6a07ffdcba015f733f2fb85490 --- /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 ( + <> + + +

    {first_name} {last_name}

    + + +

    {email}

    + + +

    {role?.name || ""}

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

      -

      } + {projects?.map((project) => { + return
    • {project.nom}
    • + })} +
    + + +
    + + +
    + + + 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 0000000000000000000000000000000000000000..ff8ef679f0d7958bb6da219df3c92ac8e5740c95 --- /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

    +
    + :
    + + + + + + + + + {users?.map((element) => { + return + })} +
    NomEmailRôleProjectsAction
    +
    + }} +
    +
    +
    + ) +} + +export default UserPage \ No newline at end of file diff --git a/src/app/zone/CreateNewZone.jsx b/src/app/zone/CreateNewZone.jsx index d02c3f50d56ceba253747f29fd72886305f1829e..76b78c6076fb6c1c0d46c89848de0d8aca664fb9 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 5ef81721048e4eb4c8157e7519f79fed7e2cef88..baebd3ac5f5fbc663a5c4c91e7f3931a51d67f14 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 5fc1e535eacee408e3fa04256cdaea7b1ce58138..1e3c0328e37c17844ca3fd5db5b4f0e9cb3011f7 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/add.svg b/src/static/image/svg/add.svg new file mode 100644 index 0000000000000000000000000000000000000000..37eaf783c6b70ff9297a84dfff632105ad288efe --- /dev/null +++ b/src/static/image/svg/add.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/place.svg b/src/static/image/svg/place.svg new file mode 100644 index 0000000000000000000000000000000000000000..cd980fb5ad668846821b605178091cf7f442719e --- /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 0000000000000000000000000000000000000000..0d56955abbb30e580bf87d3f9e05f09d93691805 --- /dev/null +++ b/src/static/image/svg/table.svg @@ -0,0 +1 @@ + \ No newline at end of file