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)/assign-zone-project/AffectationsFilter.jsx b/src/app/(dashboard)/assign-zone-project/AffectationsFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0fe90257abd8ec687345b3033229257446c0f713 --- /dev/null +++ b/src/app/(dashboard)/assign-zone-project/AffectationsFilter.jsx @@ -0,0 +1,217 @@ +import React, { memo, useEffect, useState } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +import Select from "react-select"; +const AffectationsFilter = memo(function Page({ setFilter, filter, zones, projects }) { + + // { + // "idZone": 1, + // "nomZone": "A", + // "numeroEtage": 5 + // } + + // { + // "nom": "weleaf", + // "id": 1 + // } + console.log("filterr", filter) + const handleResetFilters = (event) => { + const value = event.target.value + setFilter({ semaines: [], jours: [], projects: [], zones: [] }) + } + + const handleWeekChange = (selectedOptions) => { + setFilter({ ...filter, semaines: selectedOptions }) + } + const handleDayChange = (selectedOptions) => { + setFilter({ ...filter, jours: selectedOptions }) + } + const handleProjectChange = (selectedOptions) => { + setFilter({ ...filter, projects: selectedOptions }) + } + const handleZoneChange = (selectedOptions) => { + setFilter({ ...filter, zones: selectedOptions }) + } + return ( +
+

Recherche

+
+
+ + ({ label: "Jour" + element, value: element }))} + styles={ + { + menu: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + menuList: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + control: (provided, state) => ({ + ...provided, + borderColor: state.isFocused ? '#93a84c' : 'rgb(212 212 212)', + borderWidth: "1px", + boxShadow: 'none', + borderRadius: "0.375rem", + minHeight: "2.5rem", + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isSelected ? '#D3E193' : state.isFocused ? '#d9e1b5' : 'white', + color: state.isSelected ? 'black' : 'inherit', + '&:hover': { + backgroundColor: '#d9e1b5', // Tailwind's blue-200 + }, + }), + } + } + /> +
+
+ + ({ label: 'Zone:'+ element.nomZone +' - '+ 'Étage:' +element.numeroEtage , value: element.id }))} + styles={ + { + menu: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + menuList: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + control: (provided, state) => ({ + ...provided, + borderColor: state.isFocused ? '#93a84c' : 'rgb(212 212 212)', + borderWidth: "1px", + boxShadow: 'none', + borderRadius: "0.375rem", + minHeight: "2.5rem", + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isSelected ? '#D3E193' : state.isFocused ? '#d9e1b5' : 'white', + color: state.isSelected ? 'black' : 'inherit', + '&:hover': { + backgroundColor: '#d9e1b5', // Tailwind's blue-200 + }, + }), + } + } + /> +
+
+
+ +
+
+ ) +}) + +export default AffectationsFilter \ No newline at end of file diff --git a/src/app/(dashboard)/assign-zone-project/page.jsx b/src/app/(dashboard)/assign-zone-project/page.jsx index 51c353754c604b197ef490a74857747bda96038f..f3e0a85c02405dc0dd284f6aa272ee568ff2def9 100644 --- a/src/app/(dashboard)/assign-zone-project/page.jsx +++ b/src/app/(dashboard)/assign-zone-project/page.jsx @@ -10,6 +10,9 @@ import DeleteIcon from "@/static/image/svg/delete.svg"; import fetchRequest from "@/app/lib/fetchRequest"; import ConfirmationModal from "@/app/ui/ConfirmationModal"; import CompleteAffectation from './CompleteAffectation'; +import AffectationsFilter from './AffectationsFilter' +import { isArray } from '../../lib/TypesHelper' + @@ -19,12 +22,42 @@ const AffectingZoneProject = () => { const [isOpenCompleteAffectation, setIsOpenCompleteAffectation] = useState(false) const [listProjectsAffected, setListProjectsAffected] = useState([]) const [isLoadingListProjects, setIsLoadingListProjects] = useState(false) - const { toggleNotification } = useNotification() + const {toggleNotification} = useNotification() const [selectedWeek, setSelectedWeek] = useState(null) const [selectedDay, setSelectedDay] = useState(null) const [selectedAffectaionToDelete, setSelectedAffectationToDelete] = useState(null) const [isModalOpen, setModalOpen] = useState(false); const [listProjectsSemiAffected, setListProjectsSemiAffected] = useState([]) + const [ projects, setProjects ] = useState([]) + const [ zones, setZones ] = useState([]) + const [filter, setFilter] = useState({ semaines: [], jours: [], projects: [], zones: [] }); + + useEffect( () => { + const getDistinctsProjectsAndZones = async () => { + try { + const { isSuccess, errors, data } = + await fetchRequest(`/zoaning/distinct-projects-zones/` ,{ method: 'GET' } + ) + if (isSuccess) { + setProjects(data.projects) + setZones(data.zones) + } else { + toggleNotification({ + visible: true, + message: errors[0].message, + type: "error" + }) + } + } catch (error) { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getDistinctsProjectsAndZones() + }, []) useEffect(() => { @@ -73,22 +106,20 @@ const AffectingZoneProject = () => { return result; } - + const getListOfAffectedProjects = async () => { setIsLoadingListProjects(true) try { - if (selectedDay && selectedWeek) { - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/${selectedDay}/${selectedWeek}/`, { method: 'GET' }) - } else if (selectedWeek) { - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByWeek/${selectedWeek}/`, { method: 'GET' }) - } else if (selectedDay) { - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjectsByDay/${selectedDay}/`, { method: 'GET' }) - } else { - var { isSuccess, errors, data } = await fetchRequest(`/zoaning/getListAffectedProjects/`, { method: 'GET' }) - } + var zonesIdsString = filter.zones.map((element) => element.value).toString() + var projectsIdsString = filter.projects.map((element) => element.value).toString() + var semaines = filter.semaines.map((element) => element.value).toString() + var jours = filter.jours.map((element) => element.value).toString() + const { isSuccess, errors, data } = + await fetchRequest(`/zoaning/getListAffectedProjects/?days=${jours}&weeks=${semaines}&projects=${projectsIdsString}&zones=${zonesIdsString}`, + { method: 'GET' } + ) if (isSuccess) { setListProjectsAffected(data) - console.log("this is our ", data); setListProjectsSemiAffected(filterAndGroupProjects(data)) } else { toggleNotification({ @@ -109,7 +140,7 @@ const AffectingZoneProject = () => { } } getListOfAffectedProjects() - }, [selectedDay, selectedWeek]) + }, [filter.jours, filter.semaines, filter.projects, filter.zones]) const handleOpenAssignProject = () => { @@ -218,9 +249,89 @@ const AffectingZoneProject = () => { } - return ( -
+ <> + +
+ {isOpen && } + {(isOpenCompleteAffectation) && } +
+

Liste des Affectations

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

Aucune Affectation n'a été trouvé.

+
+ :
+ + + + + + + + + + + {(listProjectsAffected.map((element, index) => + + {/* */} + + + + + + + + + ))} +
DatePlateauProjetPlaces occupéesNb des personnesPlaces disponibleActions
+ Semaine: {element.semaine} - Jour: {element.jour} + + {element.id_zone.nom}-{element.id_zone.id_etage.numero} + + {element.id_project.nom} + + {element.places_occuper} + + {element.nombre_personnes} + + {element.places_disponibles} + +
handleDeleteClick(element)} class="font-medium text-blue-600 hover:underline">
+
+
+ }} + { + (listProjectsSemiAffected && listProjectsSemiAffected.length) ?
+ +
+ : + "" + } + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Êtes-vous sûr de vouloir supprimer l'affectation "${selectedAffectaionToDelete?.id_project.nom}"?`} + /> +
+ + ); +} + +export default AffectingZoneProject; + + + +{/*
{isOpen && } {(isOpenCompleteAffectation) && } @@ -252,8 +363,8 @@ const AffectingZoneProject = () => {
{(!isLoadingListProjects) ? (listProjectsAffected && listProjectsAffected.length > 0) ? - - +
+ {(listProjectsAffected.map((element, index) => - - {/* */} - + + ))} @@ -329,10 +440,4 @@ const AffectingZoneProject = () => { onConfirm={handleConfirmDelete} message={`Êtes-vous sûr de vouloir supprimer l'affectation "${selectedAffectaionToDelete?.id_project.nom}"?`} /> - - - ); -} - - -export default AffectingZoneProject; + */} diff --git a/src/app/(dashboard)/consultation-reservations/BookedUsersTable.jsx b/src/app/(dashboard)/consultation-reservations/BookedUsersTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ad6fc10791af7e6c3a11f36b5594acf7a99ad42d --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/BookedUsersTable.jsx @@ -0,0 +1,43 @@ +import React from 'react' +import EditIcon from "@/static/image/svg/edit.svg"; +import DeleteIcon from "@/static/image/svg/trash-bold.svg"; + + +const BookedUsersTable = ({bookedPlaces, presence}) => { + return ( + <> +
+ {presence && +
    +
  • Liste des collaborateurs ayant confirmé + leur présence :
  • +
} + {!presence && +
    +
  • Liste des collaborateurs ayant réservé + sans être présents :
  • +
} +
Date @@ -280,9 +391,9 @@ const AffectingZoneProject = () => {
+
Semaine: {element.semaine} - Jour: {element.jour} @@ -301,7 +412,7 @@ const AffectingZoneProject = () => { {element.places_disponibles} -
handleDeleteClick(element)} class="font-medium text-blue-600 dark:text-blue-500 hover:underline">
+
handleDeleteClick(element)} class="font-medium text-blue-600 hover:underline">
+ + + + + + + + + {bookedPlaces.filter((bookedPlace) => bookedPlace.presence === presence).map((bookedPlace, index) => ( + + + + + + ))} + +
NomPrénomProjet
{bookedPlace.first_name}{bookedPlace.last_name}{bookedPlace.project_name}
+
+ + ) +} + +export default BookedUsersTable \ No newline at end of file diff --git a/src/app/(dashboard)/consultation-reservations/ConsultationReservationFilters.jsx b/src/app/(dashboard)/consultation-reservations/ConsultationReservationFilters.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7aaeb5babbfcb192edcc49c6190a9f44638c48de --- /dev/null +++ b/src/app/(dashboard)/consultation-reservations/ConsultationReservationFilters.jsx @@ -0,0 +1,72 @@ +import React, { memo } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +import ArrowUturnLeft from "@/static/image/svg/arrow-uturn-left.svg"; +import SearchIcon from "@/static/image/svg/search.svg"; +const ConsultationReservationFilters = memo(function page({ setFilter }) { + const handleChangeFilter = (event) => { + const name = event.target.name + const value = event.target.value + setFilter((filter) => ({...filter, [name]: value})) + } + return ( +
+

Recherche

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+ +
+
+ + +
+
+ ) +}) + +export default ConsultationReservationFilters \ No newline at end of file diff --git a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx index 0f8bedf525d6a2136f04143d1d06e9b7637fbcba..681767e1691c9aa640b34c6e05498f04200354a1 100644 --- a/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/PlaceUI.jsx @@ -1,57 +1,96 @@ -import React, { useContext, useState } from 'react'; +import React, { useContext, useState, useEffect, useRef } from 'react'; import { ReservationContext } from './page'; +import PlacePresent from "@/static/image/svg/placePresent.svg"; +import PlaceBooked from "@/static/image/svg/placeBooked.svg"; +import PlaceUnavailableIcon from "@/static/image/svg/placeUnavailable.svg"; -const colors = [ - '#FF5733', '#50cc65', '#3357FF', '#c9ce41', '#FF33A8', - '#24c4b8', '#FF8333', '#8333FF', '#3383FF', '#83FF33', - '#FF3383', '#2ac567', '#FF33F3', '#4cb7be', '#F333FF', - '#cdd927', '#FF33A8', '#80d3cd', '#FF8333', '#8333FF', - '#3383FF', '#92d965', '#FF3383', '#4ebb78', '#FF33F3', -]; - -const getColorForProject = (projectId) => { - const index = projectId % colors.length; - return colors[index]; -}; - -const PlaceUI = ({ id, isTop }) => { +const PlaceUI = ({ id, rotate }) => { const { allPlaces, bookedPlaces } = useContext(ReservationContext); const [showTooltip, setShowTooltip] = useState(false); + const [tooltipStyle, setTooltipStyle] = useState({}); + const tooltipRef = useRef(null); const place = allPlaces?.find((place) => place.id === id); const bookedPlace = bookedPlaces?.find((place) => place.id_place === id); - if (place) { - const backgroundColor = getColorForProject(place.project_id); // Assuming place object has a project_id field + const updateTooltipPosition = () => { + if (showTooltip && tooltipRef.current) { + const tooltip = tooltipRef.current; + const parent = tooltip.parentElement; + const { top, left, right, bottom } = tooltip.getBoundingClientRect(); + const { innerWidth, innerHeight } = window; + const parentRect = parent.getBoundingClientRect(); + + let newStyle = { + top: '100%', + left: '50%', + transform: rotate ? 'rotate(180deg) translateX(-50%)' : 'translateX(-50%)', + }; + + if (right > parentRect.right) { + newStyle.left = 'auto'; + newStyle.right = '0'; + newStyle.transform = rotate ? 'rotate(180deg) translateX(-100%)' : 'translateX(-100%)'; + } + + if (left < parentRect.left) { + newStyle.left = '0'; + newStyle.right = 'auto'; + newStyle.transform = rotate ? 'rotate(180deg) translateX(0)' : 'translateX(0)'; + } + + setTooltipStyle(newStyle); + } + }; + + useEffect(() => { + updateTooltipPosition(); + }, [showTooltip]); + + const Tooltip = ({ bookedPlace }) => ( +
+

Nom: {bookedPlace.first_name}

+

Prénom: {bookedPlace.last_name}

+

Rôle: {bookedPlace.role}

+

Statut : {bookedPlace.presence ? "Présent" : ""}

+

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

+
+ ); + + if (!place) { return ( -
setShowTooltip(true)} - onMouseLeave={() => setShowTooltip(false)} - > -

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

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

Nom: {bookedPlace.first_name}

-

Prénom: {bookedPlace.last_name}

-

Role: {bookedPlace.role}

-

Presence: {bookedPlace.presence}

-

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

-
- )} +
+
+
+
+
+
); - } else { - return ( -
- ); } + + const PlaceContent = () => ( +
setShowTooltip(true)} + onMouseLeave={() => setShowTooltip(false)}> +
+

{place.project_name || ""}

+
+
+
+
+ {bookedPlace?.presence ? : } + {bookedPlace && ( +
+ )} + {showTooltip && bookedPlace && } +
+ ); + + return ; }; export default PlaceUI; diff --git a/src/app/(dashboard)/consultation-reservations/TableUI.jsx b/src/app/(dashboard)/consultation-reservations/TableUI.jsx index ce702a2af8182dceaa21dc700b0ad20d3e6ee9fe..79d8f7718a4174752cd5027589d0ad40f71f0fd7 100644 --- a/src/app/(dashboard)/consultation-reservations/TableUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/TableUI.jsx @@ -1,7 +1,7 @@ import React from 'react' import PlaceUI from './PlaceUI'; -const TableUI = ({id, numero, places}) => { +const TableUI = ({ places }) => { function groupConsecutive(arr) { arr = arr.sort((a, b) => a.id - b.id) @@ -14,7 +14,8 @@ const TableUI = ({id, numero, places}) => { while (counter < arr.length) { if (counter + 1 < arr.length) { grouped.push([arr[counter], arr[counter + 1]]) - } else { + } + else { grouped.push([arr[counter]]) } counter += 2; @@ -22,27 +23,20 @@ const TableUI = ({id, numero, places}) => { return grouped; } - const processedPlaces = groupConsecutive(places).reverse() if (!processedPlaces || processedPlaces.length === 0) return <> return ( -
+
{processedPlaces.map((element, index) => { - return
-
- + return
+
+ +
+
+ {(element.length > 1) && }
-
- {(element.length > 1) &&
- -
}
})}
diff --git a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx index 3ea1b5c54124d4f5667af17bb637dd64d32a4a3a..e7d99428f1b432080e2ca2b1f080206916c7bacc 100644 --- a/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx +++ b/src/app/(dashboard)/consultation-reservations/ZoneUI.jsx @@ -3,13 +3,10 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return ( -
-

Zone {nom}

-
+
{tables.map((table) => { - return - })} -
+ return + })}
) } diff --git a/src/app/(dashboard)/consultation-reservations/page.jsx b/src/app/(dashboard)/consultation-reservations/page.jsx index 253e302e1b4215f1469ff8f844917e1944471119..a1ce7ba0ae266e40c204a52e2284fe445b3414d1 100644 --- a/src/app/(dashboard)/consultation-reservations/page.jsx +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -1,34 +1,35 @@ 'use client' -import React, { useEffect, useState, useRef } from 'react' +import React, {useEffect, useState, useRef} from 'react' import ZoneUI from './ZoneUI' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' -import { useNotification } from '@/context/NotificationContext' +import {useNotification} from '@/context/NotificationContext' +import ArrowUturnLeft from "@/static/image/svg/arrow-uturn-left.svg"; +import SearchIcon from "@/static/image/svg/search.svg"; +import Select from "react-select"; +import BookedUsersTable from "@/app/(dashboard)/consultation-reservations/BookedUsersTable"; export const ReservationContext = React.createContext() const Reservation = () => { const [isLoadingData, setIsLoadingData] = useState(false) - const { toggleNotification } = useNotification() - const [date, setDate] = useState({ day: null, week: null }) + const {toggleNotification} = useNotification() + const [date, setDate] = useState({day: null, week: null}) const [isLoadingSelectsData, setIsLoadingSelectsData] = useState(true) - const [floors, setFloors] = useState([]) const [projectsData, setProjectsData] = useState([]) + const [filteredProjectsData, setFilteredProjectsData] = useState([]) const [currentDateData, setCurrentDateData] = useState(null) const [datesData, setDatesData] = useState(null) const [bookedPlaces, setBookedPlaces] = useState([]) - const [ etages, setEtages ] = useState([]) - const [ isLoadingEtages, setIsLoadingEtages ] = useState(true) - const [ selectedEtage, setSelectedEtage ] = useState(null) - const [ zones, setZones ] = useState([]) - const [ isLoadingZones, setIsLoadingZones ] = useState(true) - const [ selectedZone, setSelectedZone ] = useState(null) + const [floors, setFloors] = useState([]) + const [selectedZone, setSelectedZone] = useState({floorId: null, zoneId: null}) + const [NameProjectFilter, setNameProjectFilter] = useState("") useEffect(() => { const getPlan = async () => { try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/etage-zone-table-place/') + const {isSuccess, errors, data} = await fetchRequest('/zoaning/etage-zone-table-place/') setIsLoadingSelectsData(false) if (isSuccess) { setFloors(data) @@ -47,42 +48,22 @@ const Reservation = () => { getPlan() }, []) - useEffect(() => { - const getAllEtages = async () => { - try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) - setIsLoadingEtages(false) - if (isSuccess) { - setEtages(data) - } else { - setEtages([]) - } - } catch (error) { - setIsLoadingEtages(false) - console.log(error) - } - } - getAllEtages() - }, []) - - useEffect(() => { - if(selectedEtage){ - const concernedEtage = floors.find((element) => element.id == selectedEtage) - setZones(concernedEtage.zones) - } - }, [selectedEtage]) - - console.log(floors) - console.log("selectedEtage", selectedEtage) - console.log("zones", zones) - - useEffect(() => { const getSyncData = async () => { - const getUserPlan = async () => { - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/projects-zones-places/${date.week}/${date.day}`) + const getProjectsData = async () => { + const { + isSuccess, + errors, + data + } = await fetchRequest(`/zoaning/projects-zones-places/${date.week}/${date.day}`) if (isSuccess) { setProjectsData(data) + setFilteredProjectsData(data) + if (data && data.length && data[0].zones.length) setSelectedZone({ + floorId: data[0].zones[0].id_etage.id, + zoneId: data[0].zones[0].id + }) + else setSelectedZone({floorId: null, zoneId: null}) } else { console.log(errors) toggleNotification({ @@ -93,12 +74,21 @@ const Reservation = () => { } } const getBookedPlaces = async () => { - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/reservations/date/${date.date}/`) + const {isSuccess, errors, data} = await fetchRequest(`/zoaning/reservations/date/${date.date}/`) if (isSuccess) { console.log("booked places : :", data) - setBookedPlaces(data.map((element) => ({ ...element, id_place: element.id_place.id, presence: element.presence, id_user: element.id_user.id, created_at: element.created_at, first_name: element.id_user.first_name, last_name: element.id_user.last_name, role: element.id_user.role.name }))) - } - else { + setBookedPlaces(data.map((element) => ({ + ...element, + id_place: element.id_place.id, + presence: element.presence, + id_user: element.id_user.id, + created_at: element.created_at, + first_name: element.id_user.first_name, + last_name: element.id_user.last_name, + role: element.id_user.role.name, + project_name: element.id_user.projects[0].nom + }))) + } else { console.log(errors) toggleNotification({ visible: true, @@ -108,12 +98,13 @@ const Reservation = () => { } } setIsLoadingData(true) - await Promise.all([getUserPlan(), getBookedPlaces()]) + await Promise.all([getProjectsData(), getBookedPlaces()]) setIsLoadingData(false) } if (date.week && date.day) getSyncData() else { setProjectsData([]) + setFilteredProjectsData([]) setBookedPlaces([]) } @@ -121,7 +112,7 @@ const Reservation = () => { useEffect(() => { const getCurrentDateData = async () => { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/current-date/'); + const {isSuccess, errors, data} = await fetchRequest('/zoaning/current-date/'); if (isSuccess) { setCurrentDateData(data) } else { @@ -133,7 +124,7 @@ const Reservation = () => { } } const YearCalendar = async () => { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/dates/'); + const {isSuccess, errors, data} = await fetchRequest('/zoaning/dates/'); if (isSuccess) { console.log("dates data", data) setDatesData(data) @@ -157,48 +148,43 @@ const Reservation = () => { const dateSelected = JSON.parse(event.target.value); console.log("weekMonthly", dateSelected.weekMonthly); console.log("day", dateSelected.day); - setDate({ day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date }) - setSelectedEtage(null) - setSelectedZone(null) - } - else { - setDate({ day: null, week: null }) - setSelectedEtage(null) - } - } - - const handleChangeEtage = (event) =>{ - const etage_id = event.target.value - if(selectedEtage !== etage_id){ - setSelectedEtage(etage_id) - setSelectedZone(null) - setSelectedZone(null) + setDate({day: dateSelected.day, week: dateSelected.weekMonthly, date: dateSelected.date}) + // setSelectedEtage(null) + // setSelectedZone({floorId: null, zoneId: null}) + } else { + setDate({day: null, week: null}) + // setSelectedEtage(null) } } - const handleChangeZone = (event) =>{ - const zone_id = event.target.value - if(selectedZone !== zone_id){ - setSelectedZone(zone_id) - } - } - const concernedZones = projectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] + const concernedZones = filteredProjectsData?.map((element) => element.zones.map((zone) => zone.id)).flatMap((element) => element) || [] const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] - const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] + const allPlaces = filteredProjectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ + ...place, + project_name: project.project_name, + project_id: project.project_id + })))).flat(2) || [] const dateRef = useRef(null) useEffect(() => { if (dateRef.current && currentDateData?.date) { dateRef.current.value = JSON.stringify(currentDateData) - setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) + setDate({day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date}) } }, [dateRef.current, currentDateData?.date]) + useEffect(() => { + if (NameProjectFilter) { + setFilteredProjectsData(projectsData.filter((element) => element.project_name.toLowerCase().includes(NameProjectFilter.toLowerCase()))) + } else { + setFilteredProjectsData(projectsData) + } + }, [NameProjectFilter]) if (isLoadingSelectsData) return
- +
const currentMonth = new Date().getMonth() + 1 @@ -209,82 +195,263 @@ const Reservation = () => { return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) }) + const selectedZoneData = floors.find((element) => element.id == selectedZone.floorId)?.zones.find((zone) => zone.id === selectedZone.zoneId) + + const handleProjectChange = (selectedOption) => { + const selectedProjects = selectedOption.map((element) => element.value) + setFilteredProjectsData(projectsData.filter((element) => selectedProjects.includes(element.project_id))) + } + console.log(floors.find((element) => element.id == selectedZone.floorId)?.image) return ( -
-
- {(filteredDatesData && filteredDatesData.length) && } - - + <> +
+ {floors.map((element, index) => { + if (element.id !== selectedZone.floorId) + return
setSelectedZone({floorId: element.id, zoneId: element.zones[0].id})} + className='cursor-pointer px-3 pb-2 h-full flex items-center justify-center'> +

Etage {element.numero}

+
+ return
setSelectedZone({floorId: element.id, zoneId: element.zones[0].id})} + className='cursor-pointer px-3 pb-2 h-full relative flex items-center justify-center before:content-[""] before:absolute before:bottom-0 before:left-0 before:bg-sushi-500 before:w-full before:h-0.5'> +

Etage {element.numero}

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

Etage {floor.numero}

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

Recherche

+
+
+ + { + // event.target.value to int + setSelectedZone({floorId: parseInt(event.target.value), zoneId: null}) + }} + disabled={!(date.day && date.week) || isLoadingData} + name="etage" + id='etage' + className='rounded-md px-3 w-full duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-300 outline-none'> + + + {floors.map((etage, index) => ( + + ))} + +
+
+ + +
+
+ +
+ + +
+
+
+
+

Consultation des + réservations

+ {(filteredDatesData && filteredDatesData.length) && + } +
+ {(selectedZone.floorId && !isLoadingData) && +
+ {!(floors.find((element) => element.id == selectedZone.floorId).image) ? +
+
+
+ + Aucune Illustration fournie pour cet étage +
+
+
+ : +
+ element.id == selectedZone.floorId).image} + className='w-full [aspect-ratio:2.5]'> +
+ } + {(!isLoadingData) && +
+ {/*selectedZone.floorId*/} + {floors.filter((element) => element.id == selectedZone.floorId).map((floor, index) => { + return floor.zones?.map((zone, index) => { + if (selectedZone.zoneId == zone.id && selectedZone.floorId == floor.id) return
setSelectedZone({floorId: floor.id, zoneId: zone.id})} + key={index} + className='rounded-md not-first:-left-2 text-white relative whitespace-nowrap flex items-center justify-center border border-[#B2CEDE] text-xs md:text-sm lg:text-base px-5 lg:px-10 h-9 bg-[#B2CEDE]'> +

Etage {floor.numero} - Zone {zone.nom}

+
+ return
setSelectedZone({ + floorId: floor.id, + zoneId: zone.id + })} + className='cursor-pointer not-first:-left-2 hover:bg-neutral-100 duration-300 rounded-md text-[#8E8D8D] relative whitespace-nowrap border flex items-center justify-center border-[#D9D9D9] text-xs md:text-sm lg:text-base lg:px-10 px-5 h-9 bg-white'> +

Etage {floor.numero} - Zone {zone.nom}

}) - : - floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { - return
-

Etage {floor.numero}

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

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

-
} - + })} +
} +
} +
+
+
+
+

Disponible

+
+
+
+

Réservé

+
+
+
+

Présent

+
- - :
- + {(!isLoadingData) + ? + + <> +
+ {(selectedZoneData) && +
+ +
} + {(!isLoadingData && !selectedZoneData) + &&
+

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

+
} +
+ +
+ :
+ +
+ }
- } -
+
+ + +
+
+ ) } -export default Reservation \ No newline at end of file +export default Reservation diff --git a/src/app/(dashboard)/etage/AddEtageComponent.jsx b/src/app/(dashboard)/etage/AddEtageComponent.jsx deleted file mode 100644 index aaf361f797a0a86cf95d9f66d384a46cf468b1c5..0000000000000000000000000000000000000000 --- a/src/app/(dashboard)/etage/AddEtageComponent.jsx +++ /dev/null @@ -1,69 +0,0 @@ -'use client' -import Loader from '@/components/Loader/Loader' -import React, { useState, useRef } from 'react' -import fetchRequest from '../../lib/fetchRequest' -import { useNotification } from '@/context/NotificationContext' - - - - -const AddEtageComponent = ({ etagesState }) => { - const [numeroEtage, setNumeroEtage] = useState("") - const [isLoading, setIsLoading] = useState(false) - const inputRef = useRef(null) - const { toggleNotification } = useNotification() - - - - const handleNewEtage = (e) => { - setNumeroEtage(e.target.value) - } - - const handleAddNewEtage = async () => { - setIsLoading(true) - const { data, errors, isSuccess } = await fetchRequest("/zoaning/etages/", { - method: "POST", - body: JSON.stringify({ numero: numeroEtage }) - }) - if (isSuccess) { - setIsLoading(false) - inputRef.current.value = "" - console.log(data) - etagesState((prevEtagesState) => [...prevEtagesState, data]); - toggleNotification({ - type: "success", - message: "L'étage a été créer avec succès.", - visible: true, - }) - } else { - setIsLoading(false) - if (errors.type == "ValidationError") - toggleNotification({ - type: "warning", - message: "Le numéro détage déja existe.", - visible: true, - }) - else { - toggleNotification({ - type: "error", - message: "Une erreur s'est produite lors de la création de la zone.", - visible: true, - }) - } - console.log(errors) - } - - } - - return ( -
- Nouveau étage: -
- -
- -
- ) -} - -export default (AddEtageComponent) \ No newline at end of file diff --git a/src/app/(dashboard)/etage/CreateEtage.jsx b/src/app/(dashboard)/etage/CreateEtage.jsx new file mode 100644 index 0000000000000000000000000000000000000000..4e27ebc279ca63b9aaf88aa8e7b5577adde6608e --- /dev/null +++ b/src/app/(dashboard)/etage/CreateEtage.jsx @@ -0,0 +1,209 @@ +/* eslint-disable @next/next/no-img-element */ +'use client' +import Loader from '@/components/Loader/Loader' +import React, { useRef, useState, useEffect } from 'react' +import fetchRequest from '../../lib/fetchRequest' +import { useNotification } from "@/context/NotificationContext"; +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import ImageIcon from "@/static/image/svg/image.svg" +import ColoredImageIcon from "@/static/image/svg/image2.svg" +import CrossIcon from "@/static/image/svg/cancel.svg" + +const CreateEtage = ({ appendEtage, cancelCreate }) => { + const [isLoading, setIsLoading] = useState(false) + const [numeroEtage, setNumeroEtage] = useState(""); + const { toggleNotification } = useNotification() + const handlePrivilegeNameChange = (event) => { + setNumeroEtage(event.target.value) + } + const inputRef = useRef(null) + const imageInput = useRef(null) + const [image, setImage] = useState(null) + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoading(true) + const formData = new FormData() + formData.append("numero", numeroEtage) + if (image) formData.append("image", image) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/etages/", { + method: "POST", + body: formData, + }) + if (isSuccess) { + setIsLoading(false) + appendEtage(data) + inputRef.current.value = "" + setNumeroEtage("") + toggleNotification({ + visible: true, + message: "L'étage a été créé avec succès", + type: "success" + }) + cancelCreate(); + } else { + setIsLoading(false) + if (errors.type === "ValidationError") { + if (errors.detail.image) { + toggleNotification({ + type: "warning", + message: "Le type de l'image n'est pas supporté", + visible: true, + }) + } + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le numéro d'étage existe déjà", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de l'étage", + type: "warning" + }) + } + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + } + + 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("etageRowCreationSVG")) return; + if (!isInsideRowRef) { + cancelCreate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + useEffect(() => { + if (inputRef?.current) { + inputRef.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, []) + + + const removeImage = () => { + imageInput.current.value = ""; + setImage(null); + setIsDisplayingImage(false) + }; + + const handleChangeImage = (e) => { + if (imageInput.current.value !== "") { + const supportedType = ["image/jpg", "image/jpeg", "image/png"]; + if (supportedType.includes(imageInput.current.files[0].type)) { + setImage(imageInput.current.files[0]); + } else { + removeImage(); + setIsDisplayingImage(false) + toggleNotification({ + type: "warning", + message: "Le type de l'image n'est pas supporté", + visible: true + }) + } + } else { + setImage(null); + setIsDisplayingImage(false) + } + }; + const [isDisplayingImage, setIsDisplayingImage] = useState(false) + const viewImage = () => { + setIsDisplayingImage(true) + } + + return + + + + + {(!image) && <> + + } + + + + {(image) && <> +
+
+
+ +
+
+

{image.name}

+

{Math.round((image.size / (1024 * 1024)) * 100) / 100}Mo

+
+
+

Voir l'image

+
+ {(isDisplayingImage) &&
+
+
setIsDisplayingImage(false)} className='ml-auto relative bottom-3'> + +
+
+ User uploaded +
+
+
} +
+
+ +
+
+
+
+ } + + +
+ + +
+ + +} + +export default CreateEtage \ No newline at end of file diff --git a/src/app/(dashboard)/etage/EtageTableRow.jsx b/src/app/(dashboard)/etage/EtageTableRow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..eb44b096e9415a0ade6ec91efee3e11ad30d4a27 --- /dev/null +++ b/src/app/(dashboard)/etage/EtageTableRow.jsx @@ -0,0 +1,270 @@ +/* eslint-disable @next/next/no-img-element */ +'use client' +import React, { useEffect, useRef, useState } from 'react' +import fetchRequest from '../../lib/fetchRequest' +import Loader from '@/components/Loader/Loader' +import DeleteIcon from "@/static/image/svg/trash-bold.svg" +import EditIcon from "@/static/image/svg/edit-bold.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '../../ui/ConfirmationModal' +import CrossIcon from "@/static/image/svg/cancel.svg" +import ColoredImageIcon from "@/static/image/svg/image2.svg" +import ImageIcon from "@/static/image/svg/image.svg" + +const EtageTableRow = ({ id, numero, setEtages, image: etageImage }) => { + const [numeroEtage, setNumeroEtage] = useState(numero) + const [loadingStatus, setLoadingStatus] = useState(false) + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false) + const [isUpdating, setIsUpdating] = useState(false) + const [image, setImage] = useState(etageImage) + const imageInput = useRef(null) + const showDeletePopup = () => { + setModalOpen(true); + } + useEffect(() => { + setNumeroEtage(numero) + setImage(etageImage) + inputRef.current.value = numero + }, [numero, etageImage]) + const handleUpdateEtage = async () => { + setLoadingStatus(true) + const formData = new FormData() + formData.append("numero", numeroEtage) + if (!image) formData.append("image", "") + else if (image instanceof Object) { + formData.append("image", image) + } + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/etages/${id}/`, { + method: "PATCH", + body: formData + }) + setLoadingStatus(false) + if (isSuccess) { + setEtages((etages) => etages.map((element) => element.id === id ? data.data : element)) + setIsUpdating(false) + toggleNotification({ + visible: true, + message: "L'étage a été modifié avec succès.", + type: "success" + }) + } else { + if (errors.type === "ValidationError") { + if (errors.detail.name) { + toggleNotification({ + type: "warning", + message: "Le numéro de l'étage existe déjà", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de l'étage", + type: "warning" + }) + } + } else if (status === 404) { + toggleNotification({ + visible: true, + message: "L'étage n'a pas été trouvé", + type: "warning" + }) + } else { + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + console.log(errors) + } + + } + + const removeImage = () => { + imageInput.current.value = ""; + setImage(null); + setIsDisplayingImage(false) + }; + const handleChangeImage = (e) => { + if (imageInput.current.value !== "") { + const supportedType = ["image/jpg", "image/jpeg", "image/png"]; + if (supportedType.includes(imageInput.current.files[0].type)) { + setImage(imageInput.current.files[0]); + } else { + removeImage(); + setIsDisplayingImage(false) + toggleNotification({ + type: "warning", + message: "Le type de l'image n'est pas supporté", + visible: true + }) + } + } else { + setImage(null); + setIsDisplayingImage(false) + } + }; + const handleDelete = async () => { + const { isSuccess, errors, status } = await fetchRequest(`/zoaning/etages/${id}/`, { method: "DELETE" }) + if (isSuccess) { + setEtages((etages) => etages.filter((element) => element.id !== id)) + toggleNotification({ + visible: true, + message: "L'étage a été supprimée avec succès", + type: "success" + }) + } else if (status == 404) { + toggleNotification({ + visible: true, + message: "L'étage n'a pas été trouvé", + type: "warning" + }) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + setModalOpen(false) + } + 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("etageRowSVG")) return; + if (!isInsideRowRef) { + cancelUpdate(); + document.removeEventListener("click", handleUpdateBlur); + } + } + const [isDisplayingImage, setIsDisplayingImage] = useState(false) + const viewImage = () => { + setIsDisplayingImage(true) + } + const cancelUpdate = () => { + setIsUpdating(false) + setNumeroEtage(numero) + setImage(etageImage) + inputRef.current.value = numero + } + + var imageAttributs = image + ? image instanceof Object + ? { name: image.name, size: image.size } + : { name: image.split("/")[image.split("/").length - 1] } + : null + return ( + <> + + + setNumeroEtage(event.target.value)} defaultValue={numero} type='text' className='w-full min-w-80 border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 disabled:bg-transparent duration-100 h-14 outline-none' /> + + + {(isDisplayingImage && image) &&
+
+
setIsDisplayingImage(false)} className='ml-auto relative bottom-3'> + +
+
+ User uploaded +
+
+
} + {isUpdating && + <> + {(!image) && <> + + } + + {(image) && <> +
+
+
+ +
+
+

{decodeURIComponent(imageAttributs.name)}

+ {(imageAttributs.size) &&

{Math.round((imageAttributs.size / (1024 * 1024)) * 100) / 100}Mo

} +
+
+

Voir l'image

+
+
+
+ +
+
+
+
+ } + } + {(!image && !isUpdating) &&

+ Aucune image n'est associée à cet étage. +

} + {(image && !isUpdating) &&
+

Voir l'image

+
} + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + + setModalOpen(false)} + onConfirm={handleDelete} + message={`Voulez-vous vraiment supprimer l'étage ?`} + /> + + ) + +} + +export default EtageTableRow \ No newline at end of file diff --git a/src/app/(dashboard)/etage/EtagesFilter.jsx b/src/app/(dashboard)/etage/EtagesFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..406e6e3cdbaa2d7b5d0f35b5f162525646883319 --- /dev/null +++ b/src/app/(dashboard)/etage/EtagesFilter.jsx @@ -0,0 +1,31 @@ +import React, { memo } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +const EtagesFilter = memo(function page({ setFilter, filter }) { + const handleChangeFilter = (event) => { + const name = event.target.name + const value = event.target.value + setFilter((filter) => ({ ...filter, [name]: value })) + } + const handleResetFilters = () => { + setFilter({ etage: "" }) + } + return ( +
+

Recherche

+
+
+ + +
+
+
+ +
+
+ ) +}) + +export default EtagesFilter \ No newline at end of file diff --git a/src/app/(dashboard)/etage/page.jsx b/src/app/(dashboard)/etage/page.jsx index 9d69312dc9f14e1af2e1c403ce13ba840f0006b1..59ac933a3f717494a0eb7b4b39991dc001ea7a2b 100644 --- a/src/app/(dashboard)/etage/page.jsx +++ b/src/app/(dashboard)/etage/page.jsx @@ -1,125 +1,64 @@ -"use client" -import React from 'react' -import AddEtageComponent from './AddEtageComponent' +'use client' +import React, { useEffect, useState } from 'react' +import CreateEtage from './CreateEtage' import fetchRequest from '../../lib/fetchRequest' -import { useState, useEffect } from 'react'; +import { isArray } from '../../lib/TypesHelper' +import EtageTableRow from './EtageTableRow' import Loader from '@/components/Loader/Loader' -import { useNotification } from '@/context/NotificationContext' -import ConfirmationModal from "@/app/ui/ConfirmationModal"; - - - - +import EtagesFilter from "./EtagesFilter" +import AddIcon from "@/static/image/svg/add.svg" const Etage = () => { const [etages, setEtages] = useState([]) - const [isLoadingData, setIsLoadingData] = useState(true) - const { toggleNotification } = useNotification() - const [isModalOpen, setModalOpen] = useState(false); - const [etageToDelete, setEtageToDelete] = useState(null); - - - // Fetch data from external API + const [isLoading, setIsLoading] = useState(true) + const [filter, setFilter] = useState({ etage: "" }); + const [isCreating, setIsCreating] = useState(false) useEffect(() => { - const getAllEtages = async () => { - try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) - setIsLoadingData(false) - if (isSuccess) { - setEtages(data) - } else { - setEtages([]) - } - } catch (error) { - setIsLoadingData(false) - console.log(error) - } - } - getAllEtages() - }, []) - const handleDeleteEtage = async (etage) => { - try { - console.log(etage) - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/etages/${etage.id}/`, { method: "DELETE" }) - console.log(isSuccess) - console.log(errors) - console.log(data) + const getEtages = async () => { + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/etages/?etage_numero=${filter.etage}`) + setIsLoading(false) if (isSuccess) { - console.log(etages) - console.log("etage: ", etage) - setEtages((prevEtages) => prevEtages.filter((e) => e.id !== etage.id)); - toggleNotification({ - type: "success", - message: "L'étage a été supprimer avec succès.", - visible: true, - }) + setEtages(data) } else { - toggleNotification({ - type: "error", - message: "Une erreur s'est produite lors de la suppression de l'étage.", - visible: true, - }) + console.log(errors) } - } catch (error) { - toggleNotification({ - type: "error", - message: "Internal Server Error", - visible: true, - }) } + getEtages() + }, [filter.etage]) + const appendEtage = (etage) => { + setEtages((data) => [etage, ...data]) } - - const handleDeleteClick = (etage) => { - setEtageToDelete(etage); - setModalOpen(true); - } - - const handleConfirmDelete = () => { - handleDeleteEtage(etageToDelete); - setModalOpen(false); - setProjectToDelete(null); - }; - return ( -
-
-
-

Liste des étages

-
-
- {(!isLoadingData) ? etages && etages?.length ?
    - {etages?.map((etage, index) => - ( -
  • - Etage numéro: {etage.numero} -
    { handleDeleteClick(etage) }}> - -
    -
  • - ) - )} -
: -
- Aucun étage n'a été ajouté -
- : -
- -
- } - -
-
- + <> + +
+
+

