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..9d9c2224804cc518671a6e7fa659f59b3d89bbba --- /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 + // } + + 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..e7a343c4c6250fde953bf70a2a9b2505e258a558 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,44 @@ 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) { + console.log("getDistinctsProjectsAndZones", data) + 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 +108,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 +142,7 @@ const AffectingZoneProject = () => { } } getListOfAffectedProjects() - }, [selectedDay, selectedWeek]) + }, [filter.jours, filter.semaines, filter.projects, filter.zones]) const handleOpenAssignProject = () => { @@ -218,9 +251,90 @@ const AffectingZoneProject = () => { } - + console.log("listProjectsAffected", listProjectsAffected) 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éesPlaces disponiblePlaces occupéesActions
+ 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 +366,8 @@ const AffectingZoneProject = () => {
{(!isLoadingListProjects) ? (listProjectsAffected && listProjectsAffected.length > 0) ? - - +
+ {(listProjectsAffected.map((element, index) => - - {/* */} - + + ))} @@ -329,10 +443,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/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..32ddab3a79827fd99307377b5c0ecaf8b78fe2fd 100644 --- a/src/app/(dashboard)/consultation-reservations/page.jsx +++ b/src/app/(dashboard)/consultation-reservations/page.jsx @@ -1,34 +1,32 @@ '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"; 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 +45,23 @@ const Reservation = () => { getPlan() }, []) - useEffect(() => { - const getAllEtages = async () => { - try { - const { isSuccess, errors, data } = await fetchRequest('/zoaning/etages/', { method: 'GET' }) - setIsLoadingEtages(false) - if (isSuccess) { - setEtages(data) - } else { - setEtages([]) - } - } catch (error) { - setIsLoadingEtages(false) - console.log(error) - } - } - getAllEtages() - }, []) - - useEffect(() => { - 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 +72,20 @@ 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 + }))) + } else { console.log(errors) toggleNotification({ visible: true, @@ -108,12 +95,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 +109,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 +121,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 +145,42 @@ 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) + 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 handleChangeEtage = (event) =>{ - const etage_id = event.target.value - if(selectedEtage !== etage_id){ - setSelectedEtage(etage_id) - setSelectedZone(null) - setSelectedZone(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 +191,177 @@ 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) + 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}

+
+ } + )} +
+
+

Recherche

+
+
+ + setNameProjectFilter(event.target.value)} + type="text" name="name" id="name" + 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"/> +
+
+ + +
+
+ + +
+
+ +
+ + +
- {(!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 - }) - } -
- }) - : - 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 - })} +
+
+

Consultation des + réservations

