diff --git a/src/components/IncompleteEvent/IncompleteEvent.jsx b/src/components/IncompleteEvent/IncompleteEvent.jsx index 3f9beb3..ed4ac34 100644 --- a/src/components/IncompleteEvent/IncompleteEvent.jsx +++ b/src/components/IncompleteEvent/IncompleteEvent.jsx @@ -1,33 +1,54 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import styles from "./IncompleteEvent.module.css"; +import getAllEvents from "../../utils/events/getAllEvents"; function IncompleteEvent() { - const incompleteEvents = [ - { title: "Événement A", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux", - "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] }, - { title: "Événement B", tasks: ["Réservation de la salle", "Vérification du matériel"] }, - { title: "Événement C", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] }, - { title: "Événement D", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm"] }, - { title: "Événement E", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux", - "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] }, - { title: "Événement F", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux", - "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] }, - { title: "Événement G", tasks: ["Communication sur les réseaux", "Enormément de comm", "Toujours + de comm,Communication sur les réseaux", - "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm","Communication sur les réseaux", "Enormément de comm", "Toujours + de comm]"] }, -]; + const [events, setEvents] = useState([]); + const [loading, setLoading] = useState(true); + + useEffect(() => { + const fetchEvents = async () => { + const data = await getAllEvents(); + setEvents(data?.data || data || []); + setLoading(false); + }; + fetchEvents(); + }, []); + + if (loading) { + return ( +
+

Derniers événements

+
+
+

Chargement des événements...