Liste des étages

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

Aucun étage n'a été trouvée.

+
+ :
+ + + + + + + {(isCreating) && setIsCreating(false)} appendEtage={appendEtage} />} + {etages?.map((element) => { + return + })} +
Numéro ÉtageL'image de l'étageActions
+
+ }}
- setModalOpen(false)} - onConfirm={handleConfirmDelete} - message={`Êtes-vous sûr de vouloir supprimer l'étage numéro "${etageToDelete?.numero}"?`} - /> -
- + ) } diff --git a/src/app/(dashboard)/layout.jsx b/src/app/(dashboard)/layout.jsx index a0a0e46e3ddfef34b87ece119a33310a2241c8f6..828318ce06a1ba3512626bdb9569823d89fdc724 100644 --- a/src/app/(dashboard)/layout.jsx +++ b/src/app/(dashboard)/layout.jsx @@ -2,14 +2,16 @@ import React from 'react' import SideBar from '../ui/SideBar' import Header from '../ui/Header' import Footer from '../ui/Footer' +import SubSideBar from '../ui/SubSideBar' const layout = ({ children }) => { return ( <>
- -
+
+ + {children}
diff --git a/src/app/(dashboard)/place/CreatePlace.jsx b/src/app/(dashboard)/place/CreatePlace.jsx new file mode 100644 index 0000000000000000000000000000000000000000..84218a06c9d530172e942efd1ec2d25fdec32a8c --- /dev/null +++ b/src/app/(dashboard)/place/CreatePlace.jsx @@ -0,0 +1,177 @@ +import Loader from '@/components/Loader/Loader' +import React, { useState, useRef, useEffect, useMemo } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +const CreatePlace = ({ setPlaces, setIsOpen, etages, zones, tables }) => { + const [ nbrPlacesToCreate, setNbrPlacesToCreate ] = useState(null) + const [ selectedZone, setSelectedZone ] = useState(null) + const [ selectedEtage, setSelectedEtage ] = useState(null) + const [ selectedTable, setSelectedTable ] = useState(null) + const { toggleNotification } = useNotification() + const [isLoadingAction, setIsLoadingAction] = useState(false) + + const inputRef = useRef(null) + const selectRefZones = useRef(null) + const selectRefEtages = useRef(null) + const selectedRefTables = useRef(null) + + + // create component files + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/places/", { + method: "POST", + body: JSON.stringify({ nbrPlaces: nbrPlacesToCreate, id_table: selectedTable.id }) + }) + if (isSuccess) { + setIsLoadingAction(false) + setPlaces(prevPlaceState => [ + ...prevPlaceState, + ...data.map(place => { + const foundTable = tables.find(table => table.id == place.id_table); + return { + ...place, + id_table: { + ...place.id_table, + id_zone: { id: foundTable.zone.id, nom: foundTable.zone.nom, id_etage: { id: foundTable.etage.id, numero: foundTable.etage.numero } }, + id: foundTable.id, + numero: foundTable.numero + } + }; + }) + ]); + setNbrPlacesToCreate(null) + setSelectedTable(null) + setSelectedEtage(null) + setSelectedZone(null) + setIsOpen(false) + toggleNotification({ + visible: true, + message: "La table a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + setNbrPlacesToCreate(null) + setSelectedTable(null) + setSelectedEtage(null) + setSelectedZone(null) + setIsOpen(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 table change + const handleEtageSelection = (event) => { + const etageId = event.target.value + if(selectedEtage != etageId){ + const selectedEtage = etages.find(etage => etage.id == etageId) + setSelectedEtage(selectedEtage) + setSelectedZone(null) + setSelectedTable(null) + } + } + + // Handle the name of zone change + const handleZoneSelection = (event) => { + const zoneId = event.target.value + if(selectedZone != zoneId){ + const selectedZone = zones.find(zone => zone.id == zoneId) + setSelectedZone(selectedZone) + setSelectedTable(null) + } + } + + const handleTableSelection = (event) => { + const tableId = event.target.value + if(selectedZone != tableId){ + const selectedTable = tables.find(table => table.id == tableId) + setSelectedTable(selectedTable) + } + } + + + const handleNbrPlaceChange = (event) => { + const nbrPlace = event.target.value + setNbrPlacesToCreate(nbrPlace) + } + + return ( +
+
+ {(true) ? +
+

Ajouter table

+
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+ + +
+
+
+ : +
} +
+
+ ) +} + +export default CreatePlace \ No newline at end of file diff --git a/src/app/(dashboard)/place/PlaceRow.jsx b/src/app/(dashboard)/place/PlaceRow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d9136324a830fe3ae1500ec28e2e2feb2b5fb0c9 --- /dev/null +++ b/src/app/(dashboard)/place/PlaceRow.jsx @@ -0,0 +1,293 @@ +import React, { useState, useRef, useEffect } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import DeleteIcon from "@/static/image/svg/trash-bold.svg" +import EditIcon from "@/static/image/svg/edit-bold.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '@/app/ui/ConfirmationModal' +import Loader from '@/components/Loader/Loader' + + +const PlaceRow = ({ id, numero, id_table, placeState, etages, zones, tables }) => { + const [isUpdating, setIsUpdating] = useState(false) + const [numPlace, setNumPlace] = useState(numero) + const [selectedZone, setSelectedZone] = useState(id_table.id_zone.id) + const [selectedEtage, setSelectedEtage] = useState(id_table.id_zone.id_etage.id) + const [selectedTable, setSelectedTable] = useState(id_table.id) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRefZone = useRef(null) + const selectRefEtage = useRef(null) + const selectRefTable = useRef(null) + const rowRef = useRef(null) + + + //Logic + useEffect(() => { + setNumPlace(numero) + setSelectedTable(id_table?.id) + setSelectedZone(id_table?.id_zone?.id) + setSelectedEtage(id_table?.id_zone?.id_etage?.id) + selectRefTable.current.value = id_table?.id + selectRefZone.current.value = id_table?.id_zone?.id + selectRefEtage.current.value = id_table?.id_zone?.id_etage?.id + inputRef.current.value = numero + }, [numero, id_table]) + + const handleUpdateZone = 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) { + if (data.message === "NO_CHANGES") { + toggleNotification({ + visible: true, + message: "Aucun changement n'a été effectué.", + type: "warning" + }) + setIsUpdating(false) + return + } + console.log("retunred data ", data) + const concernedTableData = tables.find(table => table.id === data.data.id_table) + placeState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: {...concernedTableData, id_zone: {...concernedTableData.zone, id_etage: concernedTableData.etage}} } : 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.nom) { + toggleNotification({ + type: "warning", + message: "Le numéro de la place existe déjà", + visible: true, + }) + } else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nuémro de la place saisie déjà existe.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la zone", + 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) { + placeState((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) + setSelectedEtage(id_table?.id_zone?.id_etage.id) + setSelectedZone(id_table?.id_zone.id) + setSelectedTable(id_table?.id) + selectRefTable.current.value = id_table?.id + selectRefZone.current.value = id_table?.id_zone.id + selectRefEtage.current.value = id_table?.id_zone.id_etage.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 && selectRefTable?.current && selectRefZone?.current && selectRefEtage?.current) { + inputRef.current.focus() + selectRefZone.current.focus() + selectRefEtage.current.focus() + selectRefTable.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + // Handle the numero of etage change + const handleEtageSelection = (event) => { + const etageId = event.target.value + if(selectedEtage != etageId){ + setSelectedEtage(etageId) + setSelectedZone("") + setSelectedTable("") + } + } + + // Handle the name of zone change + const handleZoneSelection = (event) => { + const zoneId = event.target.value + console.log("zoneID", zoneId) + if(selectedZone != zoneId){ + setSelectedZone(zoneId) + setSelectedTable("") + } + } + + // Handle the numero of table change + const handleTableSelection = (event) => { + const tableId = event.target.value + console.log("zoneID", tableId) + if(selectedTable != tableId){ + setSelectedTable(tableId) + } + } + + + return ( + <> + + + setNumPlace(event.target.value)} defaultValue={numero} type='text' className='border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none bg-transparent' /> + + + + + + + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Voulez-vous vraiment supprimer la place ?`} + /> + + + ) +} + +export default PlaceRow \ No newline at end of file diff --git a/src/app/(dashboard)/place/PlacesFilter.jsx b/src/app/(dashboard)/place/PlacesFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..01a903856145191aad8dce8431a88524111e6373 --- /dev/null +++ b/src/app/(dashboard)/place/PlacesFilter.jsx @@ -0,0 +1,174 @@ +import React, { memo, useEffect, useState } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +import Select from "react-select"; +const PlacesFilter = memo(function Page({ setFilter, filter, etages, zones, tables }) { + + const handleChangePlaceNumero = (event) => { + const value = event.target.value + setFilter({ ...filter, place: value }) + } + const handleTableChange = (selectedOptions) => { + setFilter({ ...filter, tables: selectedOptions }) + } + const handleEtageChange = (selectedOptions) => { + setFilter({ ...filter, etage: selectedOptions.value, zone: "", tables: [] }) + } + + const handleZonesChange = (selectedOptions) => { + setFilter({ ...filter, zone: selectedOptions.value, tables: [] }) + } + + const handleResetFilters = () => { + setFilter({ place: "", etage: "", zone: "", tables: [] }) + } + console.log("filter", filter) + + console.log("zones", zones) + console.log("etages", etages) + return ( +
+

Recherche

+
+
+ + +
+
+ + zone.id === filter.zone)?.nom, value: zones.find(zone => zone.id === filter.zone)?.id} || ""} + name="zones" + onChange={handleZonesChange} + options={etages.find( (etage) => etage.id === filter.etage)?.zones.map((zone) => ({ label: zone.nom, value: zone.id }))} + styles={ + { + menu: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + menuList: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + control: (provided, state) => ({ + ...provided, + borderColor: state.isFocused ? '#93a84c' : 'rgb(212 212 212)', + borderWidth: "1px", + boxShadow: 'none', + borderRadius: "0.375rem", + minHeight: "2.5rem", + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isSelected ? '#D3E193' : state.isFocused ? '#d9e1b5' : 'white', + color: state.isSelected ? 'black' : 'inherit', + '&:hover': { + backgroundColor: '#d9e1b5', // Tailwind's blue-200 + }, + }), + } + } + /> +
+
+ + 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 diff --git a/src/app/(dashboard)/place/page.jsx b/src/app/(dashboard)/place/page.jsx index 68f65d6352d6bc002d969f4f964e7c24536c3636..08c1014fbc4cb4d318103c60db2fde98dd97f708 100644 --- a/src/app/(dashboard)/place/page.jsx +++ b/src/app/(dashboard)/place/page.jsx @@ -1,36 +1,46 @@ -"use client" -import React from 'react' -import fetchRequest from '../../lib/fetchRequest' -import { useState, useEffect, useRef } from 'react'; +'use client' +import React, { useState, useEffect } from 'react' +import CreatePlace from './CreatePlace' import Loader from '@/components/Loader/Loader' +import PlaceRow from './PlaceRow' +import fetchRequest from '@/app/lib/fetchRequest' import { isArray } from '../../lib/TypesHelper' -import RowPlace from './RowPlace' -import PlaceIcon from "@/static/image/svg/place.svg" +import AddIcon from "@/static/image/svg/add.svg" import { useNotification } from '@/context/NotificationContext' - - - +import PlacesFilter from "./PlacesFilter" const Place = () => { const [places, setPlaces] = useState([]) - const [isLoadingData, setIsLoadingData] = useState(true) const [tables, setTables] = useState([]) - const [etages, setEtages] = useState([]) const [zones, setZones] = useState([]) - - const [error, setError] = useState(null) - const [isLoadingAction, setIsLoadingAction] = useState(false) - const [nbrPlacesToCreate, setNbrPlacesToCreate] = useState(null) - const [ selectedTable, setSelectedTable ] = useState(null) - const [ selectedEtage, setSelectedEtage ] = useState(null) - const [ selectedZone, setSelectedZone ] = useState(null) - const [ filteredPlaces, setFilteredPlaces ] = useState([]) - + const [etages, setEtages] = useState([]) + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) const { toggleNotification } = useNotification() + const [filter, setFilter] = useState({ place: "", etage: "", zone: "", tables: [] }); + useEffect(() => { + const getPlaces = async () => { + var tablesIdString = filter.tables.map((element) => element.value).toString() + const etageSelected = filter.etage + const zoneSelected = filter.zone + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/places/?place_numero=${filter.place}&etage_id=${etageSelected}&zone_id=${zoneSelected}&table_ids=${tablesIdString}`) + setIsLoading(false) + if (isSuccess) { + setPlaces(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getPlaces() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter.place, filter.etage, filter.zone, filter.tables]) - - // Fetch data from external API useEffect(() => { function extractFilters(data) { let etagesMap = {}; @@ -75,7 +85,6 @@ const Place = () => { return { etages, zones, tables }; } - const getAllTables = async () => { try { const { isSuccess, errors, data } = await fetchRequest('/zoaning/tables/', { method: 'GET' }) @@ -93,269 +102,43 @@ const Place = () => { console.log(error) } } - const getAllPlaces = async () => { - try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/places/', { method: 'GET' }) - if (isSuccess) { - setPlaces(data) - setFilteredPlaces(data) - } else { - setPlaces([]) - setFilteredPlaces([]) - } - } 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' }) - if (isSuccess) { - setPlaces(data) - setFilteredPlaces(data) - } else { - setPlaces([]) - setFilteredPlaces([]) - } - } catch (error) { - console.log(error) - } - } - - - // create component files - const handleSubmit = async (event) => { - event.preventDefault() - setIsLoadingAction(true) - const { data, errors, isSuccess } = await fetchRequest("/zoaning/places/", { - method: "POST", - body: JSON.stringify({ nbrPlaces: nbrPlacesToCreate, id_table: selectedTable.id }) - }) - if (isSuccess) { - setIsLoadingAction(false) - setPlaces(prevPlaceState => [ - ...prevPlaceState, - ...data.map(place => { - const foundTable = tables.find(table => table.id == place.id_table); - return { - ...place, - id_table: { - ...place.id_table, - id_zone: { id: foundTable.zone.id, nom: foundTable.zone.nom, id_etage: { id: foundTable.etage.id, numero: foundTable.etage.numero } }, - id: foundTable.id, - numero: foundTable.numero - } - }; - }) - ]); - setFilteredPlaces(prevPlaceState => [ - ...prevPlaceState, - ...data.map(place => { - const foundTable = tables.find(table => table.id == place.id_table); - return { - ...place, - id_table: { - ...place.id_table, - id_zone: { id: foundTable.zone.id, nom: foundTable.zone.nom, id_etage: { id: foundTable.etage.id, numero: foundTable.etage.numero } }, - id: foundTable.id, - numero: foundTable.numero - } - }; - }) - ]); - setNbrPlacesToCreate(null) - setSelectedTable(null) - setSelectedEtage(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 handleEtageSelection = (event) => { - const etageId = event.target.value - setFilteredPlaces(places.filter(place => place.id_table.id_zone.id_etage.id == etageId)) - if(selectedEtage != etageId){ - const selectedEtage = etages.find(etage => etage.id == etageId) - setSelectedEtage(selectedEtage) - setZones(selectedEtage.zones) - // Extract and format tables from the selected étage's zones - const updatedTables = selectedEtage.zones.flatMap(zone => - zone.tables.map(table => ({ - ...table, - zone: { - id: zone.id, - nom: zone.nom - }, - etage: { - id: selectedEtage.id, - numero: selectedEtage.numero - } - })) - ); - setTables(updatedTables) - setSelectedZone(null) - setSelectedTable(null) - } - } - - const handleZoneSelection = (event) => { - const zoneId = event.target.value - setFilteredPlaces(places.filter(place => place.id_table.id_zone.id == zoneId)) - if(selectedZone != zoneId){ - const refreshTables = selectedEtage.zones.flatMap(zone => - zone.tables.map(table => ({ - ...table, - zone: { - id: zone.id, - nom: zone.nom - }, - etage: { - id: selectedEtage.id, - numero: selectedEtage.numero - } - })) - ); - const concernedZone = zones.find(zone => zone.id == zoneId) - setSelectedZone(concernedZone) - const updatedTables = concernedZone.tables.map(table => { - return refreshTables.find(originalTable => originalTable.id === table.id); - }); - setTables(updatedTables) - setSelectedTable(null) - } - } - const handleTableSelection = (event) => { - const tableId = event.target.value - setFilteredPlaces(places.filter(place => place.id_table.id == tableId)) - if(selectedZone != tableId){ - const selectedTable = tables.find(table => table.id == tableId) - setSelectedTable(selectedTable) - } - } - - const handleNbrPlaceChange = (event) => { - const nbrPlace = event.target.value - setNbrPlacesToCreate(nbrPlace) - } - - return ( -
-
-
- {!isLoadingData ? - <> -
-

Ajout d'une place

-
-
- - -
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-

Liste 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(filteredPlaces) && filteredPlaces?.length != 0 && isArray(tables) && tables?.length != 0 ? -
- - - - - - - {filteredPlaces?.map((element) => { - return - })} -
PlaceTable-Zone-EtageAction
-
- : -
-

Pas encore des places

-
} - - : -
- } + <> + +
+ {openCreatePopup && } +
+

Liste des Places

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

Aucune Place n'a été trouvé.

+
+ :
+ + + + + + + + + {places?.map((element) => { + return + })} +
PlaceTableZoneÉtageActions
+
+ }}
-
- + ) } diff --git a/src/app/(dashboard)/planning/PlanningTable.jsx b/src/app/(dashboard)/planning/PlanningTable.jsx index 0db1822080d008a6a286261b3dfbb57f361cd64f..7b2fea1f5941e8e53298e6fcb2aadcdfc65e4dd8 100644 --- a/src/app/(dashboard)/planning/PlanningTable.jsx +++ b/src/app/(dashboard)/planning/PlanningTable.jsx @@ -1,26 +1,144 @@ import React, {useEffect, useState} from 'react'; import fetchRequest from "@/app/lib/fetchRequest"; import {useNotification} from "@/context/NotificationContext"; +import {blankData} from "@/app/lib/constants"; +import ConfirmationModal from "@/app/ui/ConfirmationModal"; +import ArrowUturnLeft from "@/static/image/svg/arrow-uturn-left.svg"; +import DeleteIcon from "@/static/image/svg/trash-bold.svg"; +import EditIcon from "@/static/image/svg/edit.svg"; -const PlanningTable = ({ data, typePresences, onTypePresenceChange, selectedProject }) => { +const PlanningTable = ({ project, typePresences}) => { // fetch type presence const [errors, setErrors] = useState(); const [loading, setLoading] = useState(false); const {toggleNotification} = useNotification() const [selectedPresence, setSelectedPresence] = useState(''); + const [planningData, setPlanningData] = useState(project.planning ? structuredClone(project.planning) : blankData); + const [isModalOpen, setModalOpen] = useState(false); + const handleSave = async () => { + // verify if planing data has an empty value + const emptyValue = planningData.planning_data.some(week => week.days.some(day => day.nom === '')); + if (emptyValue) { + toggleNotification({ + visible: true, + message: 'Veuillez remplir toutes les valeurs de planification', + type: 'error' + }); + return; + } + setLoading(true); + const requestBody = {id_project: project.id, planning_data: planningData.planning_data}; + console.log({id_project: project.id, planning_data: planningData.planning_data}) + const {isSuccess, errors,data} = await fetchRequest(`/plannings/`, { + method: 'POST', + body: JSON.stringify(requestBody) + }); + setLoading(false); + if (isSuccess) { + toggleNotification({ + visible: true, + message: 'Données de planification enregistrées avec succès', + type: 'success' + }); + setPlanningData(data); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Échec de lenregistrement des données de planification', + type: 'error' + }); + } + }; + + const handleUpdate = async () => { + setLoading(true); + const requestBody = {id_project: project.id, planning_data: planningData.planning_data}; + const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { + method: 'PUT', + body: JSON.stringify(requestBody) + }); + setLoading(false); + if (isSuccess) { + toggleNotification({ + visible: true, + message: 'Données de planification mises à jour avec succès', + type: 'success' + }); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Échec de la mise à jour des données de planification', + type: 'error' + }); + } + } + + const handleDelete = async () => { + setLoading(true); + // delete by planning id not project id + const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { + method: 'DELETE' + }); + setLoading(false); + if (isSuccess) { + setPlanningData(blankData); + toggleNotification({ + visible: true, + message: 'Données de planification supprimées avec succès', + type: 'success' + }); + } else { + setErrors(errors); + toggleNotification({ + visible: true, + message: 'Échec de la suppression des données de planification', + type: 'error' + }); + } + }; + const handleDeleteClick = () => { + setModalOpen(true); + }; + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + const handleTypePresenceChange = (weekIndex, dayIndex, value) => { + // const updatedData = {...planningData}; + // get the presence type by id (value is a string convert it to int + const typePresence = typePresences.find(typePresence => typePresence.id === parseInt(value)); + + // Shallow Copy not working cause i need to get previous data + // const updatedData = {...planningData}; + // Deep Copy working + const updatedData = structuredClone(planningData); + updatedData.planning_data[weekIndex].days[dayIndex] = typePresence; + setPlanningData(updatedData); + }; const handleRadioChange = (e) => { const newPresence = e.target.value; setSelectedPresence(newPresence); - data.forEach((row, rowIndex) => { + + // Use a deep copy to update the entire planning data + const updatedData = structuredClone(planningData); + + updatedData?.planning_data.forEach((row, rowIndex) => { row.days.forEach((day, dayIndex) => { - onTypePresenceChange(rowIndex, dayIndex, newPresence); + // Update the presence type directly + const typePresence = typePresences.find(typePresence => typePresence.id === parseInt(newPresence)); + updatedData.planning_data[rowIndex].days[dayIndex] = typePresence; }); }); + + // Set the updated planning data + setPlanningData(updatedData); }; return ( -
+
Réglez Tout sur : {typePresences.map((typePresence) => ( @@ -38,27 +156,27 @@ const PlanningTable = ({ data, typePresences, onTypePresenceChange, selectedProj ))}
- - - - - - - - + + + + + + + + - {data.map((row, rowIndex) => ( - - + {planningData?.planning_data.map((row, rowIndex) => ( + + {row.days.map((day, dayIndex) => ( -
Semaine/JourJ1J2J3J4J5
Semaine/JourLundi (J1)Mardi (J2)Mercredi (J3)Jeudi (J4)Vendredi (J5)
Semaine {row.week}
Semaine {row.week} +
+ {/* crud for buttons*/} +
+ + {planningData.id ? + <> + + + : + + } + +
+ + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Etes-vous sûr de vouloir supprimer ces données de planification ?`} + />
); }; diff --git a/src/app/(dashboard)/planning/page.jsx b/src/app/(dashboard)/planning/page.jsx index f6dc6d33ac762cd5301da62de4c97a9ae1ac4785..e2237139c66fc0ede6705b67a7c5891851d1758a 100644 --- a/src/app/(dashboard)/planning/page.jsx +++ b/src/app/(dashboard)/planning/page.jsx @@ -1,34 +1,27 @@ 'use client'; -import { useEffect, useState } from 'react'; +import React, {useEffect, useState} from 'react'; import Dropdown from "@/app/ui/Dropdown"; import PlanningTable from "@/app/(dashboard)/planning/PlanningTable"; import fetchRequest from "@/app/lib/fetchRequest"; -import { useNotification } from "@/context/NotificationContext"; +import {useNotification} from "@/context/NotificationContext"; import ConfirmationModal from "@/app/ui/ConfirmationModal"; +import ArrowUturnLeft from "@/static/image/svg/arrow-uturn-left.svg"; +import SearchIcon from "@/static/image/svg/search.svg"; +import AddChart from "@/static/image/svg/addchart.svg"; +import TypePresence from "@/app/(dashboard)/planning/type-presence/TypePresence"; +import Accordion from "@/app/ui/Accordion"; +import {blankData} from "@/app/lib/constants"; const PlanningPage = () => { - const blankData = { - planning_data: [ - { week: 1, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, - { week: 2, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, - { week: 3, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, - { week: 4, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, - { week: 5, days: [{"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}, {"id": 2, "nom": "Teletravail"}] }, - ] - } const [projects, setProjects] = useState([]); - const [selectedProject, setSelectedProject] = useState(''); - const [planningData, setPlanningData] = useState(blankData); + const [selectedProject, setSelectedProject] = useState({}); const [errors, setErrors] = useState(); - const [loading, setLoading] = useState(false); - const { toggleNotification } = useNotification(); const [typePresences, setTypePresences] = useState([]); - const [isModalOpen, setModalOpen] = useState(false); - + const [openPopup, setOpenPopup] = useState(false); const fetchProjects = async () => { - const { isSuccess, errors, data } = await fetchRequest('/projects/'); + const {isSuccess, errors, data} = await fetchRequest('/projects/'); if (isSuccess) { setProjects(data); setErrors(null); @@ -42,34 +35,17 @@ const PlanningPage = () => { fetchProjects(); }, []); - const handleProjectChange = (e) => setSelectedProject(e.target.value); - - const fetchPlanningData = async () => { - if (!selectedProject) return; - - // const { isSuccess, errors, data } = await fetchRequest(`/planning/?project=${selectedProject}`); - const { isSuccess, errors, data } = await fetchRequest(`/plannings/project/${selectedProject}/`); - if (isSuccess) { - // if the project have no data the response is [] in this case we should set the blankData - if (data.length === 0) { - setPlanningData(blankData); - setErrors(null); - return; - } - setPlanningData(data[0]); - setErrors(null); - } else { - console.error("Failed to fetch planning data"); - setPlanningData(blankData) - setErrors(errors); + const handleProjectChange = (e) => { + if (e.target.value === '') { + setSelectedProject({}); + return; } - }; - useEffect(() => { - fetchPlanningData(); - }, [selectedProject]); + const project = projects.find(project => project.id == e.target.value); + setSelectedProject(project); + } const fetchTypePresences = async () => { - const { isSuccess, errors, data } = await fetchRequest('/type-presences/'); + const {isSuccess, errors, data} = await fetchRequest('/type-presences/'); if (isSuccess) { setTypePresences(data); setErrors(null); @@ -82,169 +58,79 @@ const PlanningPage = () => { fetchTypePresences() }, []); - const handleTypePresenceChange = (weekIndex, dayIndex, value) => { - const updatedData = { ...planningData }; - // get the presence type by id (value is a string convert it to int - const typePresence = typePresences.find(typePresence => typePresence.id === parseInt(value)); - console.log(typePresence) - - // the value I want to add should be {"id": 1, "nom": "Travail à Temps Partiel"} and not just the id - updatedData.planning_data[weekIndex].days[dayIndex] = typePresence; - setPlanningData(updatedData); - console.log(value) - console.log(updatedData); - }; - - const handleSave = async () => { - if (!selectedProject) { - toggleNotification({ - visible: true, - message: 'Veuillez sélectionner un projet', - type: 'error' - }); - return; - } - // verify if planing data has an empty value - const emptyValue = planningData.planning_data.some(week => week.days.some(day => day.nom === '')); - if (emptyValue) { - toggleNotification({ - visible: true, - message: 'Veuillez remplir toutes les valeurs de planification', - type: 'error' - }); - return; - } - setLoading(true); - const requestBody = { id_project: selectedProject, planning_data: planningData.planning_data }; - console.log({ id_project: selectedProject, planning_data: planningData.planning_data }) - const { isSuccess, errors } = await fetchRequest(`/plannings/`, { - method: 'POST', - body: JSON.stringify(requestBody) - }); - setLoading(false); - if (isSuccess) { - toggleNotification({ - visible: true, - message: 'Données de planification enregistrées avec succès', - type: 'success' - }); - fetchPlanningData() - } else { - setErrors(errors); - toggleNotification({ - visible: true, - message: 'Échec de lenregistrement des données de planification', - type: 'error' - }); - } - }; - - const handleUpdate = async () => { - setLoading(true); - const requestBody = {id_project: selectedProject, planning_data: planningData.planning_data}; - const {isSuccess, errors} = await fetchRequest(`/plannings/${planningData.id}/`, { - method: 'PUT', - body: JSON.stringify(requestBody) - }); - setLoading(false); - if (isSuccess) { - toggleNotification({ - visible: true, - message: 'Données de planification mises à jour avec succès', - type: 'success' - }); - } else { - setErrors(errors); - toggleNotification({ - visible: true, - message: 'Échec de la mise à jour des données de planification', - type: 'error' - }); - } - } - - const handleDelete = async () => { - setLoading(true); - // dlete by planning id not project id - const { isSuccess, errors } = await fetchRequest(`/plannings/${planningData.id}/`, { - method: 'DELETE' - }); - setLoading(false); - if (isSuccess) { - setPlanningData(blankData); - toggleNotification({ - visible: true, - message: 'Données de planification supprimées avec succès', - type: 'success' - }); - } else { - setErrors(errors); - toggleNotification({ - visible: true, - message: 'Échec de la suppression des données de planification', - type: 'error' - }); - } - }; - const handleDeleteClick = () => { - setModalOpen(true); - }; - - const handleConfirmDelete = () => { - handleDelete(); - setModalOpen(false); - }; - return ( -
-

Planning

- -
- + <> +
+

Recherche

+
+
+ + +
+
+
+ + +
- {/* crud buttons*/} -
- - {planningData.id ? - <> - - - : - - } - +
+
+

Planning

+ +
+ {/*iterate for projects */} +
+ {selectedProject.id ? ( +
+ +
+ +
+
+
+ ) : projects.length === 0 ? ( +
+

Pas encore des projets

+
+ ) : ( + projects.map(project => ( +
+ +
+ +
+
+
+ )) + )} +
- setModalOpen(false)} - onConfirm={handleConfirmDelete} - message={`Etes-vous sûr de vouloir supprimer ces données de planification ?`} - /> -
+ {openPopup && } + ); }; diff --git a/src/app/(dashboard)/planning/type-presence/EntityForm.jsx b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx index 408e3a41c2afc4009945d83e6f1d7db4930c3f42..3397d1e53fd1c9984f3e2bba89b07027c2a1f333 100644 --- a/src/app/(dashboard)/planning/type-presence/EntityForm.jsx +++ b/src/app/(dashboard)/planning/type-presence/EntityForm.jsx @@ -1,4 +1,4 @@ -import { useState, useEffect } from 'react'; +import React, { useState, useEffect } from 'react'; import fetchRequest from "@/app/lib/fetchRequest"; import {useNotification} from "@/context/NotificationContext"; @@ -44,31 +44,32 @@ const EntityForm = ({ entity, id, 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" + 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 />
-
+
diff --git a/src/app/(dashboard)/planning/type-presence/EntityList.jsx b/src/app/(dashboard)/planning/type-presence/EntityList.jsx index d2f5d9e4b0f588a3eb15f1e4ce221f9ccf4b3d99..4086efa4e0971c1943b5a5fdfe964e44304408a9 100644 --- a/src/app/(dashboard)/planning/type-presence/EntityList.jsx +++ b/src/app/(dashboard)/planning/type-presence/EntityList.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react'; import ConfirmationModal from '@/app/ui/ConfirmationModal'; 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 EntityList = ({ title, items, setState, handleDelete, handleEdit }) => { const [isModalOpen, setModalOpen] = useState(false); @@ -20,38 +20,35 @@ const EntityList = ({ title, items, setState, handleDelete, handleEdit }) => { return (
- - - - - - - - - {items.map(item => ( - - - - - ))} - -
NomActions
{item.nom} - - -
+
+ {items.map((item, index) => ( +
+

{item.nom}

+
+ {item.nom === 'Presentiel' || item.nom ==="Teletravail" ? <>: + } + +
+
+ ))} +
setModalOpen(false)} diff --git a/src/app/(dashboard)/planning/type-presence/TypePresence.jsx b/src/app/(dashboard)/planning/type-presence/TypePresence.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6a2cdc312d5713536fa56466844735ef2526bdc6 --- /dev/null +++ b/src/app/(dashboard)/planning/type-presence/TypePresence.jsx @@ -0,0 +1,71 @@ +"use client"; + +import React, {useEffect, useState} from 'react'; +import fetchRequest from "@/app/lib/fetchRequest"; +import EntityList from "@/app/(dashboard)/planning/type-presence/EntityList"; +import EntityForm from "@/app/(dashboard)/planning/type-presence/EntityForm"; +import {useNotification} from "@/context/NotificationContext"; +import CancelIcon from "@/static/image/svg/cancel.svg"; + +const TypePresence = ({setIsOpen}) => { + const [typePresences, setTypePresences] = useState([]); + const [editingEntity, setEditingEntity] = useState({entity: null, id: null}); + const {toggleNotification} = useNotification() + + const fetchData = async () => { + const typePresencesResponse = await fetchRequest('/type-presences/'); + + if (typePresencesResponse.isSuccess) setTypePresences(typePresencesResponse.data); + }; + + useEffect(() => { + fetchData(); + }, []); + + const handleDelete = async (endpoint, id, setState, currentState) => { + const response = await fetchRequest(`/${endpoint}/${id}/`, {method: 'DELETE'}); + if (response.isSuccess) { + setState(currentState.filter(item => item.id !== id)); + toggleNotification({ + visible: true, + message: `${currentState.find(item => item.id === id).nom} a été supprimé avec succès`, + type: "success" + }) + } + await fetchData(); + }; + + return ( +
+
+ { + setEditingEntity({entity: null, id: null}); + setIsOpen(false)}} + className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600"/> +
+
+
+

Type de présence

+ setEditingEntity({entity: null, id: null})} + /> + setEditingEntity({entity: 'type-presences', id})} + /> +
+
+
+
+
+ ); +}; + +export default TypePresence; diff --git a/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx index 861c65b910f8b4c8b386866ac59fb2e7c9c110ef..a156ab54e57d49a52e27a1692b86f0a034fdf239 100644 --- a/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx +++ b/src/app/(dashboard)/privilege/CreatePrivilegeForm.jsx @@ -1,16 +1,17 @@ 'use client' import Loader from '@/components/Loader/Loader' -import React, { useRef, useState } from 'react' +import React, { useRef, useState, useEffect } from 'react' import fetchRequest from '../../lib/fetchRequest' import { useNotification } from "@/context/NotificationContext"; +import CancelIcon from "@/static/image/svg/cancel.svg" +import CheckIcon from "@/static/image/svg/check.svg" -const CreatePrivilegeForm = ({ appendPrivilege }) => { + +const CreatePrivilegeForm = ({ appendPrivilege, cancelCreate }) => { const [isLoading, setIsLoading] = useState(false) - const [error, setError] = useState(null) const [privilegeName, setPrivilegeName] = useState(""); const { toggleNotification } = useNotification() const handlePrivilegeNameChange = (event) => { - setError("") setPrivilegeName(event.target.value) } const inputRef = useRef(null) @@ -31,6 +32,7 @@ const CreatePrivilegeForm = ({ appendPrivilege }) => { message: "L'habilitation a été créé avec succès", type: "success" }) + cancelCreate(); } else { setIsLoading(false) if (errors.type === "ValidationError") { @@ -58,21 +60,56 @@ const CreatePrivilegeForm = ({ appendPrivilege }) => { console.log(errors) } } - return ( -
-

Ajout d'habilitation

-
- - -
-

{error}

-
- +
-
- ) + + } export default CreatePrivilegeForm \ No newline at end of file diff --git a/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx b/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx index d0527ec777e59d06f3301aadc10b47744500378b..c1389375bf37c24c2712b86844c04c895d44b9bf 100644 --- a/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx +++ b/src/app/(dashboard)/privilege/PrivilegeTableRow.jsx @@ -2,8 +2,8 @@ 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 DeleteIcon from "@/static/image/svg/trash-bold.svg" +import EditIcon from "@/static/image/svg/edit-bold.svg" import CancelIcon from "@/static/image/svg/cancel.svg" import CheckIcon from "@/static/image/svg/check.svg" import { useNotification } from '@/context/NotificationContext' @@ -133,33 +133,33 @@ 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' /> + + + setPrivilegeName(event.target.value)} defaultValue={name} type='text' className='w-full border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 disabled:bg-transparent duration-100 h-10 outline-none' /> - + {!isUpdating - ?
- -
- :
- -
} - + setModalOpen(false)} diff --git a/src/app/(dashboard)/privilege/PrivilegesFilter.jsx b/src/app/(dashboard)/privilege/PrivilegesFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..d251f0ec25a7bdee631202c022a730d0e4513a0a --- /dev/null +++ b/src/app/(dashboard)/privilege/PrivilegesFilter.jsx @@ -0,0 +1,31 @@ +import React, { memo } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +const PrivilegesFilter = memo(function page({ setFilter, filter }) { + const handleChangeFilter = (event) => { + const name = event.target.name + const value = event.target.value + setFilter((filter) => ({ ...filter, [name]: value })) + } + const handleResetFilters = () => { + setFilter({ privilege: "" }) + } + return ( +
+

Recherche

+
+
+ + +
+
+
+ +
+
+ ) +}) + +export default PrivilegesFilter \ No newline at end of file diff --git a/src/app/(dashboard)/privilege/page.jsx b/src/app/(dashboard)/privilege/page.jsx index f10039bf8b06739cf424d8b4c759122b37c03223..c05236641540fecb8cca8f975cd417af7e901455 100644 --- a/src/app/(dashboard)/privilege/page.jsx +++ b/src/app/(dashboard)/privilege/page.jsx @@ -5,12 +5,16 @@ import fetchRequest from '../../lib/fetchRequest' import { isArray } from '../../lib/TypesHelper' import PrivilegeTableRows from './PrivilegeTableRow' import Loader from '@/components/Loader/Loader' +import PrivilegesFilter from "./PrivilegesFilter" +import AddIcon from "@/static/image/svg/add.svg" const Privilege = () => { const [privileges, setPrivileges] = useState([]) const [isLoading, setIsLoading] = useState(true) + const [filter, setFilter] = useState({ privilege: "" }); + const [isCreating, setIsCreating] = useState(false) useEffect(() => { const getPrivileges = async () => { - const { data, errors, isSuccess } = await fetchRequest("/privileges") + const { data, errors, isSuccess } = await fetchRequest(`/privileges/?privilege_name=${filter.privilege}`) setIsLoading(false) if (isSuccess) { setPrivileges(data) @@ -19,32 +23,41 @@ const Privilege = () => { } } getPrivileges() - }, []) + }, [filter.privilege]) const appendPrivilege = (privilege) => { setPrivileges((data) => [privilege, ...data]) } return ( -
- -

Liste des habilitations

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

Pas encore des habilitations

+ <> + +
+
+

Liste des habilitations

+
- :
- - - - - - {privileges.map((element) => { - return - })} -
HabilitationAction
-
- }} -
+ {isLoading &&
} + {!isLoading && <> {(!isArray(privileges) || privileges?.length === 0) + ?
+

Aucune habilitation n'a été trouvée.

+
+ :
+ + + + + + {(isCreating) && setIsCreating(false)} appendPrivilege={appendPrivilege} />} + {privileges?.map((element) => { + return + })} +
HabilitationActions
+
+ }} +
+ ) } diff --git a/src/app/(dashboard)/projects/ProjectForm.jsx b/src/app/(dashboard)/projects/ProjectForm.jsx index bb531d52e9639b8047f24d18afc63b4444fb5b18..40e29d1d391b85bcc183b16971c8b802108a1446 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,139 @@ 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 && ( -
    - {userSuggestions.map(user => ( -
  • !isUserChosen(user) && handleUserSelect(user)} - className={`px-3 py-2 border-b border-chicago-200 ${isUserChosen(user) ? 'bg-chicago-100 cursor-not-allowed' : 'hover:bg-sushi-200 cursor-pointer'}`} - > - {/*add tick is chosen*/} - {isUserChosen(user) && '✔ '} - {user.first_name} {user.last_name} ({user.role.name}) {user.email} -
  • - ))} -
- )} -
- {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-100 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}

} +
+
+ {/* cancel*/} + {editingProject && ( -
- ))} -
- {errors.userIds &&

{errors.userIds}

} + )} + +
+
- - {/* 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..c9f8ff450da2ca068058c29a6954f831e48d8903 --- /dev/null +++ b/src/app/(dashboard)/projects/ProjectsFilter.jsx @@ -0,0 +1,76 @@ +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, filter}) => { + const handleChangeFilter = (event) => { + const name = event.target.name + const value = event.target.value + setFilter((filter) => ({...filter, [name]: value})) + } + + const resetFilter = () => { + setFilter({ + name: "", + user_count: "", + name_client: "", + status: "", + date_debut: "", + date_fin: "" + }) + } + + 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..d35d5bcf0bea7c5cda68de63ea7f4901d6745293 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,77 @@ 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/(dashboard)/reporting/BubbleStatistic.jsx b/src/app/(dashboard)/reporting/BubbleStatistic.jsx index a3d82a4170b120a41d02a142c15a0ae90842637a..641e0c65ea3b587ca3975f136a50c40083c907ba 100644 --- a/src/app/(dashboard)/reporting/BubbleStatistic.jsx +++ b/src/app/(dashboard)/reporting/BubbleStatistic.jsx @@ -21,7 +21,6 @@ ChartJS.register( const BubbleStatistic = React.memo(function BubbleStatistic({ axisX, data, title }) { - const colors = useMemo(() => data ? generateColors(data.length) : [], [data]) const options = { responsive: true, diff --git a/src/app/(dashboard)/reporting/page.jsx b/src/app/(dashboard)/reporting/page.jsx index 47058e4bce01ba403b2a2878ac147e39372cbd40..9f11566f4bc829ade86d3f72261281be24a1b4a5 100644 --- a/src/app/(dashboard)/reporting/page.jsx +++ b/src/app/(dashboard)/reporting/page.jsx @@ -1,31 +1,246 @@ -"use client" +// "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'; +// import UserIcon from "@/static/image/svg/user.svg" +// import ProjectIcon from "@/static/image/svg/project.svg" +// import StatisticsIcon from "@/static/image/svg/statistics.svg" +// import DeskIcon from "@/static/image/svg/desk-1.svg" + + +// const Reporting = () => { +// const [chartDataZone, setChartDataZone] = useState(null) +// const [chartDataProject, setChartDataProject] = useState(null) +// const [isLoadingZone, setIsLoadingZone] = useState(false) +// const [isLoadingProject, setIsLoadingProject] = useState(false) +// const [countUsers, setCountUsers] = useState(0) +// const [countProjects, setCountProjects] = useState(0) +// const [countPlaces, setCountPlaces] = useState(0) +// const { toggleNotification } = useNotification() +// const [dates, setDates] = useState({ fromDate: extractDate(subtractDays(new Date(), 4)), toDate: extractDate(new Date()) }) + +// useEffect(() => { +// const getStats = async () => { +// const { data, errors, isSuccess } = await fetchRequest(`/statistics/`, { +// method: "GET", +// }) +// if (isSuccess) { +// setCountUsers(data.user_count) +// setCountProjects(data.project_count) +// setCountPlaces(data.place_count) +// } else { +// console.log(errors); +// toggleNotification({ +// type: "error", +// message: "Internal Server Error", +// visible: true +// }) +// } +// } +// getStats() +// }, []) +// 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) { +// console.log("Zones presences :", data) +// 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) => { +// console.log("week selection",event.target.value) +// 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()) }) +// } +// console.log(dates) +// 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 ( +//
+//
+//
+//

Collaborateurs

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

Projets

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

Places

+//
+// {countPlaces} +//
+// +// +//
+//
+//
+//
+//
+// +// +//
+//
+// +// +// {/* */} +//
+//
+//
+// {(isLoadingZone || isLoadingProject) ?
+// +//
:
+// +// +//
} +//
+// ) +// } + +// export default Reporting + +"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'; +import { getWeeksBetween, getWeekRange, extractDate } from '@/app/lib/DateHelper'; import UserIcon from "@/static/image/svg/user.svg" import ProjectIcon from "@/static/image/svg/project.svg" import StatisticsIcon from "@/static/image/svg/statistics.svg" import DeskIcon from "@/static/image/svg/desk-1.svg" - const Reporting = () => { const [chartDataZone, setChartDataZone] = useState(null) const [chartDataProject, setChartDataProject] = useState(null) const [isLoadingZone, setIsLoadingZone] = useState(false) const [isLoadingProject, setIsLoadingProject] = useState(false) - const [ countUsers, setCountUsers ] = useState(0) - const [ countProjects, setCountProjects ] = useState(0) - const [ countPlaces, setCountPlaces ] = useState(0) + const [countUsers, setCountUsers] = useState(0) + const [countProjects, setCountProjects] = useState(0) + const [countPlaces, setCountPlaces] = useState(0) const { toggleNotification } = useNotification() - const [dates, setDates] = useState({ fromDate: extractDate(subtractDays(new Date(), 4)), toDate: extractDate(new Date()) }) + + const getCurrentWeekString = () => { + const currentDate = new Date(); + const year = currentDate.getFullYear(); + const week = getWeekNumber(currentDate); + return `${year}-W${week.toString().padStart(2, '0')}`; + } + + const currentWeekString = getCurrentWeekString(); + + const [weeks, setWeeks] = useState({ + fromWeek: getCurrentWeekString(), // Initialize with current week + toWeek: getCurrentWeekString() + }); useEffect(() => { - const getStats= async () => { + const getStats = async () => { const { data, errors, isSuccess } = await fetchRequest(`/statistics/`, { method: "GET", }) @@ -44,22 +259,22 @@ const Reporting = () => { } getStats() }, []) - console.log(countPlaces) - console.log(countProjects) - console.log(countUsers) useEffect(() => { const getZonesPresenceStatistic = async () => { setIsLoadingZone(true) + const { weekStart: from_date, weekEnd: to_date } = getWeekRange(weeks.fromWeek); + const { weekStart: end_from_date, weekEnd: end_to_date } = getWeekRange(weeks.toWeek); const { data, errors, isSuccess } = await fetchRequest(`/zoaning/zone-presence/`, { method: "POST", body: JSON.stringify({ - from_date: dates.fromDate, - to_date: dates.toDate + from_date: from_date, + to_date: end_to_date }) }) setIsLoadingZone(false) if (isSuccess) { + console.log("zones presences :", data) setChartDataZone(data); } else { console.log(errors); @@ -71,23 +286,23 @@ const Reporting = () => { } } getZonesPresenceStatistic() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dates.fromDate, dates.toDate]) - + }, [weeks.fromWeek, weeks.toWeek]) useEffect(() => { const getProjectsPresenceStatistic = async () => { setIsLoadingProject(true) + const { weekStart: from_date, weekEnd: to_date } = getWeekRange(weeks.fromWeek); + const { weekStart: end_from_date, weekEnd: end_to_date } = getWeekRange(weeks.toWeek); const { data, errors, isSuccess } = await fetchRequest(`/zoaning/project-presence/`, { method: "POST", body: JSON.stringify({ - from_date: dates.fromDate, - to_date: dates.toDate + from_date: from_date, + to_date: end_to_date }) }) setIsLoadingProject(false) if (isSuccess) { - console.log(data); + console.log("projects presences :", data) setChartDataProject(data); } else { console.log(errors); @@ -99,29 +314,31 @@ const Reporting = () => { } } getProjectsPresenceStatistic() - // eslint-disable-next-line react-hooks/exhaustive-deps - }, [dates.fromDate, dates.toDate]) + }, [weeks.fromWeek, weeks.toWeek]) + const handleDateChange = (event) => { const name = event.target.name const value = event.target.value if (value) { - setDates({ ...dates, [name]: value }) + setWeeks({ ...weeks, [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()) }) + else if (!value && name === "fromWeek") setWeeks({ ...weeks, "fromWeek": currentWeekString }) + else if (!value && name === "toWeek") setWeeks({ ...weeks, "toWeek": currentWeekString }) } - const axisX = useMemo(() => getDateRange(dates.fromDate, dates.toDate), [dates.fromDate, dates.toDate]) + + const axisX = useMemo(() => getWeeksBetween(weeks.fromWeek, weeks.toWeek).map(week => week.axisX), [weeks.fromWeek, weeks.toWeek]); + const processedProjectData = useMemo(() => { return chartDataProject?.map((project, index) => ( { label: project.label, data: [...project.data.map((element) => ( { - x: element.date.split("-").reverse().join("-"), + x: element.start_date_week+' -- '+element.end_date_week, y: element.pourcentage, r: (index * 2) + 6 } - )), ...axisX.filter((element) => !project.data.find((elm) => elm.date.split("-").reverse().join("-") === element)).map((element) => ({ + )), ...axisX.filter((element) => !project.data.find((elm) => elm.start_date_week == element.split('--')[0].trim())).map((element) => ({ x: element, y: 0, r: (index * 2) + 6 @@ -135,26 +352,27 @@ const Reporting = () => { label: "Zone " + zone.label, data: [...zone.data.map((element) => ( { - x: element.date.split("-").reverse().join("-"), + x: element.start_date_week+' -- '+element.end_date_week, y: element.pourcentage, r: (index * 2) + 6 } - )), ...axisX.filter((element) => !zone.data.find((elm) => elm.date.split("-").reverse().join("-") === element)).map((element) => ({ + )), ...axisX.filter((element) => !zone.data.find((elm) => elm.start_date_week == element.split('--')[0].trim())).map((element) => ({ x: element, y: 0, r: (index * 2) + 6 }))] })) }, [chartDataZone, axisX]) + return ( -
-
+
+

