diff --git a/package-lock.json b/package-lock.json index 459b2227cce39d19951c37da3eba4458035b75f7..269508a006a04983c49c570f73acc87423f2e5ed 100644 --- a/package-lock.json +++ b/package-lock.json @@ -20,6 +20,7 @@ }, "devDependencies": { "@svgr/webpack": "^8.1.0", + "@tailwindcss/forms": "^0.5.7", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", @@ -2813,6 +2814,18 @@ "tailwindcss": ">=2.0.0 || >=3.0.0 || >=3.0.0-alpha.1" } }, + "node_modules/@tailwindcss/forms": { + "version": "0.5.7", + "resolved": "https://registry.npmjs.org/@tailwindcss/forms/-/forms-0.5.7.tgz", + "integrity": "sha512-QE7X69iQI+ZXwldE+rzasvbJiyV/ju1FGHH0Qn2W3FKbuYtqp8LKcy6iSw79fVUT5/Vvf+0XgLCeYVG+UV6hOw==", + "dev": true, + "dependencies": { + "mini-svg-data-uri": "^1.2.3" + }, + "peerDependencies": { + "tailwindcss": ">=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", @@ -5836,6 +5849,15 @@ "node": ">=8.6" } }, + "node_modules/mini-svg-data-uri": { + "version": "1.4.4", + "resolved": "https://registry.npmjs.org/mini-svg-data-uri/-/mini-svg-data-uri-1.4.4.tgz", + "integrity": "sha512-r9deDe9p5FJUPZAk3A59wGH7Ii9YrjjWw0jmw/liSbHl2CHiyXj6FcDXDu2K3TjVAXqiJdaw3xxwlZZr9E6nHg==", + "dev": true, + "bin": { + "mini-svg-data-uri": "cli.js" + } + }, "node_modules/minimatch": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.2.tgz", diff --git a/package.json b/package.json index f9e94e36ed36bfa864f43a911327d1743842b629..f52a15c2119037727944b3bb0740a1bd569872de 100644 --- a/package.json +++ b/package.json @@ -21,6 +21,7 @@ }, "devDependencies": { "@svgr/webpack": "^8.1.0", + "@tailwindcss/forms": "^0.5.7", "eslint": "^8", "eslint-config-next": "14.2.3", "postcss": "^8", diff --git a/src/app/(dashboard)/projects/ProjectForm.jsx b/src/app/(dashboard)/projects/ProjectForm.jsx index bb531d52e9639b8047f24d18afc63b4444fb5b18..912651519a5eb757983d682cf6365a2e7dcc5af6 100644 --- a/src/app/(dashboard)/projects/ProjectForm.jsx +++ b/src/app/(dashboard)/projects/ProjectForm.jsx @@ -1,7 +1,10 @@ -import { useState, useEffect } from 'react'; +import React, {useState, useEffect} from 'react'; import fetchRequest from '@/app/lib/fetchRequest'; +import CancelIcon from "@/static/image/svg/cancel.svg"; +import SearchIcon from "@/static/image/svg/search-magnifying-glass.svg"; +import XMarkIcon from "@/static/image/svg/x-mark.svg"; -const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingProject }) => { +const ProjectForm = ({setIsOpen, onAddProject, onEditProject, editingProject, setEditingProject}) => { const [projectName, setProjectName] = useState(''); const [clientName, setClientName] = useState(''); const [dateDebut, setDateDebut] = useState(''); @@ -30,7 +33,7 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr useEffect(() => { if (searchQuery.length > 1) { const fetchUsers = async () => { - const { isSuccess, data } = await fetchRequest(`/search-users/?q=${searchQuery}`); + const {isSuccess, data} = await fetchRequest(`/search-users/?q=${searchQuery}`); if (isSuccess) { setUserSuggestions(data); } @@ -87,111 +90,137 @@ const ProjectForm = ({ onAddProject, onEditProject, editingProject, setEditingPr }; return ( -
-
- - setProjectName(e.target.value)} - 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-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" - /> - {errors.dateFin &&