+ {(filteredDatesData && filteredDatesData.length) && + } +
+ {(selectedZone.floorId && !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)).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..2d16cebac618d8f66a40d831cfda4d2e19dca3e3 --- /dev/null +++ b/src/app/(dashboard)/etage/CreateEtage.jsx @@ -0,0 +1,115 @@ +'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" + + +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 handleSubmit = async (event) => { + event.preventDefault() + setIsLoading(true) + const { data, errors, isSuccess } = await fetchRequest("/zoaning/etages/", { + method: "POST", + body: JSON.stringify({ numero: numeroEtage }) + }) + 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.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) + } + }, []) + + return
+ + + +} + +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..ff162b580369800b60cdeb49623f554d924fce04 --- /dev/null +++ b/src/app/(dashboard)/etage/EtageTableRow.jsx @@ -0,0 +1,96 @@ +'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' +const EtageTableRow = ({ id, numero, setEtages }) => { + const [numeroEtage, setNumeroEtage] = useState(numero) + const [loadingStatus, setLoadingStatus] = useState(false) + const { toggleNotification } = useNotification() + const [isModalOpen, setModalOpen] = useState(false) + const showDeletePopup = () => { + setModalOpen(true); + } + useEffect(() => { + setNumeroEtage(numero) + inputRef.current.value = numero + }, [numero]) + 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); + } + } + return ( + <> + + + + + 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..8deae510305aed6510cf98a804356ea6650668fe 100644 --- a/src/app/(dashboard)/etage/page.jsx +++ b/src/app/(dashboard)/etage/page.jsx @@ -1,125 +1,63 @@ -"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.

+
+ :
+
Date @@ -280,9 +394,9 @@ const AffectingZoneProject = () => {
+
Semaine: {element.semaine} - Jour: {element.jour} @@ -301,7 +415,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">
+ + +
+ + +
+
+ setNumeroEtage(event.target.value)} defaultValue={numero} 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' /> + +
+ +
+
+ + + + + {(isCreating) && setIsCreating(false)} appendEtage={appendEtage} />} + {etages?.map((element) => { + return + })} +
Numéro É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..2c23693535f98767b929ac83f713f8b91d4dbddc --- /dev/null +++ b/src/app/(dashboard)/place/PlacesFilter.jsx @@ -0,0 +1,106 @@ +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: [] }) + } + + return ( +
+

Recherche

+
+
+ + +
+
+ + setSelectedWeek(e.target.value)} className='rounded-md px-3 duration-150 delay-75 focus:ring ring-offset-1 ring-sushi-200 border h-10 border-neutral-500 outline-none'> + + + + + + + +
+ +
+
+
+
+ +
+
+ ) +}) + +export default PlacesFilter \ No newline at end of file diff --git a/src/app/(dashboard)/place/RowPlace.jsx b/src/app/(dashboard)/place/RowPlace.jsx deleted file mode 100644 index 32ffc3f7d85b462114f5dbfa8554c60a6f2b4479..0000000000000000000000000000000000000000 --- a/src/app/(dashboard)/place/RowPlace.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 RowPlace = ({ id, numero, table, placesState, tables, filteredPlacesState }) => { - console.log(table) - //states - const [isUpdating, setIsUpdating] = useState(false) - const [numPlace, setNumPlace] = useState(numero) - const [selectedTable, setSelectedTable] = useState(table) - const [loadingStatus, setLoadingStatus] = useState(false) - const [isModalOpen, setModalOpen] = useState(false); - const { toggleNotification } = useNotification() - //refs - const inputRef = useRef(null) - const selectRef = useRef(null) - const rowRef = useRef(null) - - //Logic - useEffect(() => { - setNumPlace(numero) - setSelectedTable(table?.id) - selectRef.current.value = table?.id - inputRef.current.value = numero - }, [numero, table]) - - const handleUpdatePlace = async () => { - setLoadingStatus(true) - const { isSuccess, errors, data, status } = await fetchRequest(`/zoaning/places/${id}/`, { - method: "PATCH", - body: JSON.stringify({ numero: numPlace, id_table: selectedTable }) - }) - setLoadingStatus(false) - if (isSuccess) { - if (data.message === "NO_CHANGES") { - toggleNotification({ - visible: true, - message: "Aucun changement n'a été effectué.", - type: "warning" - }) - setIsUpdating(false) - return - } - placesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) - filteredPlacesState((prevPlacesState) => prevPlacesState.map((element) => element.id === id ? { ...data.data, id_table: tables.find(table => table.id === data.data.id_table) } : element)) - setIsUpdating(false) - toggleNotification({ - visible: true, - message: "La place a été modifiée avec succès.", - type: "success" - }) - } else { - if (errors.type === "ValidationError") { - if (errors.detail.name) { - toggleNotification({ - type: "warning", - message: "Le numéro de la place déjà existe.", - visible: true, - }) - } else if (errors.detail.non_field_errors) { - toggleNotification({ - type: "warning", - message: "Le numéro de la place saisie existe déjà.", - visible: true, - }) - } - else { - toggleNotification({ - visible: true, - message: "Erreur de validation de la place", - type: "warning" - }) - } - } else if (status === 404) { - toggleNotification({ - visible: true, - message: "La place n'a pas été trouvé", - type: "warning" - }) - } else { - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - } - console.log(errors) - } - - } - - const handleDelete = async () => { - const { isSuccess, errors, status } = await fetchRequest(`/zoaning/places/${id}/`, { method: "DELETE" }) - if (isSuccess) { - placesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) - filteredPlacesState((prevPlacesState) => prevPlacesState.filter((element) => element.id !== id)) - toggleNotification({ - visible: true, - message: "La place a été supprimée avec succès", - type: "success" - }) - } else if (status == 404) { - toggleNotification({ - visible: true, - message: "La place n'a pas été trouvé", - type: "warning" - }) - } else { - console.log(errors) - toggleNotification({ - visible: true, - message: "Internal Server Error", - type: "error" - }) - } - } - - const cancelUpdate = () => { - setIsUpdating(false) - setNumPlace(numero) - setSelectedTable(table.id) - selectRef.current.value = table.id - inputRef.current.value = numero - } - - const handleUpdateBlur = (event) => { - const eventTarget = event.target - let isInsideRowRef = false; - let element = eventTarget; - while (element !== null) { - if (element === rowRef.current) { - isInsideRowRef = true; - break; - } - if (element.parentElement === null) { - isInsideRowRef = false; - break; - } - element = element.parentElement; - } - if (!isInsideRowRef && element?.classList.contains("placeRowSVG")) return; - if (!isInsideRowRef) { - cancelUpdate(); - document.removeEventListener("click", handleUpdateBlur); - } - } - - useEffect(() => { - if (isUpdating && inputRef?.current && selectRef?.current) { - inputRef.current.focus() - selectRef.current.focus() - document.addEventListener("click", handleUpdateBlur) - } - return () => { - document.removeEventListener("click", handleUpdateBlur) - } - }, [isUpdating]) - - const handleDeleteClick = () => { - setModalOpen(true); - } - - const handleConfirmDelete = () => { - handleDelete(); - setModalOpen(false); - }; - - return ( - - - setNumPlace(event.target.value)} defaultValue={numero} type='text' className='disabled:bg-white border-0 rounded-md px-2 enabled:drop-shadow border-none enabled:bg-gray-100 duration-100 h-10 outline-none' /> - - - - - - {!isUpdating - ?
- - -
- :
- - -
- } - - setModalOpen(false)} - onConfirm={handleConfirmDelete} - message={`Êtes-vous sûr de vouloir supprimer la place "${numero}"?`} - /> - - ) -} - - -export default RowPlace diff --git a/src/app/(dashboard)/place/page.jsx b/src/app/(dashboard)/place/page.jsx index 68f65d6352d6bc002d969f4f964e7c24536c3636..120a0ac121257463e3703e4d3a143408cafbc0ae 100644 --- a/src/app/(dashboard)/place/page.jsx +++ b/src/app/(dashboard)/place/page.jsx @@ -1,36 +1,47 @@ -"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: [] }); + console.log(filter) + 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 +86,6 @@ const Place = () => { return { etages, zones, tables }; } - const getAllTables = async () => { try { const { isSuccess, errors, data } = await fetchRequest('/zoaning/tables/', { method: 'GET' }) @@ -93,269 +103,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)/privilege/PrivilegesFilter.jsx b/src/app/(dashboard)/privilege/PrivilegesFilter.jsx index 306083f4772e2bb142a0606b921f2693ab3ef2b3..d251f0ec25a7bdee631202c022a730d0e4513a0a 100644 --- a/src/app/(dashboard)/privilege/PrivilegesFilter.jsx +++ b/src/app/(dashboard)/privilege/PrivilegesFilter.jsx @@ -19,7 +19,7 @@ const PrivilegesFilter = memo(function page({ setFilter, filter }) {
- diff --git a/src/app/(dashboard)/privilege/page.jsx b/src/app/(dashboard)/privilege/page.jsx index f7e796676219363d84f60122ce4bc01bdf8e059e..c05236641540fecb8cca8f975cd417af7e901455 100644 --- a/src/app/(dashboard)/privilege/page.jsx +++ b/src/app/(dashboard)/privilege/page.jsx @@ -6,7 +6,6 @@ import { isArray } from '../../lib/TypesHelper' import PrivilegeTableRows from './PrivilegeTableRow' import Loader from '@/components/Loader/Loader' import PrivilegesFilter from "./PrivilegesFilter" -import SubSideBar from '@/app/ui/SubSideBar' import AddIcon from "@/static/image/svg/add.svg" const Privilege = () => { const [privileges, setPrivileges] = useState([]) @@ -28,25 +27,23 @@ const Privilege = () => { const appendPrivilege = (privilege) => { setPrivileges((data) => [privilege, ...data]) } - const subLinks = [{ label: "Utilisateurs", link: "/user" }, { label: "Rôles", link: "/role" }, { label: "Habilitations", link: "/privilege" }, { label: "Projets", link: "/projects" }] return ( <> - -
+

Liste des habilitations

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

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

- :
+ :
diff --git a/src/app/(dashboard)/projects/page.jsx b/src/app/(dashboard)/projects/page.jsx index abdaafc760abb68f5abb39a8901e4e23e39a27f8..c6deb1db2bdde43797dce8afb9ac727e4933b4d7 100644 --- a/src/app/(dashboard)/projects/page.jsx +++ b/src/app/(dashboard)/projects/page.jsx @@ -145,7 +145,6 @@ const Projects = () => { return ( <> -
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 22ca155804efd376b20ae37d7647109bf5bbcd82..9f11566f4bc829ade86d3f72261281be24a1b4a5 100644 --- a/src/app/(dashboard)/reporting/page.jsx +++ b/src/app/(dashboard)/reporting/page.jsx @@ -1,18 +1,220 @@ -"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) @@ -22,7 +224,20 @@ const Reporting = () => { 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 () => { @@ -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,16 +352,17 @@ 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 (
@@ -178,15 +396,14 @@ const Reporting = () => {
-
+
- - + +
- - - {/* */} + +
@@ -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..0b82d13f3fe2d8704a69d36eda7945c42388da87 100644 --- a/src/app/(dashboard)/reservation/page.jsx +++ b/src/app/(dashboard)/reservation/page.jsx @@ -8,6 +8,7 @@ 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' export const ReservationContext = React.createContext() @@ -26,6 +27,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 +42,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 +72,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 +159,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 +166,7 @@ const Reservation = () => { setDate({ day: currentDateData.day, week: currentDateData.weekMonthly, date: currentDateData.date }) } }, [dateRef.current, currentDateData?.date]) + if (isLoadingSelectsData) return
@@ -265,67 +179,84 @@ const Reservation = () => { const month = date.getMonth() + 1 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) return ( -
-
- {(filteredDatesData && filteredDatesData.length) && } -
-
-
-
-

Autres Projets

-
-
-
-

Disponible

-
-
-
-

Réservé par vous

-
-
-
-

Réservé par un collègue

+ <> +
+
+

Réservation place

+ {(filteredDatesData && filteredDatesData.length !== 0) && }
-
-
-

Présent

-
-
- {(!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.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}

- })} - {!isLoadingData && floors.filter((element) => concernedFloors.includes(element.id)).length === 0 - &&
-

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

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

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 a834a619b496ff0e7f107bf50c6205e35e7a6826..d461524eae4cbc0cf3dbae0452e16f48e8b11c3b 100644 --- a/src/app/(dashboard)/role/RoleTableRows.jsx +++ b/src/app/(dashboard)/role/RoleTableRows.jsx @@ -55,7 +55,7 @@ const RoleTableRows = ({ name, setRoles, id, privileges, setRoleToUpdate }) => {
- - + + + - + + 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..8fdb739698fd0322e3ee150ca1deba41d45cf386 --- /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 ? -
-
Habilitation
{privileges?.map((element, index) => { - return
{element.name}
+ return
{element.name}
})} {!privileges || privileges.length == 0 &&

Habilitations non accordées à ce rôle

}
diff --git a/src/app/(dashboard)/role/RolesFilter.jsx b/src/app/(dashboard)/role/RolesFilter.jsx index 25fc2cdb4fc812581f4ba9188a88e559155d4658..651c2e702739ba3cd76a0b0eb6d9a54fdfcbcde2 100644 --- a/src/app/(dashboard)/role/RolesFilter.jsx +++ b/src/app/(dashboard)/role/RolesFilter.jsx @@ -26,6 +26,7 @@ const RolesFilter = memo(function Page({ setFilter, filter }) { } getPrivileges() }, []) + return (

Recherche

@@ -77,7 +78,7 @@ const RolesFilter = memo(function Page({ setFilter, filter }) {
- diff --git a/src/app/(dashboard)/role/UpdateRoleForm.jsx b/src/app/(dashboard)/role/UpdateRoleForm.jsx index 4b35eadf0d90ee4e0aa60d40c983f0542eebfde3..c52bd74e9a13cd2643d5ad51c834fc7352570b2b 100644 --- a/src/app/(dashboard)/role/UpdateRoleForm.jsx +++ b/src/app/(dashboard)/role/UpdateRoleForm.jsx @@ -1,17 +1,19 @@ import { useNotification } from '@/context/NotificationContext' -import React, { useState, useRef, useEffect, useMemo } from 'react' +import React, { useState, useRef, useEffect } from 'react' import fetchRequest from '@/app/lib/fetchRequest' import Loader from '@/components/Loader/Loader' -import CancelIcon from "@/static/image/svg/cancel.svg" import { isArray } from '../../lib/TypesHelper' const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePrivileges, name, id }) => { const { toggleNotification } = useNotification() const [loadingStatus, setLoadingStatus] = useState(false) const [roleName, setRoleName] = useState(name) const [privileges, setPrivileges] = useState(null) - const [selectedPrivileges, setSelectedPrivileges] = useState(isArray(rolePrivileges) ? rolePrivileges : []) + const [checkedValues, setCheckedValues] = useState(isArray(rolePrivileges) ? rolePrivileges.map(element => element.id) : []); + const [selectAll, setSelectAll] = useState(false); const inputRef = useRef(null) - console.log("les priv de role ", rolePrivileges) + useEffect(() => { + setSelectAll(isArray(checkedValues) && privileges ? checkedValues.length === privileges.length : false) + }, [privileges]) useEffect(() => { const getPrivileges = async () => { const { data, errors, isSuccess } = await fetchRequest("/privileges") @@ -34,9 +36,8 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv setLoadingStatus(true) const { isSuccess, errors, data, status } = await fetchRequest(`/roles/${id}/`, { method: "PATCH", - body: JSON.stringify({ name: roleName, privileges: selectedPrivileges.filter((element) => privileges.find((prvElement) => prvElement.id === element.id)).map((element) => element.id) }) + body: JSON.stringify({ name: roleName, privileges: checkedValues }) }) - console.log(data) setLoadingStatus(false) if (isSuccess) { setRoles((roles) => roles.map((element) => element.id === id ? data : element)) @@ -92,46 +93,60 @@ const UpdateRoleForm = ({ setRoleToUpdate, setRoles, roles, privileges: rolePriv const handleRoleNameChange = (event) => { setRoleName(event.target.value) } - const handlePrivilegeClick = (privilege) => { - if (selectedPrivileges.find((element) => element.id === privilege.id)) { - setSelectedPrivileges(selectedPrivileges.filter((element) => element.id !== privilege.id)) + 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 (
-
- setRoleToUpdate(null)} className="h-8 w-8 cursor-pointer absolute top-2 right-2 fill-neutral-600" /> - {(privileges) ?
-

Modification de Rôle

-
+
+ {(privileges) ? +

Modifier 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/page.jsx b/src/app/(dashboard)/role/page.jsx index ef605d2fcec0715882d3950f05286bb9e117af89..214767ac4487f47c002c7bee4702e4b24968bc02 100644 --- a/src/app/(dashboard)/role/page.jsx +++ b/src/app/(dashboard)/role/page.jsx @@ -8,7 +8,6 @@ import { isArray } from '../../lib/TypesHelper' import AddIcon from "@/static/image/svg/add.svg" import UpdateRoleForm from './UpdateRoleForm' import { useNotification } from '@/context/NotificationContext' -import SubSideBar from '@/app/ui/SubSideBar' import RolesFilter from "./RolesFilter" const Role = () => { const [roles, setRoles] = useState([]) @@ -40,27 +39,25 @@ const Role = () => { const appendRole = (newRole) => { setRoles([newRole, ...roles]) } - const subLinks = [{ label: "Utilisateurs", link: "/user" }, { label: "Rôles", link: "/role" }, { label: "Habilitations", link: "/privilege" }, { label: "Projets", link: "/projects" }] return ( <> - -
+
{openCreatePopup && } {roleToUpdate && }

Liste des Rôles

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

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

- :
+ :
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 ( - - - - - 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 ( + <> + + + + + + + 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..53648ce6a83065f9c625e736258d77af60952eea --- /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 ? -
-
Rôle
- 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 - ?
- - -
- :
- - -
- } -
+ 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 + ?
+ + +
+ :
+ + +
+ } +
- - - - - - {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/UsersFilter.jsx b/src/app/(dashboard)/user/UsersFilter.jsx index ac52b93c9d7a9528a8a5e086f6f606b7389c7949..c284fe734a390def8d4c8830e7d9e9c4f310bc35 100644 --- a/src/app/(dashboard)/user/UsersFilter.jsx +++ b/src/app/(dashboard)/user/UsersFilter.jsx @@ -31,7 +31,7 @@ const UsersFilter = memo(function page({ setFilter, filter }) {
- diff --git a/src/app/(dashboard)/user/page.jsx b/src/app/(dashboard)/user/page.jsx index 726ea0d09011af2575115c7f413516a8e64ed851..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) - ?
+ ?

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 - ?
- - -
- :
- - -
- } -
- - - - - - {(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/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..a056401f635a4d48bcc3a3baec2a57c99ec4be23 100644 --- a/src/app/lib/DateHelper.js +++ b/src/app/lib/DateHelper.js @@ -1,32 +1,120 @@ -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; -} \ No newline at end of file + return weeks; +}; \ No newline at end of file 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/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 9534806d9808a1d0db05a1856afa3d0b3ec8b084..1b28f67da29693a02f7e580cc82a333af433884f 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 { 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,58 @@ 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", privilege: "user" }, - { label: "Habilitations", link: "/privilege", privilege: "privilege" }, - { label: "Rôles", link: "/role", privilege: "role" }, - { label: "Projets", link: "/projects", privilege: "projects" }, - { label: "Réservation", link: "/reservation", privilege: "reservation" }, - { label: "Type de Presence", link: "/planning/type-presence", privilege: "planning/type-presence" }, - { label: "Planning", link: "/planning", privilege: "planning" }, - { 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: "Gestion des zones", link: "/assign-zone-project", privilege: "assign-zone-project" }, - { label: "Consulter les réservations", link: "/consultation-reservations", privilege: "consultation-reservations" }, - { label: "Reporting", link: "/reporting", 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", - - } - ] - + 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..22e3549fcc0cc8b504f4223614a14c6bbf8e21d9 100644 --- a/src/app/ui/SideBarLink.jsx +++ b/src/app/ui/SideBarLink.jsx @@ -3,7 +3,7 @@ import Link from 'next/link'; import { usePathname } from 'next/navigation'; import React from 'react'; -const SideBarLink = ({ link, label }) => { +const SideBarLink = ({ links, label }) => { const pathname = usePathname(); const hideSideBar = () => { @@ -16,16 +16,15 @@ const SideBarLink = ({ link, label }) => { console.log(target, sideBar); }; - const isActive = pathname === link; - + const isActive = links?.find((element) => element.link === 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/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/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/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/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/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/tailwind.config.js b/tailwind.config.js index 3bbb6b72085fcbdfd21e91adecef10316559650b..0726de941644e8dbd0fcc6728f41e993a5d35389 100644 --- a/tailwind.config.js +++ b/tailwind.config.js @@ -65,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)'); + }, + ], };