Collaborateurs

{countUsers}
- - + +
@@ -163,8 +381,8 @@ const Reporting = () => {
{countProjects}
- - + +
@@ -173,26 +391,25 @@ const Reporting = () => {
{countPlaces}
- - + +
-
+
- - + +
- - - {/* */} + +
{(isLoadingZone || isLoadingProject) ?
-
:
+
:
} @@ -200,4 +417,17 @@ const Reporting = () => { ) } -export default Reporting \ No newline at end of file +export default Reporting + +/** + * Get the ISO week number of a date. + * @param {Date} date - The date to get the week number for. + * @returns {number} The ISO week number. + */ +const getWeekNumber = (date) => { + const tempDate = new Date(date.getTime()); + tempDate.setHours(0, 0, 0, 0); + tempDate.setDate(tempDate.getDate() + 3 - ((tempDate.getDay() + 6) % 7)); + const week1 = new Date(tempDate.getFullYear(), 0, 4); + return 1 + Math.round(((tempDate.getTime() - week1.getTime()) / 86400000 - 3 + ((week1.getDay() + 6) % 7)) / 7); +}; diff --git a/src/app/(dashboard)/reservation/PlaceUI.jsx b/src/app/(dashboard)/reservation/PlaceUI.jsx index 43d2f8f76985f016934e10178a36cc79c3f35f89..631f5de274a31897817becd5a04158fa123624db 100644 --- a/src/app/(dashboard)/reservation/PlaceUI.jsx +++ b/src/app/(dashboard)/reservation/PlaceUI.jsx @@ -3,13 +3,20 @@ import React, { useContext, useState } from 'react' import { ReservationContext } from './page' import fetchRequest from '@/app/lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' -import ConfirmationModal from '@/app/ui/ConfirmationModal' -const PlaceUI = ({ id }) => { +import PlaceUnavailableIcon from "@/static/image/svg/placeUnavailable.svg" +import PlaceRedIcon from "@/static/image/svg/placeRed.svg" +import PlacePresent from "@/static/image/svg/placePresent.svg" +import PlaceBlue from "@/static/image/svg/placeBlue.svg" +import PlaceBooked from "@/static/image/svg/placeBooked.svg" +import ReservationConfirmation from '@/app/ui/ReservationConfirmation' + +const PlaceUI = ({ id, rotate, numero, id_table: table }) => { const { allPlaces, selectedDate, bookedPlaces, setBookedPlaces, authenticatedUserData, hasPlace } = useContext(ReservationContext) const { toggleNotification } = useNotification() const [isOpenBooking, setIsOpenBooking] = useState(false) const [isOpenCanceling, setIsOpenCanceling] = useState(false) - + const [isLoadingBooking, setIsLoadingBooking] = useState(false) + const [isLoadingCanceling, setIsLoadingCanceling] = useState(false) const place = allPlaces?.find((place) => place.id === id) const bookedPlace = bookedPlaces?.find((bookedPlace) => bookedPlace.id_place === id) const handleBooking = (event) => { @@ -25,6 +32,7 @@ const PlaceUI = ({ id }) => { else setIsOpenBooking(true) } const handleBookingConfirmation = async () => { + setIsLoadingBooking(true) const { isSuccess, errors, data } = await fetchRequest('/zoaning/reservations/', { method: "POST", body: JSON.stringify( { @@ -34,6 +42,7 @@ const PlaceUI = ({ id }) => { } ) }); + setIsLoadingBooking(false) if (isSuccess) { console.log(data); setBookedPlaces([...bookedPlaces, data]) @@ -60,11 +69,12 @@ const PlaceUI = ({ id }) => { } } const handleCancelingConfirmation = async (idReservation) => { + setIsLoadingCanceling(true) const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/reservations/${idReservation}`, { method: "DELETE" }); + setIsLoadingCanceling(false) if (isSuccess) { - console.log(data); setBookedPlaces(bookedPlaces.filter((element) => element.id !== idReservation)) toggleNotification({ visible: true, @@ -100,24 +110,51 @@ const PlaceUI = ({ id }) => { event.stopPropagation() setIsOpenBooking(false) } - - if (authenticatedUserData) if (place) if (bookedPlace) if (bookedPlace.id_user === authenticatedUserData.sessionData?.user_id) - return
{ }} className={`${bookedPlace.presence ? "bg-green-500/80" : "bg-amber-500/80 cursor-pointer"} absolute items-center flex justify-center h-full px-[2px] w-full text-sm`}> -

{place.project_name || ""}

- handleCancelingConfirmation(bookedPlace.id)} onClose={closeCancelingPopup} /> + if (bookedPlace.presence) return
+
+

{place.project_name || ""}

+
+
+
- else return
-

{place.project_name || ""}

+ else return <> +
+
+

{place.project_name || ""}

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

{place.project_name || ""}

+
+
+ +
+ else return <> +
+
+

{place.project_name || ""}

+
+
+
- else return
-

{place.project_name || ""}

- + + + else return
+
+
- else return
+
+ +
else return <> } diff --git a/src/app/(dashboard)/reservation/PresenceButton.jsx b/src/app/(dashboard)/reservation/PresenceButton.jsx index ae586779712c63922b807b1025dd52d959a1edba..7fba96b70ca1c608f2947d16ba779e45162de035 100644 --- a/src/app/(dashboard)/reservation/PresenceButton.jsx +++ b/src/app/(dashboard)/reservation/PresenceButton.jsx @@ -4,26 +4,107 @@ import Loader from '@/components/Loader/Loader' import { useNotification } from '@/context/NotificationContext' import React, { useContext, useEffect, useState } from 'react' import { ReservationContext } from './page' +import ReservationConfirmation from '@/app/ui/ReservationConfirmation' -const PresenceButton = ({ processUserLocation, geolocationError, isInside }) => { +const PresenceButton = ({ isDisabled, isConfirmed }) => { const { toggleNotification } = useNotification() const [isLoading, setIsLoading] = useState(false) const { setBookedPlaces, hasPlace } = useContext(ReservationContext) + const [confirmationPopup, setConfirmationPopup] = useState({ isOpen: false, message: "", type: "", title: "" }) + + const processUserLocation = () => { + const companyLatitude = 36.8402141; // Example: Company's latitude + const companyLongitude = 10.2432573; // Example: Company's longitude + const allowedRadius = 300; // Example: Radius in meters + + function getDistanceFromLatLonInMeters(lat1, lon1, lat2, lon2) { + const R = 6371000; // Radius of the Earth in meters + const dLat = (lat2 - lat1) * Math.PI / 180; + const dLon = (lon2 - lon1) * Math.PI / 180; + const a = + Math.sin(dLat / 2) * Math.sin(dLat / 2) + + Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * + Math.sin(dLon / 2) * Math.sin(dLon / 2); + const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); + const distance = R * c; // Distance in meters + return distance; + } + + return new Promise((resolve) => { + if (navigator.geolocation) { + navigator.geolocation.getCurrentPosition( + function (position) { + const userLatitude = position.coords.latitude; + const userLongitude = position.coords.longitude; + + const distance = getDistanceFromLatLonInMeters( + userLatitude, + userLongitude, + companyLatitude, + companyLongitude + ); + + const isInside = distance <= allowedRadius; + resolve({ isInside, geolocationError: null }); + }, + function (error) { + let geolocationError; + switch (error.code) { + case error.PERMISSION_DENIED: + geolocationError = "Vous avez refusé la demande de géolocalisation."; + break; + case error.POSITION_UNAVAILABLE: + geolocationError = "Les informations de localisation sont indisponibles."; + break; + case error.TIMEOUT: + geolocationError = "La demande de localisation a expiré."; + break; + case error.UNKNOWN_ERROR: + geolocationError = "Une erreur inconnue est survenue."; + break; + } + resolve({ isInside: false, geolocationError }); + } + ); + } else { + resolve({ + isInside: false, + geolocationError: "La géolocalisation n'est pas supportée par ce navigateur." + }); + } + }); + }; + + useEffect(() => { + navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { + }).catch((error) => { + console.error('Failed to query geolocation permission:', error); + }); + return () => { + navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { + permissionStatus.onchange = null; + }); + }; + }, []) + const handlePresenceSave = async () => { - if (hasPlace) { - setIsLoading(true) + setIsLoading(true) + const userLocation = await processUserLocation() + if (userLocation.geolocationError) { + setIsLoading(false) + setConfirmationPopup({ type: "location", message: userLocation.geolocationError, isOpen: true, title: "Alerte de Localisation" }) + } else if (!userLocation.isInside) { + setIsLoading(false) + setConfirmationPopup({ type: "location", message: "Votre localisation actuelle n'est pas conforme aux exigences.", title: "Alerte de Localisation Non Conforme", isOpen: true }) + } + else if (hasPlace) { const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/reservations/${hasPlace.id}/`, { method: "PATCH", body: JSON.stringify({ presence: true }) }) setIsLoading(false) if (isSuccess) { - toggleNotification({ - visible: true, - message: "Votre présence a été enregistrée avec succès.", - type: "success" - }) - console.log(data) + setConfirmationPopup({ type: "presence" }) setBookedPlaces((elements) => elements.map((element) => element.id === data.data?.id ? data.data : element)) } else { @@ -43,62 +124,12 @@ const PresenceButton = ({ processUserLocation, geolocationError, isInside }) => } } } - useEffect(() => { - if ("User denied the request for Geolocation." === geolocationError) { - toggleNotification({ - visible: true, - message: "Vous devez activer la géolocalisation pour enregistrer la présence", - type: "warning" - }) - } - else if (geolocationError === "The request to get user location timed out.") { - toggleNotification({ - visible: true, - message: "Vous devez activer la géolocalisation pour enregistrer la présence", - type: "warning" - }) - } - else if (geolocationError === "Location information is unavailable.") { - toggleNotification({ - visible: true, - message: "Erreur de géolocation", - type: "error" - }) - } - }, [geolocationError]) - - - if ("User denied the request for Geolocation." === geolocationError) { - return
-

Autorisez la géolocalisation pour enregistrer votre présence

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

Autorisez la géolocalisation pour enregistrer votre présence

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

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

- -
+ setConfirmationPopup({ isOpen: false, message: "", type: "", title: "" })} {...confirmationPopup} /> + } diff --git a/src/app/(dashboard)/reservation/TableUI.jsx b/src/app/(dashboard)/reservation/TableUI.jsx index bf4e5e6a0d402f0cecc996a8b87ba7e30b0ac8d8..8a12657a85edd382e7be108a04da07fd40ecf0cf 100644 --- a/src/app/(dashboard)/reservation/TableUI.jsx +++ b/src/app/(dashboard)/reservation/TableUI.jsx @@ -28,16 +28,11 @@ const TableUI = ({ places }) => { if (!processedPlaces || processedPlaces.length === 0) return <> return ( -
+
{processedPlaces.map((element, index) => { - return
-
- -
-
- {(element.length > 1) &&
- -
} + return
+ + {(element.length > 1) && }
})}
diff --git a/src/app/(dashboard)/reservation/ZoneUI.jsx b/src/app/(dashboard)/reservation/ZoneUI.jsx index 42538fde425c979c9be988919bbf6c3774c10398..e7d99428f1b432080e2ca2b1f080206916c7bacc 100644 --- a/src/app/(dashboard)/reservation/ZoneUI.jsx +++ b/src/app/(dashboard)/reservation/ZoneUI.jsx @@ -3,13 +3,10 @@ import TableUI from './TableUI' const ZoneUI = ({ id, tables, nom }) => { return ( -
-

Zone {nom}

-
- {tables.map((table) => { - return - })} -
+
+ {tables.map((table) => { + return + })}
) } diff --git a/src/app/(dashboard)/reservation/page.jsx b/src/app/(dashboard)/reservation/page.jsx index e77c657132fece448ba7ce94ecd204fde17fb2ce..422d12cc34d7b8367529011a69bc92c49d7e0c0e 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -8,6 +8,8 @@ import { useNotification } from '@/context/NotificationContext' import PresenceButton from './PresenceButton' import Cookies from 'js-cookie'; import { decrypt } from '@/app/lib/session'; +import ReservationConfirmation from '@/app/ui/ReservationConfirmation' +import { dateDiffInDays, extractDate } from '@/app/lib/DateHelper' export const ReservationContext = React.createContext() @@ -26,6 +28,8 @@ const Reservation = () => { const [authenticatedUserData, setAuthenticatedUserData] = useState(null) + const [selectedZone, setSelectedZone] = useState({ floorId: null, zoneId: null }) + const cookie = Cookies.get("session") const getUserData = async () => { try { @@ -39,97 +43,6 @@ const Reservation = () => { } useMemo(getUserData, [cookie]) - const processUserLocation = () => { - const companyLatitude = 36.8402141; // Example: Company's latitude - const companyLongitude = 10.2432573; // Example: Company's longitude - const allowedRadius = 300; // Example: Radius in meters - - - function getDistanceFromLatLonInMeters(lat1, lon1, lat2, lon2) { - const R = 6371000; // Radius of the Earth in meters - const dLat = (lat2 - lat1) * Math.PI / 180; - const dLon = (lon2 - lon1) * Math.PI / 180; - const a = - Math.sin(dLat / 2) * Math.sin(dLat / 2) + - Math.cos(lat1 * Math.PI / 180) * Math.cos(lat2 * Math.PI / 180) * - Math.sin(dLon / 2) * Math.sin(dLon / 2); - const c = 2 * Math.atan2(Math.sqrt(a), Math.sqrt(1 - a)); - const distance = R * c; // Distance in meters - return distance; - } - - if (navigator.geolocation) { - navigator.geolocation.getCurrentPosition( - function (position) { - const userLatitude = position.coords.latitude; - const userLongitude = position.coords.longitude; - - const distance = getDistanceFromLatLonInMeters( - userLatitude, - userLongitude, - companyLatitude, - companyLongitude - ); - - if (distance <= allowedRadius) { - setIsInside(true) - } else { - setIsInside(false) - } - setGeolocationError(null) - }, - function (error) { - switch (error.code) { - case error.PERMISSION_DENIED: - setGeolocationError("User denied the request for Geolocation."); - break; - case error.POSITION_UNAVAILABLE: - setGeolocationError("Location information is unavailable."); - break; - case error.TIMEOUT: - setGeolocationError("The request to get user location timed out."); - break; - case error.UNKNOWN_ERROR: - setGeolocationError("An unknown error occurred."); - break; - } - } - ); - } else { - toggleNotification({ - message: "La géolocalisation n'est pas supporté par votre navigateur.", - type: "warning", - visible: true - }) - } - } - - - useEffect(() => { - const handlePermissionChange = (permissionStatus) => { - if (permissionStatus.state === 'granted' || permissionStatus.state === 'prompt') { - processUserLocation(); - } else { - setGeolocationError("User denied the request for Geolocation."); - } - }; - - navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { - handlePermissionChange(permissionStatus); - - permissionStatus.onchange = () => { - handlePermissionChange(permissionStatus); - }; - }).catch((error) => { - console.error('Failed to query geolocation permission:', error); - }); - return () => { - navigator.permissions.query({ name: 'geolocation' }).then((permissionStatus) => { - permissionStatus.onchange = null; - }); - }; - }, []) - useEffect(() => { const getPlan = async () => { try { @@ -160,6 +73,8 @@ const Reservation = () => { const { isSuccess, errors, data } = await fetchRequest(`/zoaning/user-projects-zones-places/${date.week}/${date.day}`) if (isSuccess) { setProjectsData(data) + if (data && data.length && data[0].zones.length) setSelectedZone({ floorId: data[0].zones[0].id_etage.id, zoneId: data[0].zones[0].id }) + else setSelectedZone({ floorId: null, zoneId: null }) } else { console.log(errors) toggleNotification({ @@ -245,7 +160,6 @@ const Reservation = () => { const concernedFloors = floors?.filter((element) => element.zones.map((zone) => zone.id).some((element) => concernedZones.includes(element))).map(element => element.id) || [] const allPlaces = projectsData?.map((project) => project.zones.map((zone) => zone.places.map((place) => ({ ...place, project_name: project.project_name, project_id: project.project_id })))).flat(2) || [] const hasPlace = bookedPlaces?.find((p) => p.id_user === authenticatedUserData?.sessionData?.user_id) - const dateRef = useRef(null) useEffect(() => { if (dateRef.current && currentDateData?.date) { @@ -253,6 +167,7 @@ const Reservation = () => { setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) } }, [dateRef.current, currentDateData?.date]) + if (isLoadingSelectsData) return
@@ -260,72 +175,112 @@ const Reservation = () => { const currentMonth = new Date().getMonth() + 1 // filter dates from now to 2 weeks later + + // console.log("target date", datesData); + // const filteredDatesData = datesData?.filter((element) => { + // const date = new Date(element.date) + // const month = date.getMonth() + 1 + // return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + // }) const filteredDatesData = datesData?.filter((element) => { - const date = new Date(element.date) - const month = date.getMonth() + 1 - return (month === currentMonth) && (date.getDate() >= new Date().getDate() && date.getDate() <= new Date().getDate() + 14) + const diff = dateDiffInDays(extractDate(new Date()), element.date) + return diff >= 0 && diff <= 14 + }) + const selectedZoneData = floors.find((element) => element.id == selectedZone.floorId)?.zones.find((zone) => zone.id === selectedZone.zoneId) return ( -
-
- {(filteredDatesData && filteredDatesData.length) && } -
-
-
-
-

Autres Projets

-
-
-
-

Disponible

-
-
-
-

Réservé par vous

-
-
-
-

Réservé par un collègue

-
-
-
-

Présent

+ <> +
+
+

Réservation place

+ {(filteredDatesData && filteredDatesData.length !== 0) && }
-
- {(!isLoadingData) - ? - - <> - {(floors.filter((element) => concernedFloors.includes(element.id))?.length > 0 && hasPlace && !hasPlace.presence && currentDate && currentDate === date?.date) &&
- -
} -
- {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { - return
-

Etage {floor.numero}

- {floor.zones.filter((element) => concernedZones.includes(element.id)).map((zone, index) => { - return - })} + {(selectedZone.floorId && !isLoadingData) &&
+ { !(floors.find((element) => element.id == selectedZone.floorId).image) ?
+
+
+ + Aucune Illustration fournie pour cet étage
- })} - {!isLoadingData && floors.filter((element) => concernedFloors.includes(element.id)).length === 0 - &&
-

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

-
} +
- - - :
- -
- } -
+ : +
+ element.id == selectedZone.floorId).image} className='w-full [aspect-ratio:2.5]'> +
+ } +
+ {floors.filter((element) => concernedFloors.includes(element.id)).map((floor) => { + return floor.zones?.map((zone, index) => { + if (selectedZone.zoneId == zone.id && selectedZone.floorId == floor.id) return
setSelectedZone({ floorId: floor.id, zoneId: zone.id })} key={index} className='rounded-md not-first:-left-2 text-white relative whitespace-nowrap flex items-center justify-center border border-[#B2CEDE] text-xs md:text-sm lg:text-base px-5 lg:px-10 h-9 bg-[#B2CEDE]'> +

Etage {floor.numero} - Zone {zone.nom}

+
+ return
setSelectedZone({ floorId: floor.id, zoneId: zone.id })} className='cursor-pointer not-first:-left-2 hover:bg-neutral-100 duration-300 rounded-md text-[#8E8D8D] relative whitespace-nowrap border flex items-center justify-center border-[#D9D9D9] text-xs md:text-sm lg:text-base lg:px-10 px-5 h-9 bg-white'> +

Etage {floor.numero} - Zone {zone.nom}

+
+ }) + })} +
+
} +
+
+
+
+

Autres Projets

+
+
+
+

Disponible

+
+
+
+

Réservé par vous

+
+
+
+

Réservé par un collègue

+
+
+
+

Présent

+
+
+ {(!isLoadingData) + ? + + <> +
+ concernedFloors.includes(element.id))?.length === 0 || !hasPlace || hasPlace.presence || !currentDate || currentDate !== date?.date} /> +
+
+ {(selectedZoneData) && +
+ +
} + {(!isLoadingData && !selectedZoneData) + &&
+

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

+
} +
+ +
+ :
+ +
+ } +
+
+ + ) } diff --git a/src/app/(dashboard)/role/CreateRoleForm.jsx b/src/app/(dashboard)/role/CreateRoleForm.jsx index 3ba4a859863b9c1f5d662cd800fb10c3c48d5b10..2e5e4ce45f48e60d523d34ec97ae2ea056d8847e 100644 --- a/src/app/(dashboard)/role/CreateRoleForm.jsx +++ b/src/app/(dashboard)/role/CreateRoleForm.jsx @@ -2,13 +2,13 @@ import Loader from '@/components/Loader/Loader' import React, { useState, useRef, useEffect, useMemo } from 'react' import fetchRequest from '@/app/lib/fetchRequest' import { useNotification } from '@/context/NotificationContext' -import CancelIcon from "@/static/image/svg/cancel.svg" const CreateRoleForm = ({ appendRole, setIsOpen }) => { const [isLoading, setIsLoading] = useState(false) const [roleName, setRoleName] = useState("") const [privileges, setPrivileges] = useState(null) - const [selectedPrivileges, setSelectedPrivileges] = useState([]) + const [checkedValues, setCheckedValues] = useState([]); const { toggleNotification } = useNotification() + const [selectAll, setSelectAll] = useState(false); useEffect(() => { const getPrivileges = async () => { const { data, errors, isSuccess } = await fetchRequest("/privileges") @@ -35,12 +35,12 @@ const CreateRoleForm = ({ appendRole, setIsOpen }) => { setIsLoading(true) const { data, errors, isSuccess, status } = await fetchRequest("/roles/", { method: "POST", - body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.map((element) => element.id) }) + body: JSON.stringify({ name: roleName, privileges: checkedValues }) }) if (isSuccess) { setIsLoading(false) inputRef.current.value = "" - setSelectedPrivileges([]) + setCheckedValues([]) setRoleName("") appendRole(data) toggleNotification({ @@ -93,46 +93,63 @@ const CreateRoleForm = ({ appendRole, setIsOpen }) => { console.log(errors) } } - const handlePrivilegeClick = (privilege) => { - if (selectedPrivileges.find((element) => element.id === privilege.id)) { - setSelectedPrivileges(selectedPrivileges.filter((element) => element.id !== privilege.id)) + + + const handleCheckboxChange = (event) => { + const { value, checked } = event.target; + if (value === "selectAll") { + setSelectAll(checked); + if (checked) { + setCheckedValues(privileges.map(element => element.id)); + } else { + setCheckedValues([]); + } } else { - setSelectedPrivileges([...selectedPrivileges, privilege]) + const updatedValues = checked + ? [...checkedValues, value] + : checkedValues.filter(val => val != value); + + setCheckedValues(updatedValues); + setSelectAll(updatedValues.length === privileges.length); } - } - 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 (
-
- setIsOpen(false)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> - {(privileges) ?
-

Ajout de Rôle

-
- +
+ {(privileges) ? +

Ajouter rôle

+
+
-
-
- -
{!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

-
} +
+ + +
+
+ {privileges.length !== 0 + ? privileges?.map((privilege) => { + return
+ element == privilege.id) != undefined} type='checkbox' onChange={handleCheckboxChange} className='form-checkbox rounded-sm border-2 border-[#323232] focus:ring-0 focus:ring-transparent cursor-pointer h-4 w-4 text-[#323232]' name={"privilege_" + privilege.id} id={"privilege_" + privilege.id} /> + +
+ }) + :
+

Pas encore des habilitations

+
}
-
- +
:
} diff --git a/src/app/(dashboard)/role/RoleTableRows.jsx b/src/app/(dashboard)/role/RoleTableRows.jsx index ccdcad936b6b9f077fc3ac86eb584d85d919d691..d461524eae4cbc0cf3dbae0452e16f48e8b11c3b 100644 --- a/src/app/(dashboard)/role/RoleTableRows.jsx +++ b/src/app/(dashboard)/role/RoleTableRows.jsx @@ -1,7 +1,7 @@ import React, { useState } from 'react' import fetchRequest from '@/app/lib/fetchRequest' -import DeleteIcon from "@/static/image/svg/delete.svg" -import EditIcon from "@/static/image/svg/edit.svg" +import DeleteIcon from "@/static/image/svg/trash-bold.svg" +import EditIcon from "@/static/image/svg/edit-bold.svg" import { useNotification } from '@/context/NotificationContext' import ConfirmationModal from '@/app/ui/ConfirmationModal' @@ -48,29 +48,29 @@ const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => { return ( <> - - -

{name}

+ + +

{name}

- -
+ +
{privileges?.map((element, index) => { - return
{element.name}
+ return
{element.name}
})} - {!privileges || privileges.length == 0 &&

-

} + {!privileges || privileges.length == 0 &&

Habilitations non accordées à ce rôle

}
- -
- -
- + setModalOpen(false)} diff --git a/src/app/(dashboard)/role/RolesFilter.jsx b/src/app/(dashboard)/role/RolesFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..651c2e702739ba3cd76a0b0eb6d9a54fdfcbcde2 --- /dev/null +++ b/src/app/(dashboard)/role/RolesFilter.jsx @@ -0,0 +1,90 @@ +import React, { memo, useEffect, useState } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +import Select from "react-select"; +import fetchRequest from '@/app/lib/fetchRequest'; +const RolesFilter = memo(function Page({ setFilter, filter }) { + const [privileges, setPrivileges] = useState([]) + const handleChangeRoleName = (event) => { + const value = event.target.value + setFilter({ ...filter, "role": value }) + } + const handleResetFilters = () => { + setFilter({ "role": "", "privileges": [] }) + } + + const handlePrivilegesChange = (selectedOptions) => { + setFilter({ ...filter, privileges: selectedOptions }) + } + useEffect(() => { + const getPrivileges = async () => { + const { data, errors, isSuccess } = await fetchRequest(`/privileges`) + if (isSuccess) { + setPrivileges(data) + } else { + console.log(errors) + } + } + getPrivileges() + }, []) + + return ( +
+

Recherche

+
+
+ + +
+
+ +
-
-
- -
{!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

-
} +
+ + +
+
+ {privileges.length !== 0 + ? privileges?.map((privilege) => { + return
+ element == privilege.id) != undefined} type='checkbox' onChange={handleCheckboxChange} className='form-checkbox rounded-sm border-2 border-[#323232] focus:ring-0 focus:ring-transparent cursor-pointer h-4 w-4 text-[#323232]' name={"privilege_" + privilege.id} id={"privilege_" + privilege.id} /> + +
+ }) + :
+

Pas encore des habilitations

+
}
-
- +
:
} diff --git a/src/app/(dashboard)/role/page.jsx b/src/app/(dashboard)/role/page.jsx index dfca5e0015cdcda426b78d1e4bf8375ba0de8701..214767ac4487f47c002c7bee4702e4b24968bc02 100644 --- a/src/app/(dashboard)/role/page.jsx +++ b/src/app/(dashboard)/role/page.jsx @@ -8,15 +8,18 @@ import { isArray } from '../../lib/TypesHelper' import AddIcon from "@/static/image/svg/add.svg" import UpdateRoleForm from './UpdateRoleForm' import { useNotification } from '@/context/NotificationContext' +import RolesFilter from "./RolesFilter" const Role = () => { const [roles, setRoles] = useState([]) const [isLoading, setIsLoading] = useState(true) const [openCreatePopup, setOpenCreatePopup] = useState(null) const [roleToUpdate, setRoleToUpdate] = useState(null) const { toggleNotification } = useNotification() + const [filter, setFilter] = useState({ role: "", privileges: [] }); useEffect(() => { const getRoles = async () => { - const { data, errors, isSuccess } = await fetchRequest("/roles") + const privilegesIdsString = filter.privileges.map((element) => element.value).toString() + const { data, errors, isSuccess } = await fetchRequest(`/roles/?role_name=${filter.role}&privilege_ids=${privilegesIdsString}`) setIsLoading(false) if (isSuccess) { console.log(data) @@ -31,40 +34,44 @@ const Role = () => { } } getRoles() - }, []) + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter.role, filter.privileges]) const appendRole = (newRole) => { setRoles([newRole, ...roles]) } return ( -
- {openCreatePopup && } - {roleToUpdate && } -
-

Liste des Rôles

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

Pas encore des rôles

-
- :
- - - - - - - {roles?.map((element) => { - return - })} -
RôleHabilitationsAction
+ <> + +
+ {openCreatePopup && } + {roleToUpdate && } +
+

Liste des Rôles

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

Aucun rôle n'a été trouvé.

+
+ :
+ + + + + + + {roles?.map((element) => { + return + })} +
RôleHabilitationsActions
+
+ }} +
+ ) } diff --git a/src/app/(dashboard)/table/CreateTable.jsx b/src/app/(dashboard)/table/CreateTable.jsx new file mode 100644 index 0000000000000000000000000000000000000000..a12eabf593cb742d2b8a34807abc6bcd75156d6e --- /dev/null +++ b/src/app/(dashboard)/table/CreateTable.jsx @@ -0,0 +1,150 @@ +import Loader from '@/components/Loader/Loader' +import React, { useState, useRef, useEffect, useMemo } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +const CreateTable = ({ setTables, setIsOpen, etages, zones }) => { + const [ numeroTable, setNumeroTable] = useState(null) + const [selectedZone, setSelectedZone] = useState(null) + const [ selectedEtage, setSelectedEtage ] = useState(null) + const { toggleNotification } = useNotification() + const [isLoadingAction, setIsLoadingAction] = useState(false) + + const inputRef = useRef(null) + const selectRefZones = useRef(null) + const selectRefEtages = useRef(null) + + + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + try{ + const { data, errors, isSuccess } = await fetchRequest("/zoaning/tables/", { + method: "POST", + body: JSON.stringify({ numero: numeroTable, id_zone: selectedZone.id }) + }) + if (isSuccess) { + setIsLoadingAction(false) + setTables((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); + setNumeroTable(null) + setSelectedZone(null) + setSelectedEtage(null) + setIsOpen(false) + inputRef.current.value = "" + selectRefEtages.current.value = "" + selectRefZones.current.value = "" + toggleNotification({ + visible: true, + message: "La table a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + setNumeroTable(null) + setSelectedZone(null) + setSelectedEtage(null) + inputRef.current.value = "" + selectRefEtages.current.value = "" + selectRefZones.current.value = "" + 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 { + setIsOpen(false) + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la table.", + visible: true, + }) + } + console.log(errors) + } + }catch(error){ + console.log(error) + } + } + + const handleTableNumero = (event) => { + const numeroTable = event.target.value + setNumeroTable(numeroTable) + } + + + // Handle the name of table change + const handleEtageSelection = (event) => { + const etageId = event.target.value + if(selectedEtage != etageId){ + const selectedEtage = etages.find(etage => etage.id == etageId) + setSelectedEtage(selectedEtage) + setSelectedZone(null) + } + } + + // Handle the name of zone change + const handleZoneSelection = (event) => { + const zoneId = event.target.value + if(selectedZone != zoneId){ + const selectedZone = zones.find(zone => zone.id == zoneId) + setSelectedZone(selectedZone) + } + } + + return ( +
+
+ {(true) ? +
+

Ajouter table

+
+
+
+ + +
+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ : +
} +
+
+ ) +} + +export default CreateTable \ No newline at end of file diff --git a/src/app/(dashboard)/table/RowTable.jsx b/src/app/(dashboard)/table/RowTable.jsx deleted file mode 100644 index 30acd78b8d16f9932f72a4027989d64e755541c3..0000000000000000000000000000000000000000 --- a/src/app/(dashboard)/table/RowTable.jsx +++ /dev/null @@ -1,227 +0,0 @@ -"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, filteredPlacesState }) => { - - //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)) - filteredPlacesState((prevTableState) => prevTableState.map((element) => element.id === id ? { ...data.data, id_zone: zones.find(zone => zone.id === data.data.id_zone) } : element)) - setIsUpdating(false) - toggleNotification({ - visible: true, - 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)) - filteredPlacesState((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/(dashboard)/table/TableRow.jsx b/src/app/(dashboard)/table/TableRow.jsx new file mode 100644 index 0000000000000000000000000000000000000000..ee3150258e98f7d740311fe111af2fc91e77d464 --- /dev/null +++ b/src/app/(dashboard)/table/TableRow.jsx @@ -0,0 +1,264 @@ +import React, { useState, useRef, useEffect } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import DeleteIcon from "@/static/image/svg/trash-bold.svg" +import EditIcon from "@/static/image/svg/edit-bold.svg" +import CheckIcon from "@/static/image/svg/check.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" +import { useNotification } from '@/context/NotificationContext' +import ConfirmationModal from '@/app/ui/ConfirmationModal' +import Loader from '@/components/Loader/Loader' + + +const TableRow = ({ id, numero, id_zone, tableState, etages, zones }) => { + const [isUpdating, setIsUpdating] = useState(false) + const [numTable, setNumTable] = useState(numero) + const [selectedZone, setSelectedZone] = useState(id_zone.id) + const [selectedEtage, setSelectedEtage] = useState(id_zone.id_etage.id) + const [loadingStatus, setLoadingStatus] = useState(false) + const [isModalOpen, setModalOpen] = useState(false); + const { toggleNotification } = useNotification() + //refs + const inputRef = useRef(null) + const selectRefZone = useRef(null) + const selectRefEtage = useRef(null) + const rowRef = useRef(null) + + //Logic + useEffect(() => { + setNumTable(numero) + setSelectedZone(id_zone?.id) + setSelectedEtage(id_zone?.id_etage?.id) + selectRefZone.current.value = id_zone?.id + selectRefEtage.current.value = id_zone?.id_etage?.id + inputRef.current.value = numero + }, [numero, id_zone]) + + const handleUpdateZone = async () => { + setLoadingStatus(true) + const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/tables/${id}/`, { + method: "PATCH", + body: JSON.stringify({ numero: numTable, 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 + } + tableState((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.nom) { + toggleNotification({ + type: "warning", + message: "Le numéro de la table existe déjà", + visible: true, + }) + } else if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nuémro de la table saisie déjà existe.", + visible: true, + }) + } + else { + toggleNotification({ + visible: true, + message: "Erreur de validation de la zone", + 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) { + tableState((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) + setNumTable(numero) + setSelectedEtage(id_zone.id_etage.id) + setSelectedZone(id_zone.id) + selectRefZone.current.value = id_zone.id + selectRefEtage.current.value = id_zone.id_etage.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 && selectRefZone?.current && selectRefEtage?.current) { + inputRef.current.focus() + selectRefZone.current.focus() + selectRefEtage.current.focus() + document.addEventListener("click", handleUpdateBlur) + } + return () => { + document.removeEventListener("click", handleUpdateBlur) + } + }, [isUpdating]) + + const handleDeleteClick = () => { + setModalOpen(true); + } + + const handleConfirmDelete = () => { + handleDelete(); + setModalOpen(false); + }; + + + const handleEtageSelection = (event) => { + const etageId = event.target.value + if(selectedEtage != etageId){ + setSelectedEtage(etageId) + setSelectedZone("") + } + } + + // Handle the name of zone change + const handleZoneSelection = (event) => { + const zoneId = event.target.value + console.log("zoneID", zoneId) + if(selectedZone != zoneId){ + setSelectedZone(zoneId) + } + } + + console.log(etages) + + console.log("selected etage:", selectedEtage) + console.log("selected zone:", selectedZone) + + return ( + <> + + + setNumTable(event.target.value)} defaultValue={numero} type='text' className='border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none bg-transparent' /> + + + + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
+ } + + + setModalOpen(false)} + onConfirm={handleConfirmDelete} + message={`Voulez-vous vraiment supprimer la table ?`} + /> + + + ) +} + +export default TableRow \ No newline at end of file diff --git a/src/app/(dashboard)/table/TablesFilter.jsx b/src/app/(dashboard)/table/TablesFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..df099d97b1a102b3db17eb3b2e57f67ac503bfe6 --- /dev/null +++ b/src/app/(dashboard)/table/TablesFilter.jsx @@ -0,0 +1,123 @@ +import React, { memo, useEffect, useState } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +import Select from "react-select"; +const TablesFilter = memo(function Page({ setFilter, filter, etages, zones }) { + const handleChangeTableNumero = (event) => { + const value = event.target.value + setFilter({ ...filter, table: value }) + } + const handleResetFilters = () => { + setFilter({ table: "", etage: "", zones: [] }) + } + + const handleEtageChange = (selectedOptions) => { + setFilter({ ...filter, etage: selectedOptions.value, zones: [] }) + } + + const handleZonesChange = (selectedOptions) => { + setFilter({ ...filter, zones: selectedOptions }) + } + return ( +
+

Recherche

+
+
+ + +
+
+ + zone.id_etage.id == filter.etage).map((zone) => ({ label: zone.nom, value: zone.id }))} + styles={ + { + menu: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + menuList: (provided) => ({ + ...provided, + maxHeight: '170px', + overflowY: 'auto' + }), + control: (provided, state) => ({ + ...provided, + borderColor: state.isFocused ? '#93a84c' : 'rgb(212 212 212)', + borderWidth: "1px", + boxShadow: 'none', + borderRadius: "0.375rem", + minHeight: "2.5rem", + }), + option: (provided, state) => ({ + ...provided, + backgroundColor: state.isFocused ? '#d9e1b5' : 'white', + '&:hover': { + backgroundColor: '#d9e1b5', // Tailwind's blue-200 + }, + }), + } + } + /> +
+
+
+ +
+
+ ) +}) + +export default TablesFilter \ No newline at end of file diff --git a/src/app/(dashboard)/table/page.jsx b/src/app/(dashboard)/table/page.jsx index fa644547ea4fb2f6e5c87d52ddcd228e33c1a010..7de51af1af2f5dcd47bee61971c68bf93370f1dd 100644 --- a/src/app/(dashboard)/table/page.jsx +++ b/src/app/(dashboard)/table/page.jsx @@ -1,31 +1,44 @@ -"use client" -import React from 'react' -import fetchRequest from '../../lib/fetchRequest' -import { useState, useEffect, useRef } from 'react'; +'use client' +import React, { useState, useEffect } from 'react' +import CreateTable from './CreateTable' import Loader from '@/components/Loader/Loader' -import TableIcon from "@/static/image/svg/table.svg" +import TableRow from './TableRow' +import fetchRequest from '@/app/lib/fetchRequest' import { isArray } from '../../lib/TypesHelper' -import RowTable from './RowTable' +import AddIcon from "@/static/image/svg/add.svg" import { useNotification } from '@/context/NotificationContext' - - +import TablesFilter from "./TablesFilter" const Table = () => { const [tables, setTables] = useState([]) - const [isLoadingData, setIsLoadingData] = useState(true) const [zones, setZones] = useState([]) const [etages, setEtages] = useState([]) - const [error, setError] = useState(null) - const [isLoadingAction, setIsLoadingAction] = useState(false) - const [numeroTable, setNumeroTable] = useState(null) - const [selectedZone, setSelectedZone] = useState(null) - const [ selectedEtage, setSelectedEtage ] = useState(null) - const [ filteredTables, setFilteredTables ] = useState([]) - + const [isLoading, setIsLoading] = useState(true) + const [openCreatePopup, setOpenCreatePopup] = useState(null) const { toggleNotification } = useNotification() + const [filter, setFilter] = useState({ table: "", etage: "", zones: [] }); - // Fetch data from external API useEffect(() => { + const getTables = async () => { + var zonesIdsString = filter.zones.map((element) => element.value).toString() + const etageSelected = filter.etage + const { data, errors, isSuccess } = await fetchRequest(`/zoaning/tables/?table_numero=${filter.table}&etage_id=${etageSelected}&zones_ids=${zonesIdsString}`) + setIsLoading(false) + if (isSuccess) { + setTables(data) + } else { + console.log(errors) + toggleNotification({ + visible: true, + message: "Internal Server Error", + type: "error" + }) + } + } + getTables() + // eslint-disable-next-line react-hooks/exhaustive-deps + }, [filter.table, filter.etage, filter.zones]) + useEffect(() => { function extractAvailableEtagesWithZones(zones) { const etagesMap = new Map(); @@ -42,22 +55,6 @@ const Table = () => { return Array.from(etagesMap.values()); } - - - const getAllTables = async () => { - try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/tables/', { method: 'GET' }) - if (isSuccess) { - setTables(data) - setFilteredTables(data) - } else { - setTables([]) - setFilteredTables([]) - } - } catch (error) { - console.log(error) - } - } const getAllZones = async () => { try { const { isSuccess, errors, data } = await fetchRequest('/zoaning/zones/', { method: 'GET' }) @@ -72,181 +69,42 @@ const Table = () => { console.log(error) } } - getAllTables() getAllZones() - setIsLoadingData(false) }, []) - console.log("etages", etages) - console.log("zones", zones) - console.log("tables", tables) - const handleSearchingTable = async (e) => { - const numero = e.target.value - try { - const { isSuccess, errors, data } = await fetchRequest(`/zoaning/search/table/${numero}`, { method: 'GET' }) - if (isSuccess) { - setTables(data) - setFilteredTables(data) - } else { - setTables([]) - setFilteredTables([]) - } - } catch (error) { - console.log(error) - } - } - - - // create new table section - - const handleSubmit = async (event) => { - event.preventDefault() - setIsLoadingAction(true) - try{ - const { data, errors, isSuccess } = await fetchRequest("/zoaning/tables/", { - method: "POST", - body: JSON.stringify({ numero: numeroTable, id_zone: selectedZone.id }) - }) - if (isSuccess) { - setIsLoadingAction(false) - setTables((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); - setFilteredTables((prevTableState) => [...prevTableState, { ...data, id_zone: zones.find(zone => zone.id === data.id_zone) }]); - setNumeroTable(null) - setSelectedZone(null) - setSelectedEtage(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) - } - }catch(error){ - console.log(error) - } - } - - - // Handle the name of table change - const handleEtageSelection = (event) => { - const etageId = event.target.value - setFilteredTables(tables.filter(table => table.id_zone.id_etage.id == etageId)) - if(selectedEtage != etageId){ - const selectedEtage = etages.find(etage => etage.id == etageId) - setSelectedEtage(selectedEtage) - setZones(selectedEtage.zones) - setSelectedZone(null) - } - } - - // Handle the name of zone change - const handleZoneSelection = (event) => { - const zoneId = event.target.value - setFilteredTables(tables.filter(table => table.id_zone.id == zoneId)) - if(selectedZone != zoneId){ - const selectedZone = zones.find(zone => zone.id == zoneId) - setSelectedZone(selectedZone) - } - } - - - const handleTableNumero = (event) => { - const numeroTable = event.target.value - setNumeroTable(numeroTable) - } return ( -
-
-
- {!isLoadingData ? - <> -
-

Ajout d'une table

-
-
- - -
-
- - -
-
- - -
-
- -
-
-
-
-

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(filteredTables) && filteredTables?.length !== 0 && isArray(zones) && zones?.length !== 0 ? -
- - - - - - - {filteredTables?.map((element) => { - return - })} -
TableZone-EtageAction
-
- : -
-

Pas encore des tables

-
} - - : -
- } + <> + +
+ {openCreatePopup && } +
+

Liste des Tables

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

Aucune Table n'a été trouvé.

+
+ :
+ + + + + + + + {tables?.map((element) => { + return + })} +
TableZoneÉtageActions
+
+ }}
-
- + ) } diff --git a/src/app/(dashboard)/user/CreateUserForm.jsx b/src/app/(dashboard)/user/CreateUserForm.jsx index d4089e84c7325393c666a1bacc4135f6459361c0..a7d15dd8a235f754bb088cda37979c8e5a6e1d0e 100644 --- a/src/app/(dashboard)/user/CreateUserForm.jsx +++ b/src/app/(dashboard)/user/CreateUserForm.jsx @@ -159,33 +159,33 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { return (
- setIsOpen(false)} className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600" /> - {(roles && projects) ?
-

Ajout d'utilisateur

-
+ {(roles && projects) ? +

Ajouter utilisateur

+
- +

{errors.last_name}

- +

{errors.first_name}

-
- - +
+ +

{errors.email}

-
- +
+ +

-
+
- +
{projects.length !== 0 ? @@ -234,11 +234,12 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => { :

Pas encore des projets

} +

-
+
- +
{roles.length !== 0 ? @@ -258,9 +259,12 @@ const CreateUserForm = ({ setIsOpen, appendUser }) => {
}
-
- +
:
} diff --git a/src/app/(dashboard)/user/UpdateUserForm.jsx b/src/app/(dashboard)/user/UpdateUserForm.jsx index ff36c1ed52db07dccc39ea4e249953bbca91c3d5..54ce18090737f65b8201bc09ad9f100298156ed6 100644 --- a/src/app/(dashboard)/user/UpdateUserForm.jsx +++ b/src/app/(dashboard)/user/UpdateUserForm.jsx @@ -134,29 +134,28 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => { return (
- setUserToUpdate(null)} className="h-8 w-8 cursor-pointer md:absolute fixed top-2 right-2 fill-neutral-600" /> - {(roles && projects) ?
-

Modification d'utilisateur

-
+ {(roles && projects) ? +

Modifier d'utilisateur

+
- +

{errors.last_name}

- +

{errors.first_name}

-
- - +
+ +

{errors.email}

-
+
- +
{projects.length !== 0 ? @@ -206,11 +205,12 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => { :

Pas encore des projets

} +

-
+
- +
{roles.length !== 0 ? @@ -230,9 +230,12 @@ const UpdateUserForm = ({ setUserToUpdate, userToUpdate, setUsers }) => {
}
-
- +
:
} diff --git a/src/app/(dashboard)/user/UserTableRow.jsx b/src/app/(dashboard)/user/UserTableRow.jsx index 279a0a794d103601b4eece9af34665ca4c70b7c4..0907f00a4741a7e89736ca2741c5f89f448a0171 100644 --- a/src/app/(dashboard)/user/UserTableRow.jsx +++ b/src/app/(dashboard)/user/UserTableRow.jsx @@ -67,12 +67,12 @@ const UserTableRow = ({ email, last_name, first_name, id, setUsers, role, projec })}
- -
- -
diff --git a/src/app/(dashboard)/user/UsersFilter.jsx b/src/app/(dashboard)/user/UsersFilter.jsx index a2f65f9fdca609edac56658f3d957fb8a0502d80..c284fe734a390def8d4c8830e7d9e9c4f310bc35 100644 --- a/src/app/(dashboard)/user/UsersFilter.jsx +++ b/src/app/(dashboard)/user/UsersFilter.jsx @@ -1,40 +1,43 @@ -import React from 'react' -import SearchIcon from "@/static/image/svg/search.svg" -const UsersFilter = ({ setFilter }) => { +import React, { memo } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +const UsersFilter = memo(function page({ setFilter, filter }) { const handleChangeFilter = (event) => { const name = event.target.name const value = event.target.value setFilter((filter) => ({ ...filter, [name]: value })) } + const handleResetFilters = () => { + setFilter({ first_name: "", last_name: "", email: "", project: "" }) + } return (

Recherche

- +
- +
- +
- +
- {/*
- -
*/} +
) -} +}) export default UsersFilter \ No newline at end of file diff --git a/src/app/(dashboard)/user/page.jsx b/src/app/(dashboard)/user/page.jsx index 8f5795faa0d9353e48647e70ade6da34957de4c2..d032b245af5e17ad24d2292410924c5cfcf29264 100644 --- a/src/app/(dashboard)/user/page.jsx +++ b/src/app/(dashboard)/user/page.jsx @@ -11,7 +11,6 @@ import UserTableRow from './UserTableRow'; import { PAGINATION_SIZE } from '../../lib/constants'; import ArrowRightIcon from "@/static/image/svg/chevron-right.svg" import ArrowLeftIcon from "@/static/image/svg/chevron-left.svg" -import SubSideBar from '@/app/ui/SubSideBar'; import UsersFilter from './UsersFilter'; const UserPage = () => { const [users, setUsers] = useState([]) @@ -52,28 +51,26 @@ const UserPage = () => { setUsers([newUser, ...users]) } - const subLinks = [{ label: "Utilisateurs", link: "/user" }, { label: "Rôles", link: "/role" }, { label: "Habilitations", link: "/privilege" }, { label: "Projets", link: "/projects" }] return ( <> - - -
+ +
{openCreatePopup && } {userToUpdate && }

Liste des utilisateurs

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

Pas encore des utilisateurs

+ ?
+

Aucun utilisateurs n'a été trouvé

: -
+
@@ -89,7 +86,7 @@ const UserPage = () => {
Prénom
} } -
+
{(paginationData) &&
{(paginationData.currentPage > 1) &&
getUsers(paginationData.currentPage - 1)} className='flex cursor-pointer hover:bg-neutral-200 duration-150 delay-75 h-8 w-9 items-center justify-center'> diff --git a/src/app/(dashboard)/zone/CreateZone.jsx b/src/app/(dashboard)/zone/CreateZone.jsx new file mode 100644 index 0000000000000000000000000000000000000000..6ff7581fb980d01ec068cc0b98ced560cff2e3a0 --- /dev/null +++ b/src/app/(dashboard)/zone/CreateZone.jsx @@ -0,0 +1,109 @@ +import Loader from '@/components/Loader/Loader' +import React, { useState, useRef, useEffect, useMemo } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import { useNotification } from '@/context/NotificationContext' +const CreateZone = ({ appendZone, setIsOpen, etages }) => { + const [isLoadingAction, setIsLoadingAction] = useState(false) + const [selectedEtage, setSelectedEtage] = useState(null) + const [nomZone, setNomZone] = useState(null) + + const inputRef = useRef(null) + const selectRef = useRef(null) + const { toggleNotification } = useNotification() + + + + + const handleSubmit = async (event) => { + event.preventDefault() + setIsLoadingAction(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/zones/", { + method: "POST", + body: JSON.stringify({ nom: nomZone, id_etage: selectedEtage }) + }) + if (isSuccess) { + setIsLoadingAction(false) + appendZone((prevZoneValue) => [...prevZoneValue, { ...data, id_etage: etages.find(etage => etage.id === data.id_etage) }]); + inputRef.current.value = "" + selectRef.current.value = "" + setNomZone(null) + setSelectedEtage(null) + setIsOpen(false) + toggleNotification({ + visible: true, + message: "La zone a été créer avec succès.", + type: "success" + }) + } else { + setIsLoadingAction(false) + if (errors.type === "ValidationError") { + if (errors.detail.non_field_errors) { + toggleNotification({ + type: "warning", + message: "Le nom de la zone saisie déjà existe.", + visible: true, + }) + } + } else { + setIsOpen(false) + toggleNotification({ + type: "error", + message: "Une erreur s'est produite lors de la création de la zone.", + visible: true, + }) + } + console.log(errors) + } + } + + + const handleChangeZone = (event) => { + setNomZone(event.target.value) + } + + const handleChangeEtage = (event) => { + setSelectedEtage(event.target.value) + } + + return ( +
+
+ {(etages) ? +
+

Ajouter zone

+
+
+
+ + +
+
+ + +
+
+
+ + +
+
+
+ : +
} +
+
+ ) +} + +export default CreateZone \ No newline at end of file diff --git a/src/app/(dashboard)/zone/RowZone.jsx b/src/app/(dashboard)/zone/ZoneTableRow.jsx similarity index 59% rename from src/app/(dashboard)/zone/RowZone.jsx rename to src/app/(dashboard)/zone/ZoneTableRow.jsx index eac3a4cc6a29bbcbf676e097a3a2cb439d53f9fe..df671e854e3987a880dfac6ba27621b1ac8a3eeb 100644 --- a/src/app/(dashboard)/zone/RowZone.jsx +++ b/src/app/(dashboard)/zone/ZoneTableRow.jsx @@ -1,19 +1,15 @@ -"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 React, { useState, useRef, useEffect } from 'react' +import fetchRequest from '@/app/lib/fetchRequest' +import DeleteIcon from "@/static/image/svg/trash-bold.svg" +import EditIcon from "@/static/image/svg/edit-bold.svg" import CheckIcon from "@/static/image/svg/check.svg" +import CancelIcon from "@/static/image/svg/cancel.svg" import { useNotification } from '@/context/NotificationContext' -import ConfirmationModal from "@/app/ui/ConfirmationModal"; - - +import ConfirmationModal from '@/app/ui/ConfirmationModal' +import Loader from '@/components/Loader/Loader' -const RowZone = ({ id, nom, etage, zonesState, etages }) => { - //states +const ZoneTableRow = ({ id, nom, etage, zonesState, etages }) => { const [isUpdating, setIsUpdating] = useState(false) const [zoneName, setZoneName] = useState(nom) const [selectedEtage, setSelectedEtage] = useState(etage) @@ -175,52 +171,54 @@ const RowZone = ({ id, nom, etage, zonesState, etages }) => { }; return ( - - - setZoneName(event.target.value)} defaultValue={nom} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - - - setZoneName(event.target.value)} defaultValue={nom} type='text' className='border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none bg-transparent' /> + + + + + + {!isUpdating + ?
+ + +
+ :
+ + +
} - - - - {!isUpdating - ?
- - -
- :
- - -
- } - + + setModalOpen(false)} onConfirm={handleConfirmDelete} - message={`Êtes-vous sûr de vouloir supprimer la zone "${nom}"?`} + message={`Voulez-vous vraiment supprimer la zone ?`} /> - + + ) } - -export default RowZone \ No newline at end of file +export default ZoneTableRow \ No newline at end of file diff --git a/src/app/(dashboard)/zone/ZonesFilter.jsx b/src/app/(dashboard)/zone/ZonesFilter.jsx new file mode 100644 index 0000000000000000000000000000000000000000..0da9e91a748bdc6c820cacea8d53877e8ce6f965 --- /dev/null +++ b/src/app/(dashboard)/zone/ZonesFilter.jsx @@ -0,0 +1,77 @@ +import React, { memo, useEffect, useState } from 'react' +import ResetIcon from "@/static/image/svg/reset.svg" +import Select from "react-select"; +import fetchRequest from '@/app/lib/fetchRequest'; +const ZonesFilter = memo(function Page({ setFilter, filter, etages }) { + const handleChangeZoneName = (event) => { + const value = event.target.value + setFilter({ ...filter, "zone": value }) + } + const handleResetFilters = () => { + setFilter({ "zone": "", "etages": [] }) + } + + const handleEtagesChange = (selectedOptions) => { + setFilter({ ...filter, etages: selectedOptions }) + } + return ( +
+

Recherche

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

List des Zones

- {isArray(zones) && zones?.length !== 0 && isArray(etages) && etages?.length !== 0 ? -
- - - - - - - {(selectedEtage) ? - zones?.filter(zone => zone.id_etage.id == selectedEtage).map((element) => {return }) - : - zones?.map((element) => { - return - })} -
ZoneEtageAction
-
- : -
-

Pas encore des zones

-
} - - : -
- } + <> + +
+ {openCreatePopup && } +
+

Liste des Zones

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

Aucune zone n'a été trouvé.

+
+ :
+ + + + + + + {zones?.map((element) => { + return + })} +
ZoneÉtagesActions
+
+ }}
-
- + ) } diff --git a/src/app/auth/change-password/page.jsx b/src/app/auth/change-password/page.jsx index 6bc46caa9f1a0aa71d39d43d249f2fb50b0bbafd..235d187eb54de5f548100a684bbd91bbf4e76883 100644 --- a/src/app/auth/change-password/page.jsx +++ b/src/app/auth/change-password/page.jsx @@ -2,11 +2,12 @@ import fetchRequest from '@/app/lib/fetchRequest' import Image from 'next/image' import Link from 'next/link' -import React, { useEffect, useState } from 'react' -import { useSearchParams } from 'next/navigation' +import React, {useEffect, useState} from 'react' +import {useSearchParams} from 'next/navigation' import Loader from '@/components/Loader/Loader' -import { PASSWORD_REGEX } from '@/app/lib/constants' -import { useNotification } from '@/context/NotificationContext' +import {PASSWORD_REGEX} from '@/app/lib/constants' +import {useNotification} from '@/context/NotificationContext' +import TeambookVertical from "@/static/image/teambook-vertical.png"; const ChangePassword = () => { const [isSuccess, setIsSuccess] = useState(false) @@ -15,11 +16,11 @@ const ChangePassword = () => { const [confirmPassword, setConfirmPassword] = useState("") const [isLoading, setIsLoading] = useState(false) const params = useSearchParams(); - const { toggleNotification } = useNotification() + const {toggleNotification} = useNotification() const handleChangePassword = async (event) => { event.preventDefault() setIsLoading(true) - const { isSuccess, data, errors } = await fetchRequest(`/password_reset/confirm/?token=${params.get("token")}`, { + const {isSuccess, data, errors} = await fetchRequest(`/password_reset/confirm/?token=${params.get("token")}`, { method: "POST", body: JSON.stringify({ password, @@ -28,21 +29,18 @@ const ChangePassword = () => { }) if (isSuccess) { setIsSuccess(true) - } - else { + } else { console.log(errors) setIsLoading(false) if (errors.type === "ValidationError") { if (errors.detail.token) { setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est invalide"]) - } - else { + } else { setFormErrors(["Le Mot de passe est invalide"]) } } else if (errors?.detail?.detail?.startsWith("The OTP password entered is not valid")) { setFormErrors(["Le lien que vous avez utilisé pour réinitialiser votre mot de passe est déja utilisé"]) - } - else { + } else { toggleNotification({ type: "error", message: "Internal Server Error", @@ -66,60 +64,61 @@ const ChangePassword = () => { setFormErrors(currentErrors) }, [password, confirmPassword]) return ( -
-
-
-
- teamwill -
- {(!isSuccess) &&
-

Change your password. -

-
-
    -
  • Le mot de passe doit contenir au moins 8 caractères
  • -
  • Le mot de passe doit contenir au moins un chiffre
  • -
  • Le mot de passe doit contenir au moins un caractère spécial
  • -
+
+
+ {TeambookVertical} +
+ {(!isSuccess) && +
+

Changer votre Mot de passe +

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

    The password has been changed!

    - log in again?
    - )} + +
      0 && !isEmptyFields ? "bg-red-100 border border-red-300 min-h-10" : ""} w-full px-3 text-xs py-3 rounded relative mt-9 list-inside list-disc`} + role="alert"> + {!isEmptyFields && formErrors.map((error, index) => { + return
    • {error}
    • + })} +
    + +
} + {(isSuccess) && ( +
+

The password has been changed!

+ log in + again?
-
-
+ )} +
) } diff --git a/src/app/auth/forgot-password/page.jsx b/src/app/auth/forgot-password/page.jsx index d74d5db9ab4b3f3bd45be27257ce6fead4b790b3..434c4237f9113dcd94ce2b11265a98e151ca0e4a 100644 --- a/src/app/auth/forgot-password/page.jsx +++ b/src/app/auth/forgot-password/page.jsx @@ -5,6 +5,8 @@ import { useNotification } from '@/context/NotificationContext' import Image from 'next/image' import Link from 'next/link' import React, { useState } from 'react' +import TeambookVertical from "@/static/image/teambook-vertical.png"; +import UserIcon from "@/static/image/svg/perm_identity.svg"; const ForgotPassword = () => { const [email, setEmail] = useState("") @@ -40,39 +42,31 @@ const ForgotPassword = () => { } } return ( -
-
-
- teamwill - {(!isSuccess) &&
-
-

Forgot your password!! No Problem - Reset it here +
+ {TeambookVertical} + {(!isSuccess) &&
+
+

Mot de passe oublié!! Aucun problème + Réinitialisez-le ici

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

Email Sent!

Check your email and open the link we sent to continue

} -
-

) } diff --git a/src/app/auth/layout.jsx b/src/app/auth/layout.jsx new file mode 100644 index 0000000000000000000000000000000000000000..27b0f05df286f35aa68a77ba58383b0f6aef9cbe --- /dev/null +++ b/src/app/auth/layout.jsx @@ -0,0 +1,48 @@ +import React from "react"; +import Link from "next/link"; +import Image from "next/image"; +import Hero from "@/static/image/pc_phone.png"; +import LineWhite from "@/static/image/svg/line-white4.svg"; +import LineSushi from "@/static/image/svg/line-sushi.svg"; + +const layout = ({children}) => { + return ( + <> +
+
+ {children} + +
+ + Aide + + + Vos questions + +
+
+
+
+ +
+
+ Hero +
+
+ + Teambook 2024 © + + + Politique de confidentialité + + + Termes et conditions + +
+
+
+ + ) +} + +export default layout \ No newline at end of file diff --git a/src/app/auth/login/page.jsx b/src/app/auth/login/page.jsx index 2bdb9a070baaa5983da96f4d5185e8c7919b7d6e..690dcca62084ba67dd408c19c1d355e0a3bff024 100644 --- a/src/app/auth/login/page.jsx +++ b/src/app/auth/login/page.jsx @@ -1,11 +1,15 @@ 'use client'; -import { useState } from 'react'; +import {useState} from 'react'; import Link from 'next/link'; import Cookies from 'js-cookie'; -import { createSession } from "@/app/lib/session"; -import { useRouter } from 'next/navigation' +import {createSession} from "@/app/lib/session"; +import {useRouter} from 'next/navigation' import Loader from '@/components/Loader/Loader'; +import UserIcon from "@/static/image/svg/perm_identity.svg"; +import VisibilityIcon from "@/static/image/svg/visibility_off.svg"; +import Image from "next/image"; +import TeambookVertical from "@/static/image/teambook-vertical.png"; const LoginPage = () => { const [username, setUsername] = useState(''); @@ -13,6 +17,11 @@ const LoginPage = () => { const [messages, setMessages] = useState(''); const [isLoading, setIsLoading] = useState(false) const router = useRouter(); + const [isPasswordVisible, setIsPasswordVisible] = useState(false); + + const togglePasswordVisibility = () => { + setIsPasswordVisible(!isPasswordVisible); + }; const handleSubmit = (event) => { event.preventDefault(); @@ -31,7 +40,7 @@ const LoginPage = () => { headers: { 'Content-Type': 'application/json', }, - body: JSON.stringify({ username, password }), + body: JSON.stringify({username, password}), }); const data = await response.json(); console.log(data) @@ -40,7 +49,12 @@ const LoginPage = () => { setMessages("Email ou mot de passe incorrect. Veuillez réessayer."); } else { await createSession(data); - router.push('/'); + // if role ='super-admin'or 'rh' role redirect to reporting + if (data.role === 'super-admin' || data.role === 'rh') { + router.push('/reporting') + } else { + router.push('/reservation'); + } } } catch (error) { setMessages('An error occurred'); @@ -48,54 +62,63 @@ const LoginPage = () => { }; return ( <> -
-
- teamwill + +
+ {TeambookVertical} +

Connectez-vous à notre plateforme

- - setUsername(e.target.value)} - /> + +
+ setUsername(e.target.value)} + /> + +
- - setPassword(e.target.value)} - /> + +
+ setPassword(e.target.value)} + /> + +
-

Mot de passe oublié?

+

Mot de + passe oublié?

- {messages && ( -

- {messages} -

- )} + {messages && ( +

+ {messages} +

+ )}
diff --git a/src/app/globals.css b/src/app/globals.css index d341d363379d5559c9d6780fb5d6e4e83e3b8275..93bcbc1b23fa6785133737a934fccda09649c13a 100644 --- a/src/app/globals.css +++ b/src/app/globals.css @@ -9,10 +9,9 @@ } } -/* Customize the scrollbar */ ::-webkit-scrollbar { width: 6px; - /* Set the width of the scrollbar */ + height: 6px; } .hideScroll::-webkit-scrollbar { @@ -123,7 +122,7 @@ height: 4px; width: 17px; border-radius: 5px; - background-color: white; + background-color: #bfcc8a; position: relative; transition: all 0.2s; } @@ -145,7 +144,7 @@ height: 4px; width: 25px; border-radius: 5px; - background-color: white; + background-color: #bfcc8a; transform: rotate(-45deg) translate(6px, -6px); transition: transform 0.2s; } @@ -158,7 +157,7 @@ height: 4px; width: 25px; border-radius: 5px; - background-color: white; + background-color: #bfcc8a; transform: rotate(45deg) translate(5px, 6px); transition: transform 0.2s; } @@ -171,7 +170,7 @@ height: 4px; width: 25px; border-radius: 5px; - background-color: white; + background-color: #bfcc8a; transform: rotate(0) translate(0, 0); transition: transform 0.2s; } @@ -184,7 +183,7 @@ height: 4px; width: 25px; border-radius: 5px; - background-color: white; + background-color: #bfcc8a; transform: rotate(0) translate(0, 0); transition: transform 0.2s; } @@ -213,4 +212,18 @@ .has-tooltip:hover .tooltip { @apply visible z-50; +} + + +.triangle { + width: 0; + height: 0; + border-left: 12px solid transparent; + border-right: 12px solid transparent; + border-bottom: 18px solid black; + transform: rotate(-90deg); +} + +.rotateProjectName { + transform: rotate(-90deg); } \ No newline at end of file diff --git a/src/app/lib/DateHelper.js b/src/app/lib/DateHelper.js index f4aac7651c2645bb2c14673c40a1bfa538c50039..39a4aa33e49972c5663b6f8b9460fd9484bec9be 100644 --- a/src/app/lib/DateHelper.js +++ b/src/app/lib/DateHelper.js @@ -1,32 +1,144 @@ -export const subtractDays = (date, numberOfDays) => { - const result = new Date(date); - result.setDate(result.getDate() - numberOfDays); - return result; +// 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; +// } + +/** + * Convert a week string (YYYY-W##) to a date object representing the first day of that week. + * @param {string} weekString - The week string in format 'YYYY-W##'. + * @returns {Date} The date object representing the first day of the week. + */ +const getFirstDayOfWeek = (weekString) => { + const [year, week] = weekString.split('-W').map(Number); + const simple = new Date(year, 0, 1 + (week - 1) * 7); + const dow = simple.getDay(); + let ISOweekStart = new Date(simple); + + if (dow <= 4) { + ISOweekStart.setDate(simple.getDate() - simple.getDay() + 1); + } else { + ISOweekStart.setDate(simple.getDate() + 8 - simple.getDay()); + } + + // Adjust for time zone offset to ensure consistency + ISOweekStart.setHours(0, 0, 0, 0); + + return ISOweekStart; +}; +/** + * Get the start and end dates of the week given a week string (YYYY-W##). + * @param {string} weekString - The week string in format 'YYYY-W##'. + * @returns {Object} An object with `weekStart` and `weekEnd` dates. + */ +export const getWeekRange = (weekString) => { + const startDate = getFirstDayOfWeek(weekString); + const endDate = new Date(startDate); + endDate.setDate(startDate.getDate() + 6); + + // Adjust for time zone offset to ensure consistency + endDate.setHours(23, 59, 59, 999); + + return { + weekStart: extractDate(startDate), + weekEnd: extractDate(endDate) + }; }; /** - * date {Date} - * @return 2024-10-12 + * Extract the date in 'YYYY-MM-DD' format. + * @param {Date} date - The date to format. + * @returns {string} The formatted date string. + * @throws {Error} If the input is not an instance of Date. */ 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") -} + if (date instanceof Date) { + return date.toJSON().split("T")[0]; + } else { + throw new Error("date isn't instance of Date in extractDate Util function"); + } +}; +/** + * Get the weeks between two week strings (YYYY-W##). + * @param {string} fromWeek - The start week string in format 'YYYY-W##'. + * @param {string} toWeek - The end week string in format 'YYYY-W##'. + * @returns {Array} An array of objects containing the start and end dates of each week. + */ +export const getWeeksBetween = (fromWeek, toWeek) => { + const fromDate = getFirstDayOfWeek(fromWeek); + const toDate = getFirstDayOfWeek(toWeek); + const weeks = []; -export const getDateRange = (fromDate, toDate) => { - const startDate = new Date(fromDate); - const endDate = new Date(toDate); + let currentDate = fromDate; + while (currentDate <= toDate) { + const weekEnd = new Date(currentDate); + weekEnd.setDate(currentDate.getDate() + 6); - const dateArray = []; + // Adjust for time zone offset to ensure consistency + weekEnd.setHours(23, 59, 59, 999); - let currentDate = startDate; - while (currentDate <= endDate) { - dateArray.push(currentDate.toISOString().split('T')[0].split("-").reverse().join("-")); + const adjustedStartDate = new Date(currentDate); + adjustedStartDate.setDate(currentDate.getDate() + 1); + weeks.push({ + axisX: `${extractDate(adjustedStartDate)} -- ${extractDate(weekEnd)}`, + weekStart: extractDate(currentDate), + weekEnd: extractDate(weekEnd) + }); - currentDate.setDate(currentDate.getDate() + 1); + currentDate.setDate(currentDate.getDate() + 7); } - return dateArray; + return weeks; +}; + +/** + * Calculates the difference in days between two dates. + * + * @param {string} date1 - The first date in 'YYYY-MM-DD' format. + * @param {string} date2 - The second date in 'YYYY-MM-DD' format. + * @returns {number} The difference in days between the two dates. + */ +export function dateDiffInDays(date1, date2) { + const dateObj1 = new Date(date1); + const dateObj2 = new Date(date2); + + dateObj1.setHours(0, 0, 0, 0); + dateObj2.setHours(0, 0, 0, 0); + + const timestamp1 = dateObj1.getTime(); + const timestamp2 = dateObj2.getTime(); + + const differenceInMillis = timestamp2 - timestamp1 + + const differenceInDays = Math.ceil(differenceInMillis / (1000 * 60 * 60 * 24)); + + return differenceInDays; } \ No newline at end of file diff --git a/src/app/lib/constants.js b/src/app/lib/constants.js index 9d34296b9316dac3c714bc0476dcfa97e05dda4d..18d2248eca10d6ac984cee828b3f8bc180176dea 100644 --- a/src/app/lib/constants.js +++ b/src/app/lib/constants.js @@ -1,3 +1,28 @@ export const PASSWORD_REGEX = /^(?=.*[a-zA-Z])(?=.*\d)(?=.*[^a-zA-Z\d\s]).*$/; export const EMAIL_REGEX = /^[a-zA-Z0-9._%+-]+@teamwillgroup\.com$/ -export const PAGINATION_SIZE = 3 \ No newline at end of file +export const PAGINATION_SIZE = 3 + +export const blankData = { + planning_data: [ + { + week: 1, + days: [{}, {}, {}, {}, {}] + }, + { + week: 2, + days: [{}, {}, {}, {}, {}] + }, + { + week: 3, + days: [{}, {}, {}, {}, {}] + }, + { + week: 4, + days: [{}, {}, {}, {}, {}] + }, + { + week: 5, + days: [{}, {}, {}, {}, {}] + }, + ] +} \ No newline at end of file diff --git a/src/app/lib/fetchRequest.js b/src/app/lib/fetchRequest.js index db2aa42c283fbd6f28b038af117d253871396c2c..bf513fe8911db983d3ba59ed500ac635c92d86fe 100644 --- a/src/app/lib/fetchRequest.js +++ b/src/app/lib/fetchRequest.js @@ -1,33 +1,44 @@ import Cookies from 'js-cookie'; import { decrypt } from "@/app/lib/session"; -const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000" +const BASE_URL = process.env.NEXT_PUBLIC_API_URL || "http://localhost:8000"; + const fetchRequest = async (url, options = {}) => { const jwtCookie = Cookies.get('session'); - console.log('jwtCookie', jwtCookie) + console.log('jwtCookie', jwtCookie); const jwtDecrypted = await decrypt(jwtCookie); - console.log('jwtDecrypted', jwtDecrypted?.sessionData?.token) + console.log('jwtDecrypted', jwtDecrypted?.sessionData?.token); + const headers = { ...options.headers, - // add authorization header with token if there is jwtDecrypted.sessionData.token + // Add authorization header with token if there is jwtDecrypted.sessionData.token Authorization: jwtDecrypted?.sessionData?.token ? `Token ${jwtDecrypted.sessionData.token}` : undefined, - 'Content-Type': 'application/json', }; + // Check if the body is FormData + const isFormData = options.body instanceof FormData; + + // If the body is not FormData, set Content-Type to application/json + if (!isFormData) { + headers['Content-Type'] = 'application/json'; + } + const response = await fetch(`${BASE_URL}${url}`, { ...options, headers, }); - console.log(response.status) + + console.log(response.status); if (!response.ok) { try { const errorData = await response.json(); - return { isSuccess: false, errors: errorData, data: null, status: response.status } + return { isSuccess: false, errors: errorData, data: null, status: response.status }; } catch (error) { - return { isSuccess: false, errors: error, data: null, status: response.status } + return { isSuccess: false, errors: error, data: null, status: response.status }; } } - console.log('response', response) + + 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'); @@ -39,4 +50,4 @@ const fetchRequest = async (url, options = {}) => { return { isSuccess: true, errors: null, data: data, status: response.status }; }; -export default fetchRequest; +export default fetchRequest; \ No newline at end of file diff --git a/src/app/page.js b/src/app/page.js index 87ce7aa74a17bf1ba51f0e9979b2d955634bf8ca..d76fd4b79a1bc5b9f3169a6fd4c60fbd59f72ca8 100644 --- a/src/app/page.js +++ b/src/app/page.js @@ -1,5 +1,6 @@ export default function Home() { + return (
Home Page diff --git a/src/app/ui/Accordion.jsx b/src/app/ui/Accordion.jsx new file mode 100644 index 0000000000000000000000000000000000000000..9d46cd956fc650987ae0c96bf616e63b6458c2aa --- /dev/null +++ b/src/app/ui/Accordion.jsx @@ -0,0 +1,34 @@ +import { useState } from 'react'; + +const Accordion = ({ title, children }) => { + const [isOpen, setIsOpen] = useState(false); + + return ( +
+ + {isOpen && ( +
+ {children} +
+ )} +
+ ); +}; + +export default Accordion; diff --git a/src/app/ui/Header.jsx b/src/app/ui/Header.jsx index 13bc0221faba899a5b28461da7c20f2c021567c4..79c3de1582410aa774edf207ff49da647c1f911a 100644 --- a/src/app/ui/Header.jsx +++ b/src/app/ui/Header.jsx @@ -30,7 +30,7 @@ const Header = async () => {
  • -
  • +
  • diff --git a/src/app/ui/LogoutButton.js b/src/app/ui/LogoutButton.jsx similarity index 100% rename from src/app/ui/LogoutButton.js rename to src/app/ui/LogoutButton.jsx 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/app/ui/ReservationConfirmation.jsx b/src/app/ui/ReservationConfirmation.jsx new file mode 100644 index 0000000000000000000000000000000000000000..7ec4532989061a06377bd22b4d0244ea0dbff54a --- /dev/null +++ b/src/app/ui/ReservationConfirmation.jsx @@ -0,0 +1,76 @@ +import Loader from '@/components/Loader/Loader'; +import React from 'react' +import WarningIcon from "@/static/image/svg/warning.svg" +const ReservationConfirmation = ({ isOpen, onClose, onConfirm, isLoading, reservationData, type, message, title }) => { + if (!isOpen) return null; + if (type === "create") + return ( +
    +
    +
    +

    Confirmation de Réservation Place

    +

    Êtes-vous sûr de vouloir confirmer la réservation de la place {reservationData.placeName || ""} à la table {reservationData.tableName || ""} dans la zone {reservationData.zoneName || ""}

    +

    Merci de confirmer votre présence le {reservationData.date.split("-").reverse().join("/") || ""}

    +
    + + +
    +
    +
    +
    + ) + else if (type === "cancel") return ( +
    +
    +
    +

    Confirmation d'annulation de Réservation

    +

    Êtes-vous sûr de vouloir annuler la réservation de la place {reservationData.placeName || ""} à la table {reservationData.tableName || ""} dans la zone {reservationData.zoneName || ""}

    +
    + + +
    +
    +
    +
    ) + else if (type === "location") return
    +
    +
    +
    + +

    {title}

    +
    +

    {message}

    +

    Veuillez vérifier et mettre à jour votre position, puis reconfirmer votre présence.

    +
    + +
    +
    +
    +
    + else if (type === "presence") return ( +
    +
    +
    +

    Validation de votre présence

    +

    Votre présence est validée avec succès.

    +
    + +
    +
    +
    +
    ) +} + +export default ReservationConfirmation \ No newline at end of file diff --git a/src/app/ui/SideBar.jsx b/src/app/ui/SideBar.jsx index ba45eace93b079bae81c0cc00950bb4f78bf2e04..fd06833e44d6d9e342637f54bd603879e3747f02 100644 --- a/src/app/ui/SideBar.jsx +++ b/src/app/ui/SideBar.jsx @@ -1,12 +1,36 @@ "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 isAuthenticated from "@/app/lib/isAuthenticated"; -import {useEffect, useState} from "react"; +import { useEffect, useState } from "react"; import { usePathname } from "next/navigation"; import './SideBar.css'; +import Link from "next/link"; +import Icone_administration from "@/static/image/svg/store.svg" +import Icone_acceuil from "@/static/image/svg/dashboard.svg" +import Icone_users from "@/static/image/svg/users.svg" +import Icone_setting from "@/static/image/svg/settings.svg" +import Icone_chair from "@/static/image/svg/chair.svg" + +export const AdminLinks = { + "Gestion": [ + { label: "Utilisateurs", link: "/user", privilege: "user" }, + { label: "Rôles", link: "/role", privilege: "role" }, + { label: "Habilitations", link: "/privilege", privilege: "privilege" }, + { label: "Projets", link: "/projects", privilege: "projects" }, + ], + "Planning": [ + { label: "Planning", link: "/planning", privilege: "planning" }, + ], + "Zones": [ + { label: "Etage", link: "/etage", privilege: "etage" }, + { label: "Zone", link: "/zone", privilege: "zone" }, + { label: "Tables", link: "/table", privilege: "table" }, + { label: "Places", link: "/place", privilege: "place" }, + { label: "Affectation projects", link: "/assign-zone-project", privilege: "assign-zone-project" }, + ] +} + const SideBar = () => { const [isAuth, setIsAuth] = useState(false); @@ -23,64 +47,57 @@ const SideBar = () => { checkAuth(); }, []); + const AdministrationLinks = { ...AdminLinks } + + Object.keys(AdministrationLinks).forEach((key) => { + const newArray = AdministrationLinks[key].filter((link) => sessionData?.privileges.includes(link.privilege)) + if (!newArray.length) delete AdministrationLinks[key] + else AdministrationLinks[key] = newArray + }); + if (!isAuth || !sessionData) { return ( ); } - const AdminLinks = [ - { label: "Utilisateurs", link: "/user", icon: , privilege: "user" }, - { label: "Habilitations", link: "/privilege", icon: , privilege: "privilege" }, - { label: "Rôles", link: "/role", icon: , privilege: "role" }, - { label: "Projets", link: "/projects", icon: , privilege: "projects" }, - { label: "Réservation", link: "/reservation", icon: , privilege: "reservation" }, - { label: "Type de Presence", link: "/planning/type-presence", icon: , privilege: "planning/type-presence" }, - { label: "Planning", link: "/planning", icon: , privilege: "planning" }, - { label: "Etage", link: "/etage", icon: , privilege: "etage" }, - { label: "Zone", link: "/zone", icon: , privilege: "zone" }, - { label: "Tables", link: "/table", icon: , privilege: "table" }, - { label: "Places", link: "/place", icon: , privilege: "place" }, - { label: "Gestion des zones", link: "/assign-zone-project", icon: , privilege: "assign-zone-project" }, - { label: "Consulter les réservations", link: "/consultation-reservations", icon: , privilege: "consultation-reservations" }, - { label: "Reporting", link: "/reporting", icon: , privilege: "reporting" }, - ]; - - console.log('sessionDataSideBar', sessionData) - const filteredLinks = AdminLinks.filter(link => sessionData?.privileges.includes(link.privilege)); - console.log('filteredLinks', filteredLinks) - - const ConsultationLinks = [ - { - label: "Consulter les réservations", - link: "/consultation-reservations", - icon: - } - ] - + const adminSectionOpen = Object.values(AdministrationLinks).flatMap((element) => element).find((element) => element.link === pathname) !== undefined return ( +
    ); } export default SideBar; \ No newline at end of file diff --git a/src/app/ui/SideBarLink.jsx b/src/app/ui/SideBarLink.jsx index 387cd820afbfe86d8fb8e816d6502f831f46b500..b64503e115a609c884b02eb302c80ccb7e3bc6b6 100644 --- a/src/app/ui/SideBarLink.jsx +++ b/src/app/ui/SideBarLink.jsx @@ -1,9 +1,9 @@ 'use client' import Link from 'next/link'; import { usePathname } from 'next/navigation'; -import React from 'react'; +import React, { useLayoutEffect, useState } from 'react'; -const SideBarLink = ({ link, label }) => { +const SideBarLink = ({ links, label }) => { const pathname = usePathname(); const hideSideBar = () => { @@ -13,19 +13,19 @@ const SideBarLink = ({ link, label }) => { target.classList.replace("active", "notActive"); sideBar.classList.replace("active", "notActive"); } - console.log(target, sideBar); }; - - const isActive = pathname === link; - + const [isActive, setActive] = useState(false) + useLayoutEffect(() => { + setActive(links?.find((element) => element.link === pathname) !== undefined) + }, [pathname]) return (
    -
    - +
    +
    diff --git a/src/app/ui/SubSideBar.jsx b/src/app/ui/SubSideBar.jsx index 49a3794d7c109b1deb779701b9aef868fd595054..0f651cfd8752502b013c7807127170d1405081e3 100644 --- a/src/app/ui/SubSideBar.jsx +++ b/src/app/ui/SubSideBar.jsx @@ -1,13 +1,40 @@ "use client" import Link from 'next/link' import { usePathname } from 'next/navigation'; -import React from 'react' +import React, { useEffect, useLayoutEffect, useState } from 'react' +import { AdminLinks } from './SideBar'; +import isAuthenticated from '../lib/isAuthenticated'; -const SubSideBar = ({ items }) => { +const SubSideBar = () => { + const [link, setLink] = useState(null) + const [sessionData, setSessionData] = useState(null); const pathname = usePathname(); + + useEffect(() => { + const checkAuth = async () => { + const authResult = await isAuthenticated(); + setSessionData(authResult.sessionData); + }; + + checkAuth(); + }, []); + useLayoutEffect(() => { + if (sessionData) { + var foundLink = null + Object.keys(AdminLinks).forEach((key) => { + const isSelectedElement = AdminLinks[key].find((element) => element.link === pathname) + if (isSelectedElement) { + foundLink = AdminLinks[key] + } + }); + if (foundLink) setLink(foundLink.filter((link) => sessionData?.privileges.includes(link.privilege))) + else setLink(null) + } + }, [pathname, sessionData]) + if (!link || !sessionData || link.length === 1) return null return (
    - {items.map((element, index) => { + {link.map((element, index) => { if (pathname !== element.link) return

    {element.label}

    diff --git a/src/components/Notification/Error.jsx b/src/components/Notification/Error.jsx index 6329a934072ae8e2b30be32c6703019794afd7f4..d5e5efe98bad76471fb44dad5c97933c38e9f59b 100644 --- a/src/components/Notification/Error.jsx +++ b/src/components/Notification/Error.jsx @@ -1,5 +1,5 @@ import React, { useEffect, useRef } from 'react' -import WarningIcon from "@/static/image/svg/WarningIcon.svg" +import WarningIcon from "@/static/image/svg/warningIcon.svg" import CrossIcon from "@/static/image/svg/cross.svg" import "./Notification.css" diff --git a/src/middleware.js b/src/middleware.js index 4b0c14d6ce4510c6cd5bcaf787dc7c1c20408fa7..96893fec50d07af83707ba904052cc220de5bfe6 100644 --- a/src/middleware.js +++ b/src/middleware.js @@ -1,9 +1,9 @@ -import { NextResponse } from 'next/server' -import { decrypt } from '@/app/lib/session' -import { cookies } from 'next/headers' +import {NextResponse} from 'next/server' +import {decrypt} from '@/app/lib/session' +import {cookies} from 'next/headers' // 1. Specify protected and public routes -const protectedRoutes = ['/reporting', '/dashboard', '/auth/verif', '/users', '/privilege', '/projects', '/role', '/etage', '/place', '/zone', '/table', '/reservation', '/planning', '/planning/type-presence', '/assign-zone-project', '/consultation-reservations'] +const protectedRoutes = ['/reporting', '/dashboard', '/auth/verif', '/user', '/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) { @@ -33,6 +33,15 @@ export default async function middleware(req) { return NextResponse.redirect(new URL('/no-access', req.nextUrl)); } + // if route is / only redirect to reporting or reservation + if (path === '/') { + if (session?.sessionData.role === 'super-admin' || session?.sessionData.role === 'rh') { + return NextResponse.redirect(new URL('/reporting', req.nextUrl)); + } else { + return NextResponse.redirect(new URL('/reservation', req.nextUrl)); + } + } + // 7. Redirect to /dashboard if the user is authenticated if ( isPublicRoute && diff --git a/src/static/image/pc_phone.png b/src/static/image/pc_phone.png new file mode 100644 index 0000000000000000000000000000000000000000..959f6fd1b3b3b0897507029262a6860f64af46eb Binary files /dev/null and b/src/static/image/pc_phone.png differ diff --git a/src/static/image/svg/Icone_acceuil_gris.svg b/src/static/image/svg/Icone_acceuil_gris.svg new file mode 100644 index 0000000000000000000000000000000000000000..1ae4cf51229860d20e998100ef21a554903fbd27 --- /dev/null +++ b/src/static/image/svg/Icone_acceuil_gris.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/Icone_administration_gris.svg b/src/static/image/svg/Icone_administration_gris.svg new file mode 100644 index 0000000000000000000000000000000000000000..0f9894d4b326c8c0ed68c6101ab887ff2b70d246 --- /dev/null +++ b/src/static/image/svg/Icone_administration_gris.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/Icone_consultation_gris.svg b/src/static/image/svg/Icone_consultation_gris.svg new file mode 100644 index 0000000000000000000000000000000000000000..867e979247162638327e6e0c21e17313e8a78f37 --- /dev/null +++ b/src/static/image/svg/Icone_consultation_gris.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/Icone_parametre_gris.svg b/src/static/image/svg/Icone_parametre_gris.svg new file mode 100644 index 0000000000000000000000000000000000000000..1c8d9f0b9bd02ff9c0b3287e302bb80e3e20ee5b --- /dev/null +++ b/src/static/image/svg/Icone_parametre_gris.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/PlaceRed.svg b/src/static/image/svg/PlaceRed.svg new file mode 100644 index 0000000000000000000000000000000000000000..81aad7fd759bcf2502cdfdd676a9abc1eaf0dd44 --- /dev/null +++ b/src/static/image/svg/PlaceRed.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/image/svg/Vector.svg b/src/static/image/svg/Vector.svg new file mode 100644 index 0000000000000000000000000000000000000000..be2d165cb5943a1a50e902a304cd7554d10a63c1 --- /dev/null +++ b/src/static/image/svg/Vector.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/addchart.svg b/src/static/image/svg/addchart.svg new file mode 100644 index 0000000000000000000000000000000000000000..7ae7662c3831ff5fab55cf06ffa0b6df5d117acc --- /dev/null +++ b/src/static/image/svg/addchart.svg @@ -0,0 +1,3 @@ + + + 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/chair.svg b/src/static/image/svg/chair.svg new file mode 100644 index 0000000000000000000000000000000000000000..1ac2dce0eedf034a4918c51c465371ed2d3d4e55 --- /dev/null +++ b/src/static/image/svg/chair.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/config.svg b/src/static/image/svg/config.svg new file mode 100644 index 0000000000000000000000000000000000000000..d876b0a766894f5bdd0d56ed6ed228b47e4da37f --- /dev/null +++ b/src/static/image/svg/config.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/dashboard.svg b/src/static/image/svg/dashboard.svg new file mode 100644 index 0000000000000000000000000000000000000000..4180da86c9b68959d3bcebb6806d6cbdb8fca9f2 --- /dev/null +++ b/src/static/image/svg/dashboard.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/image.svg b/src/static/image/svg/image.svg new file mode 100644 index 0000000000000000000000000000000000000000..0f7ca5d797cd4bbe659c28de3b59f74a8a4c9a09 --- /dev/null +++ b/src/static/image/svg/image.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/image2.svg b/src/static/image/svg/image2.svg new file mode 100644 index 0000000000000000000000000000000000000000..5d8ea1ce901c9ba7cacd89b71081f2ad50ecac0f --- /dev/null +++ b/src/static/image/svg/image2.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/line-sushi.svg b/src/static/image/svg/line-sushi.svg new file mode 100644 index 0000000000000000000000000000000000000000..c65c942599d8926f413a438c1edce9c957f42256 --- /dev/null +++ b/src/static/image/svg/line-sushi.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/line-white4.svg b/src/static/image/svg/line-white4.svg new file mode 100644 index 0000000000000000000000000000000000000000..7451059151003e0f310414ca8e5ba8d645609f1c --- /dev/null +++ b/src/static/image/svg/line-white4.svg @@ -0,0 +1,3 @@ + + + diff --git a/src/static/image/svg/perm_identity.svg b/src/static/image/svg/perm_identity.svg new file mode 100644 index 0000000000000000000000000000000000000000..c2d199ac1c7dcf4c2ac8d36c1f5671749dd576b2 --- /dev/null +++ b/src/static/image/svg/perm_identity.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/static/image/svg/placeBlue.svg b/src/static/image/svg/placeBlue.svg new file mode 100644 index 0000000000000000000000000000000000000000..7694452f2f256202ba110a900606858f17e7d099 --- /dev/null +++ b/src/static/image/svg/placeBlue.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/image/svg/placeBooked.svg b/src/static/image/svg/placeBooked.svg new file mode 100644 index 0000000000000000000000000000000000000000..b6aec6d47d74901538bfc18570c59e203b0ad6e8 --- /dev/null +++ b/src/static/image/svg/placeBooked.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/image/svg/placePresent.svg b/src/static/image/svg/placePresent.svg new file mode 100644 index 0000000000000000000000000000000000000000..6b2f08248c551dc5ee7ee2588fc00090c0543c32 --- /dev/null +++ b/src/static/image/svg/placePresent.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/image/svg/placeUnavailable.svg b/src/static/image/svg/placeUnavailable.svg new file mode 100644 index 0000000000000000000000000000000000000000..54b047570187058172bc8a2b6826be7a2c2596cf --- /dev/null +++ b/src/static/image/svg/placeUnavailable.svg @@ -0,0 +1,22 @@ + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/static/image/svg/reset.svg b/src/static/image/svg/reset.svg new file mode 100644 index 0000000000000000000000000000000000000000..8522fc76088d97c39272651b514c654c8795387a --- /dev/null +++ b/src/static/image/svg/reset.svg @@ -0,0 +1 @@ + \ No newline at end of file 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/setting.svg b/src/static/image/svg/setting.svg new file mode 100644 index 0000000000000000000000000000000000000000..f2dce76bb7bf4cb5e8be6196e953545b27d8ebd0 --- /dev/null +++ b/src/static/image/svg/setting.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/settings.svg b/src/static/image/svg/settings.svg new file mode 100644 index 0000000000000000000000000000000000000000..9635ce66d199e295e80b9339e5fd439047d03fa1 --- /dev/null +++ b/src/static/image/svg/settings.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/store.svg b/src/static/image/svg/store.svg new file mode 100644 index 0000000000000000000000000000000000000000..f20a0163c751312badb5b98bc66415d928216928 --- /dev/null +++ b/src/static/image/svg/store.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/users.svg b/src/static/image/svg/users.svg new file mode 100644 index 0000000000000000000000000000000000000000..1313d17fb9483dd699111fbb8b5e5db0100dd4f6 --- /dev/null +++ b/src/static/image/svg/users.svg @@ -0,0 +1 @@ + \ No newline at end of file diff --git a/src/static/image/svg/visibility_off.svg b/src/static/image/svg/visibility_off.svg new file mode 100644 index 0000000000000000000000000000000000000000..ceec114e0af5bc13765cdff7667a78700b5360dc --- /dev/null +++ b/src/static/image/svg/visibility_off.svg @@ -0,0 +1,10 @@ + + + + + + + + + + diff --git a/src/static/image/svg/warning.svg b/src/static/image/svg/warning.svg new file mode 100644 index 0000000000000000000000000000000000000000..1aefbbde13c00ae4e8d583ce4a5a1ff29d45c711 --- /dev/null +++ b/src/static/image/svg/warning.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/src/static/image/teambook-vertical.png b/src/static/image/teambook-vertical.png new file mode 100644 index 0000000000000000000000000000000000000000..f4be112fc362181936e03ac429004a71e76a54a1 Binary files /dev/null and b/src/static/image/teambook-vertical.png differ diff --git a/tailwind.config.js b/tailwind.config.js index 910f62575320a3695ea9880b0fd217be746c3694..0726de941644e8dbd0fcc6728f41e993a5d35389 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -10,7 +10,7 @@ module.exports = { colors: { 'sushi': { '50': '#f7f8ed', - '100': '#ebefd8', + '100': '#EFF1E9', '200': '#d9e1b5', '300': '#bfcc8a', '400': '#a6b764', @@ -28,12 +28,25 @@ module.exports = { '300': '#b0b0b0', '400': '#888888', '500': '#6d6d6d', - '600': '#5c5c5c', // grayTW: "#5c5c5c", + '600': '#5c5c5c', // grayTW: "#5c5c5c", '700': '#4f4f4f', '800': '#454545', '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 : { @@ -52,5 +65,9 @@ module.exports = { lg: '976px', xl: '1440px', }, - plugins: [require('@tailwindcss/aspect-ratio')], + plugins: [require('@tailwindcss/aspect-ratio'), require('@tailwindcss/forms')({ strategy: "class" }), + function ({ addVariant }) { + addVariant('not-first', '&:not(:first-child)'); + }, + ], };