{errors.dateFin}

} -
-
-
- - setSearchQuery(e.target.value)} - 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 && ( - - )} -
- {userIds.map((user, index) => ( -
+
+
+ { + setEditingProject(null); + setIsOpen(false) + }} + className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600"/> + +

{editingProject ? 'Modifier le projet' : 'Ajouter un projet'}

+
+
+ + setProjectName(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" + required + /> + {errors.projectName && +

{errors.projectName}

} +
+
+ + setClientName(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" + required + /> + {errors.clientName && +

{errors.clientName}

} +
+
+
+
+ + setDateDebut(e.target.value)} + className="w-full rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" + required + /> + {errors.dateDebut &&

{errors.dateDebut}

} +
+
+ + setDateFin(e.target.value)} + className="w-full rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" + /> + {errors.dateFin &&

{errors.dateFin}

} +
+
+
+ +
+ setSearchQuery(e.target.value)} + placeholder="Tapez pour rechercher des utilisateurs..." + className="w-full rounded-md pl-10 px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none" /> - + {/*search icon at the left and X at the right if user already typed*/} + {searchQuery && ( + setSearchQuery('')} + className="absolute right-3 top-1/2 transform -translate-y-1/2 text-neutral-400 cursor-pointer h-6 w-6" + /> + )} +
+ {userSuggestions.length > 0 && ( +
    + {userSuggestions.map(user => ( +
  • !isUserChosen(user) && handleUserSelect(user)} + className={`px-3 py-2 border-b border-chicago-200 ${isUserChosen(user) ? 'bg-neutral-200 cursor-not-allowed' : 'hover:bg-sushi-200/40 cursor-pointer'}`} + > + {/*add tick is chosen*/} + {isUserChosen(user) && '✔ '} + {user.first_name} {user.last_name} ({user.role.name}) {user.email} +
  • + ))} +
+ )} +
+ {userIds.map((user, index) => ( +
+

+ {`${user.first_name} ${user.last_name}`} ({user.role.name})

+ +
+ ))}
- ))} -
- {errors.userIds &&

{errors.userIds}

} + {errors.userIds &&

{errors.userIds}

} +
+ + {/* cancel*/} + {editingProject && ( + + )} +
- - {/* cancel*/} - {editingProject && ( - - )} - +
); }; diff --git a/src/app/(dashboard)/projects/ProjectList.jsx b/src/app/(dashboard)/projects/ProjectList.jsx index 67721cf53b5a616a709dd45c7877432913dcc00a..bdcc4cbd1db604e7dce8f5507e8ae5d3d6d85af0 100644 --- a/src/app/(dashboard)/projects/ProjectList.jsx +++ b/src/app/(dashboard)/projects/ProjectList.jsx @@ -1,11 +1,11 @@ import ConfirmationModal from "@/app/ui/ConfirmationModal"; -import React, { useState } from "react"; +import React, {useState} from "react"; import EditIcon from "@/static/image/svg/edit.svg"; -import DeleteIcon from "@/static/image/svg/delete.svg"; +import DeleteIcon from "@/static/image/svg/trash-bold.svg" -const ProjectList = ({ projects, onEdit, onDelete, onHandlePageUrl }) => { +const ProjectList = ({projects, onEdit, onDelete}) => { const [isModalOpen, setModalOpen] = useState(false); const [projectToDelete, setProjectToDelete] = useState(null); const handleDeleteClick = (project) => { @@ -20,39 +20,47 @@ const ProjectList = ({ projects, onEdit, onDelete, onHandlePageUrl }) => { }; return ( <> - +
- - - - - - + + + + + + + {/**/} + + - {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 clientDate débutDate finStatutActions
{project.nom}{project.users.length}{project.nomClient} + {projects + .sort((a, b) => new Date(b.updated_at) - new Date(a.updated_at)) + .map((project, index) => ( +
{project.nom}{project.users.length}{project.nomClient}{project.dateDebut}{project.dateFin}En cours de réalisation +
-
{/* Confirmation Modal */} diff --git a/src/app/(dashboard)/projects/ProjectsFilter.jsx b/src/app/(dashboard)/projects/ProjectsFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..1820453c1eb74b3cea214beb45bf4e3b94c2c44d --- /dev/null +++ b/src/app/(dashboard)/projects/ProjectsFilter.jsx @@ -0,0 +1,64 @@ +import React, {useState} from 'react' +import SearchIcon from "@/static/image/svg/search.svg" +import ArrowUturnLeft from "@/static/image/svg/arrow-uturn-left.svg" + +const ProjectsFilter = ({setFilter}) => { + const handleChangeFilter = (event) => { + const name = event.target.name + const value = event.target.value + setFilter((filter) => ({...filter, [name]: value})) + } + + return ( +
+

Recherche

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ ) +} + +export default ProjectsFilter \ No newline at end of file diff --git a/src/app/(dashboard)/projects/page.jsx b/src/app/(dashboard)/projects/page.jsx index 230000ed459f7ca8745c677ef6103a005388cdc3..463f4a2e1e8d3cb4e483c1af0b46907f3654144e 100644 --- a/src/app/(dashboard)/projects/page.jsx +++ b/src/app/(dashboard)/projects/page.jsx @@ -1,70 +1,67 @@ 'use client'; import SideBar from "@/app/ui/SideBar"; -import { useEffect, useState } from 'react'; +import React, {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 {PAGINATION_SIZE} from "@/app/lib/constants"; import Pagination from "@/app/ui/Pagination"; -import { useNotification } from "@/context/NotificationContext"; +import {useNotification} from "@/context/NotificationContext"; +import SubSideBar from "@/app/ui/SubSideBar"; +import ProjectsFilter from "@/app/(dashboard)/projects/ProjectsFilter"; +import CreateUserForm from "@/app/(dashboard)/user/CreateUserForm"; +import UpdateUserForm from "@/app/(dashboard)/user/UpdateUserForm"; +import AddIcon from "@/static/image/svg/add.svg"; +import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg"; +import ArrowRightIcon from "@/static/image/svg/chevron-right.svg"; const Projects = () => { - const [pageUrl, setPageUrl] = useState('/projects/pagination/'); const [projects, setProjects] = useState([]); const [editingProject, setEditingProject] = useState(null); const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); const [isFormOpen, setIsFormOpen] = useState(false); // State for accordion - const [searchQuery, setSearchQuery] = useState(''); // State for search query - const [pagination, setPagination] = useState({}); // State for pagination - const { toggleNotification } = useNotification(); - - const fetchProjects = async (url = pageUrl) => { - const { isSuccess, errors, data } = await fetchRequest(url); - if (isSuccess) { - setProjects(data.results); - setPagination({ - currentPage: data.currentPage, - pagesNumber: Math.ceil(data.count / PAGINATION_SIZE), - next: data.next, - previous: data.previous, - count: data.count - }); - setErrors(null); - } else { - console.error("Failed to fetch projects"); - setErrors(errors); - } - }; - - const handleSearch = async (query) => { + const [paginationData, setPaginationData] = useState(null) + const {toggleNotification} = useNotification(); + const [filter, setFilter] = useState({ + name: "", + user_count: "", + name_client: "", + status: "", + date_debut: "", + date_fin: "" + }); + + const fetchProjects = async (pageNumber = 1, signal) => { setLoading(true); - const { isSuccess, errors, data } = await fetchRequest(`/search-projects/?q=${query}`); + const URLQuery = `?page=${pageNumber}&?nom=${(filter.name)}&user_count=${(filter.user_count)}&nomClient=${(filter.name_client)}&dateDebut=${(filter.date_debut)}&dateFin=${(filter.date_fin)}` + const {isSuccess, errors, data} = await fetchRequest(`/projects/pagination/${URLQuery}`, + {signal: signal}) + setLoading(false); if (isSuccess) { setProjects(data.results); - setPagination({ - currentPage: data.currentPage, - pagesNumber: Math.ceil(data.count / PAGINATION_SIZE), - next: data.next, - previous: data.previous, - count: data.count - }); + setPaginationData({pagesNumber: Math.ceil((data.count || 0) / PAGINATION_SIZE), currentPage: pageNumber}) setErrors(null); } else { - console.error("Failed to search projects"); + console.error("Failed to fetch projects"); setErrors(errors); } - setLoading(false); }; useEffect(() => { - fetchProjects(); - }, [pageUrl]); + const controller = new AbortController() + const signal = controller.signal + fetchProjects( 1, signal) + return () => { + controller.abort("fetching another project") + } + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter.name, filter.user_count, filter.name_client, filter.date_debut, filter.date_fin]) 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), }); @@ -89,7 +86,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), }); @@ -121,7 +118,7 @@ const Projects = () => { const handleDeleteProject = async (project) => { setLoading(true); - const { isSuccess, errors } = await fetchRequest(`/projects/${project.id}/`, { + const {isSuccess, errors} = await fetchRequest(`/projects/${project.id}/`, { method: 'DELETE', }); @@ -141,88 +138,78 @@ const Projects = () => { setLoading(false); }; - const toggleForm = () => { - setIsFormOpen(!isFormOpen); // Toggle the form visibility - }; - - const handleSearchInputChange = (e) => { - const query = e.target.value; - setSearchQuery(query); - handleSearch(query); // Trigger search on input change - }; - - const handlePageChange = (url) => { - setPageUrl(url); - }; + const subLinks = [{label: "Utilisateurs", link: "/user"}, {label: "Rôles", link: "/role"}, { + label: "Habilitations", + link: "/privilege" + }, {label: "Projets", link: "/projects"}] return ( -
-
-
-
-

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

- {/* Accordion Toggle Button */} - -
- -
- + <> + + +
+
+

Liste des + Projets

+ +
+ {loading && +
Loading...
} + {!loading && projects.length === 0 ? ( +
+

Pas encore des projets

- - {/* Accordion Content */} - {isFormOpen && ( - - )} - - {/* Errors from request */} - {errors && errors.detail && Object.keys(errors.detail).map((key) => ( -
-
- - - - Something went wrong -
-

- {errors.detail[key]} -

+ ) : ( + <> +
+
- ))} - - - - - -
+
+ +
+ + )}
-
+ {/* Accordion Content */} + {isFormOpen && ( + + )} + + {/* Errors from request */} + {errors && errors.detail && Object.keys(errors.detail).map((key) => ( +
+
+ + + + Something went wrong +
+

+ {errors.detail[key]} +

+
+ ))} + ); }; diff --git a/src/app/ui/Pagination.jsx b/src/app/ui/Pagination.jsx index 6b9d2f027a0844965e5adb624c566acb5997333e..c32f4f91f814c6918dcd42639b3f52409a3fc4f4 100644 --- a/src/app/ui/Pagination.jsx +++ b/src/app/ui/Pagination.jsx @@ -1,52 +1,41 @@ import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" +import React from "react"; -const Pagination = ({ paginationData, onPageChange }) => { +const Pagination = ({paginationData, fetchProjects}) => { 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 <>; - } +
+ {(paginationData) &&
+ {(paginationData.currentPage > 1) && +
fetchProjects( 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

fetchProjects( 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'> - -
- )} -
+ {(paginationData.currentPage !== paginationData.pagesNumber) && +
fetchProjects( 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/arrow-uturn-left.svg b/src/static/image/svg/arrow-uturn-left.svg new file mode 100644 index 0000000000000000000000000000000000000000..790fa27b8465f3ff939f9a2ee951774889ec97a8 --- /dev/null +++ b/src/static/image/svg/arrow-uturn-left.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/search-magnifying-glass.svg b/src/static/image/svg/search-magnifying-glass.svg new file mode 100644 index 0000000000000000000000000000000000000000..fe1f04d008c2ad04aebfc29e9ff549c56fd510c7 --- /dev/null +++ b/src/static/image/svg/search-magnifying-glass.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/x-mark.svg b/src/static/image/svg/x-mark.svg new file mode 100644 index 0000000000000000000000000000000000000000..29f4c9b5555a7b90c947b62ff710be59c4ffc5a8 --- /dev/null +++ b/src/static/image/svg/x-mark.svg @@ -0,0 +1,3 @@ + + + diff --git a/tailwind.config.js b/tailwind.config.js index bb1243c2439aa40da55d352ec149d19dba4b43b2..76042c9bf2c5886f9e45ba9adf69768fc9d9a36a 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -34,6 +34,19 @@ module.exports = { '900': '#3d3d3d', '950': '#262626', }, + 'spindle': { + '50': '#f4f8fb', + '100': '#e9f0f5', + '200': '#cddfea', + '300': '#b2cede', + '400': '#70a4c0', + '500': '#4e89a9', + '600': '#3b6f8e', + '700': '#315973', + '800': '#2c4c60', + '900': '#284052', + '950': '#1b2a36', + }, }, // borderColor : "#93a84c", // shadowColor : {