+
+
+
+ ); + } return (
-

Événements incomplets

+

Derniers événements

- {incompleteEvents.map((event, index) => ( -
-

{event.title}

+ {events.map((event) => ( +
+

{event.name}

+

+ {new Date(event.start).toLocaleDateString("fr-FR")} - {new Date(event.end).toLocaleDateString("fr-FR")} +

+

{event.description}

+

Tâches associées :

    - {event.tasks.map((task, taskIndex) => ( -
  • {task}
  • - ))} -
+ {event.tasks?.length ? ( + event.tasks.map((task, taskIndex) => ( +
  • {task.name}
  • + )) + ) : ( +
  • Aucune tâche associée.
  • + )} +
    ))}
    diff --git a/src/components/IncompleteEvent/IncompleteEvent.module.css b/src/components/IncompleteEvent/IncompleteEvent.module.css index 966a2e5..b000887 100644 --- a/src/components/IncompleteEvent/IncompleteEvent.module.css +++ b/src/components/IncompleteEvent/IncompleteEvent.module.css @@ -2,7 +2,7 @@ width: 100%; } -.rightBlock>h2 { +.rightBlock > h2 { font-size: 29px; } @@ -17,20 +17,6 @@ scroll-behavior: smooth; } -.eventCard ul { - align-items: center; - margin-left: 8%; -} - -.eventCard ul li { - text-align: left; - list-style-type: circle; -} - -.eventsContainer::-webkit-scrollbar { -display: none; -} - .eventCard { min-width: calc((100% - 20px) / 3); max-width: calc((100% - 20px) / 3); @@ -38,63 +24,69 @@ display: none; max-height: 250px; overflow-y: auto; text-align: center; - padding: 10px 10px 200px 10px; box-sizing: border-box; opacity: 0; transform: translateX(20px); animation: slideInEvent 1s ease forwards; - flex-shrink: 1; + flex-shrink: 0; } -.eventCard:nth-child(1) { - animation-delay: 0s; +.eventCard ul { + align-items: center; + margin-left: 8%; + padding-left: 15px; } -.eventCard:nth-child(2) { - animation-delay: 0.1s; +.eventCard ul li { + text-align: left; + list-style-type: circle; } -.eventCard:nth-child(3) { - animation-delay: 0.2s; -} - -.eventCard:nth-child(4) { - animation-delay: 0.3s; -} - -.eventCard:nth-child(5) { - animation-delay: 0.4s; -} - -.eventCard:nth-child(6) { - animation-delay: 0.5s; -} - -.eventCard::-webkit-scrollbar { - display: none; -} - -.eventCard>h3 { +.eventCard > h3 { font-size: 24px; } +/* Animation des cartes avec délais */ +.eventCard:nth-child(1) { animation-delay: 0s; } +.eventCard:nth-child(2) { animation-delay: 0.1s; } +.eventCard:nth-child(3) { animation-delay: 0.2s; } +.eventCard:nth-child(4) { animation-delay: 0.3s; } +.eventCard:nth-child(5) { animation-delay: 0.4s; } +.eventCard:nth-child(6) { animation-delay: 0.5s; } + +.emptyEvent { + min-width: calc((100% - 20px) / 3); + max-width: calc((100% - 20px) / 3); + min-height: 250px; + max-height: 250px; + padding: 10px; + text-align: center; + box-sizing: border-box; + display: flex; + justify-content: center; + align-items: center; + opacity: 1; + flex-shrink: 0; +} + @keyframes slideInEvent { from { opacity: 0; transform: translateX(20px); } - to { opacity: 1; transform: translateX(0); } } +/* Adaptation pour mobile */ @media screen and (max-width: 760px) { .eventCard { min-width: 100%; min-height: 200px; max-height: fit-content; + padding-bottom: 0; } .eventCard h3 { @@ -102,18 +94,21 @@ display: none; } .eventsContainer { - display: flex; flex-direction: column; - gap: 10px; max-height: 300px; margin-top: 15px; height: fit-content; overflow-y: auto; padding-bottom: 0; - scroll-behavior: smooth; } - .rightBlock>h2 { + .rightBlock > h2 { font-size: 18px; } -} \ No newline at end of file + + .emptyEvent { + min-width: 100%; + min-height: 200px; + max-height: fit-content; + } +} diff --git a/src/components/PendingMembers/PendingMembers.jsx b/src/components/PendingMembers/PendingMembers.jsx index a931496..77fabf7 100644 --- a/src/components/PendingMembers/PendingMembers.jsx +++ b/src/components/PendingMembers/PendingMembers.jsx @@ -1,39 +1,102 @@ -import React from "react"; +import React, { useState, useEffect } from "react"; import Button from "../ui/button/button"; import styles from "./PendingMembers.module.css"; -function pendingMembers() { - const pendingMembers = [ - { name: "Antoine Ordonneau" }, - { name: "Giovanni Josserand" }, - { name: "Quentin T'Jampens" }, - { name: "Quentin T'Jampens" }, - { name: "Quentin T'Jampens" }, - { name: "Quentin T'Jampens" }, - { name: "Quentin T'Jampens" } - ]; +import getUserToValidate from "../../utils/users/getUserToValidate"; +import getUserById from "../../utils/users/getUserById"; +import validateUser from "../../utils/users/validateUser"; +import deleteUser from "../../utils/users/deleteUser"; - const handleValidate = (name) => { - console.log(`Valider ${name}`); +function PendingMembers() { + const [pendingMembers, setPendingMembers] = useState([]); + const [loading, setLoading] = useState(true); + const [actionLoading, setActionLoading] = useState(null); + const [error, setError] = useState(null); + + useEffect(() => { + const fetchPendingMembers = async () => { + setLoading(true); + const userIds = await getUserToValidate(); + if (userIds && userIds.length > 0) { + const users = await Promise.all(userIds.map((id) => getUserById(id))); + setPendingMembers(users.filter(Boolean)); + } else { + setPendingMembers([]); + } + setLoading(false); + }; + fetchPendingMembers(); + }, []); + + const removeMember = (id) => { + setPendingMembers(prevMembers => + prevMembers.filter(member => member.id !== id) + ); }; - const handleReject = (name) => { - console.log(`Refuser ${name}`); + const handleValidate = async (id) => { + setActionLoading(id); + setError(null); + try { + const result = await validateUser(id); + if (result.success) { + removeMember(id); + } else { + setError(result.message || "Échec de la validation."); + } + } catch (err) { + setError("Erreur lors de la validation."); + } finally { + setActionLoading(null); + } }; + const handleReject = async (id) => { + setActionLoading(id); + setError(null); + try { + const result = await deleteUser(id); + if (result.success) { + removeMember(id); + } else { + setError(result.message || "Échec de la suppression."); + } + } catch (err) { + setError("Erreur lors de la suppression."); + } finally { + setActionLoading(null); + } + }; + + if (loading) { + return
    Chargement des membres en attente...
    ; + } + return (

    Membres en attente de validation

    + {error &&

    {error}

    } +
    - {pendingMembers.map((member, index) => ( -
    -

    {member.name}

    + {pendingMembers.length === 0 &&

    Aucun membre en attente

    } + + {pendingMembers.map((member) => ( +
    +

    {member.name} {member.lastname}

    - -
    @@ -43,4 +106,4 @@ function pendingMembers() { ); } -export default pendingMembers; +export default PendingMembers; diff --git a/src/components/PendingMembers/PendingMembers.module.css b/src/components/PendingMembers/PendingMembers.module.css index 57919c3..c87d933 100644 --- a/src/components/PendingMembers/PendingMembers.module.css +++ b/src/components/PendingMembers/PendingMembers.module.css @@ -18,10 +18,24 @@ } -.membersContainer::-webkit-scrollbar { - display: none; +/*.membersContainer::-webkit-scrollbar { + width: 8px; } +.membersContainer::-webkit-scrollbar-track { + background: rgba(0, 0, 0, 0.1); + border-radius: 10px; +} + +.membersContainer::-webkit-scrollbar-thumb { + background: rgba(255, 0, 0, 0.5); + border-radius: 10px; +} + +.membersContainer::-webkit-scrollbar-thumb:hover { + background: rgba(125, 249, 30, 0.7); +}*/ + .memberCard { display: flex; justify-content: space-between; diff --git a/src/components/chart/ParticipationChart.jsx b/src/components/chart/ParticipationChart.jsx index 0a401ef..18aac36 100644 --- a/src/components/chart/ParticipationChart.jsx +++ b/src/components/chart/ParticipationChart.jsx @@ -1,14 +1,32 @@ -import React, { useState } from "react"; +import React, { useState, useEffect } from "react"; import { Doughnut } from "react-chartjs-2"; import { Chart, ArcElement } from "chart.js"; import { MenuItem, Select, FormControl } from "@mui/material"; import styles from "./ParticipationChart.module.css"; +import getAllEvents from "../../utils/events/getAllEvents"; Chart.register(ArcElement); function ParticipationChart() { const [selected, setSelected] = useState("3"); - const [participationRate] = useState(78); + + const [eventsCount, setEventsCount] = useState(0); + const [participationRate, setParticipationRate] = useState(0); + + useEffect(() => { + const fetchEvents = async () => { + const res = await getAllEvents(); + + const eventsArray = res?.data ?? []; + + setEventsCount(eventsArray.length); + + const rate = Math.min(eventsArray.length, 100); + setParticipationRate(rate); + }; + + fetchEvents(); + }, []); const chartData = { labels: ["Progress", "Background"], @@ -17,9 +35,7 @@ function ParticipationChart() { data: [participationRate, 100 - participationRate], backgroundColor: ["#1e60f9ff", "#ffffffff"], borderWidth: 0, - weight: 10, cutout: "93%", - circumference: 360, rotation: -90, }, ], @@ -31,56 +47,35 @@ function ParticipationChart() { legend: { display: false }, tooltip: { enabled: false }, }, - animation: { - animateScale: true, - animateRotate: true, - }, }; return (
    -
    - -
    - - - -
    +
    + + + + +
    +
    -
    {participationRate}%
    +
    + {eventsCount} events +
    diff --git a/src/components/chart/ParticipationChart.module.css b/src/components/chart/ParticipationChart.module.css index 00c9652..b35dcab 100644 --- a/src/components/chart/ParticipationChart.module.css +++ b/src/components/chart/ParticipationChart.module.css @@ -3,14 +3,18 @@ display: flex; flex-direction: column; align-items: center; - height: calc(100vh - 260px); - margin-bottom: auto; + height: 100%; + min-height: calc(100vh - 260px); + box-sizing: border-box; + padding: 20px; + margin: 0; } .participationHeader { display: flex; align-items: center; width: 100%; + margin-bottom: 10px; } .partiText { @@ -26,14 +30,15 @@ .partiText label { font-size: 20px; - text-align: right; + text-align: center; transition: all 0.3s ease; + width: 100%; } - .chartContainer { - max-width: 80%; - width: 400px; + width: 80%; + max-width: 400px; height: auto; + aspect-ratio: 1/1; margin: 0 auto; position: relative; animation: slideEvent2 2s; @@ -58,12 +63,70 @@ color: #333; } +.formControl { + display: flex; + justify-content: center; + width: 100%; + margin-top: 10px; +} + +.formDesign { + margin: 0 auto; + width: fit-content; +} + +.selectRoot { + width: 100px; + height: 35px; + font-size: 19px; + border:none; +} + +.selectRoot .MuiOutlinedInput-notchedOutline { + border: none; +} + +.selectRoot .MuiSelect-select { + padding: 8px; + padding-right: 24px; +} + +.selectRoot .MuiSvgIcon-root { + font-size: 20px; +} + +.selectRoot .MuiMenu-paper { + border-radius: 7px; + width: 100px; + max-width: 100px; +} + +.selectRoot .MuiMenuItem-root { + font-size: 18px; +} + +.selectMenuPaper { + border-radius: 7px; + width: 100px; + max-width: 120px; + max-height: 300px; + overflow-y: auto; + border:none; +} + +.selectMenuItem { + font-size: 18px; + min-height: 40px; + display: flex; + align-items: center; +} + + @keyframes slideEvent2 { from { opacity: 0; transform: translateX(-20px); } - to { opacity: 1; transform: translateX(0); @@ -75,45 +138,52 @@ opacity: 0; transform: translateX(20px); } - to { opacity: 1; transform: translateX(0); } } -.formControl { - display: flex; - justify-content: center; - width: 100%; -} - -.formDesign { - margin: 0 auto; - width: fit-content; -} - @media screen and (max-width: 760px) { - .partiText { - flex-direction: column; - justify-content: center; + .leftBlock { + width: 100%; + height: auto; + min-height: auto; + margin-bottom: 20px; + } + + .chartContainer { + width: 90%; + max-width: 300px; + height: auto; + aspect-ratio: 1/1; } .partiText label { - text-align: center; + font-size: 18px; } - .leftBlock { - width: 100%; - display: flex; - flex-direction: column; - align-items: center; - height: fit-content; - margin-bottom: auto; - margin-top: 30px; - } - .chartContainer{ - height:fit-content; + .chartPercentage { + font-size: 48px; } +} +/* scrollbar */ +::-webkit-scrollbar { + width: 8px; + height: 8px; +} + +::-webkit-scrollbar-track { + background: transparent; + border-radius: 10px; +} + +::-webkit-scrollbar-thumb { + background: rgba(100, 100, 100, 0.3); + border-radius: 10px; +} + +::-webkit-scrollbar-thumb:hover { + background: rgba(100, 100, 100, 0.5); +} -} \ No newline at end of file diff --git a/src/utils/date/formatDateLetter.js b/src/utils/date/formatDateLetter.js index 3480835..883b3c8 100644 --- a/src/utils/date/formatDateLetter.js +++ b/src/utils/date/formatDateLetter.js @@ -1,14 +1,23 @@ + + export function formatDateLetter(d) { + if (!d) return "Date inconnue"; + + const dateObj = new Date(d); + if (isNaN(dateObj.getTime())) return "Date invalide"; + const monthNames = [ "Janvier", "Février", "Mars", "Avril", "Mai", "Juin", "Juillet", "Août", "Septembre", "Octobre", "Novembre", "Décembre" ]; - const [datePart, timePart] = d.split(' '); - const [year, month, day] = datePart.split('-'); - const [hours, minutes] = timePart.split(':'); + const day = dateObj.getDate().toString().padStart(2, "0"); + const month = monthNames[dateObj.getMonth()]; + const year = dateObj.getFullYear(); + const hours = dateObj.getHours().toString().padStart(2, "0"); + const minutes = dateObj.getMinutes().toString().padStart(2, "0"); - return `${day} ${monthNames[parseInt(month, 10) - 1]} ${year} à ${hours}:${minutes}`; + return `${day} ${month} ${year} à ${hours}:${minutes}`; } @@ -19,4 +28,5 @@ export function formatDateLetterJS(d) { ]; const [year, month, day] = d.split('-'); return `${day} ${monthNames[parseInt(month) - 1]} ${year}`; -} \ No newline at end of file +} + diff --git a/src/utils/events/eventCounter.js b/src/utils/events/eventCounter.js new file mode 100644 index 0000000..20ee578 --- /dev/null +++ b/src/utils/events/eventCounter.js @@ -0,0 +1,28 @@ +import { useEffect, useState } from "react"; +import getAllEvents from "../../utils/events/getAllEvents"; + +function EventsCounter() { + const [eventsCount, setEventsCount] = useState(0); + const [loading, setLoading] = useState(true); + useEffect(() => { + const fetchEvents = async () => { + try { + const events = await getAllEvents(); + if (Array.isArray(events)) { + setEventsCount(events.length); + } else { + setEventsCount(0); + } + } catch (err) { + console.error(err); + setEventsCount(0); + } finally { + setLoading(false); + } + }; + fetchEvents(); + }, []); + if (loading) return

    Chargement...

    ; + return

    Nombre d’événements : {eventsCount}

    ; +} +export default EventsCounter; diff --git a/src/utils/users/deleteUser.js b/src/utils/users/deleteUser.js new file mode 100644 index 0000000..5c4a8d3 --- /dev/null +++ b/src/utils/users/deleteUser.js @@ -0,0 +1,35 @@ +import getXSRFToken from "../getXSRF"; + +export default async function deleteUser(userId) { + + const csrfToken = await getXSRFToken(); + + try { + + + const res = await fetch( + `http://${import.meta.env.VITE_API_URL}/api/users/${userId}`, + { + method: "DELETE", + credentials: "include", + headers: { + Accept: "application/json", + "X-XSRF-TOKEN": csrfToken, + }, + } + ); + + if (!res.ok) { + let message = "Erreur backend"; + try { + const errorData = await res.json(); + message = errorData.message || message; + } catch {} + return { success: false, message }; + } + return { success: true }; + } catch (err) { + console.error(err); + return { success: false, message: "Erreur réseau" }; + } +} diff --git a/src/utils/users/getUserToValidate.js b/src/utils/users/getUserToValidate.js new file mode 100644 index 0000000..f619c0b --- /dev/null +++ b/src/utils/users/getUserToValidate.js @@ -0,0 +1,21 @@ +export default async function getUserToValidate() { + try { + const res = await fetch(`http://${import.meta.env.VITE_API_URL}/api/users/invalid`, { + method: 'GET', + credentials: 'include', + headers: { + 'Accept': 'application/json' + } + }); + + if (!res.ok) { + throw new Error(`Erreur serveur : ${res.status}`); + } + + const data = await res.json(); + return data; + } catch (err) { + console.error('Erreur :', err); + return null; + } +} \ No newline at end of file diff --git a/src/utils/users/validateUser.js b/src/utils/users/validateUser.js new file mode 100644 index 0000000..21f4d1e --- /dev/null +++ b/src/utils/users/validateUser.js @@ -0,0 +1,27 @@ +import getXSRFToken from "../getXSRF.js"; + +export default async function validateUser(userId) { + const csrfToken = await getXSRFToken(); + try { + const res = await fetch( + `http://${import.meta.env.VITE_API_URL}/api/users/${userId}/validate`, + { + method: "POST", + credentials: "include", + headers: { + Accept: "application/json", + 'X-XSRF-TOKEN': csrfToken, + }, + } + ); + if (!res.ok) { + const errorData = await res.json(); + console.error("Erreur backend:", errorData); + return false; + } + return true; + } catch (err) { + console.error("Erreur réseau:", err); + return false; + } +}