From 72f98bb1e56e8419d158682ebea097826869f604 Mon Sep 17 00:00:00 2001 From: Raed BOUAFIF Date: Thu, 20 Jun 2024 11:23:06 +0100 Subject: [PATCH 1/4] 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 2/4] 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

-
+ +
: "" } 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 4/4] 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