diff --git a/.hintrc b/.hintrc
new file mode 100644
index 0000000..7c59e66
--- /dev/null
+++ b/.hintrc
@@ -0,0 +1,17 @@
+{
+ "extends": [
+ "development"
+ ],
+ "hints": {
+ "compat-api/css": [
+ "default",
+ {
+ "ignore": [
+ "scrollbar-color",
+ "scrollbar-width",
+ "scrollbar-width: thin"
+ ]
+ }
+ ]
+ }
+}
\ No newline at end of file
diff --git a/README.md b/README.md
index 5694b7a..712999b 100644
--- a/README.md
+++ b/README.md
@@ -54,6 +54,8 @@ Durante el desarrollo del proyecto, se utilizaron las siguientes librerías:
```sh
npm install react-router-dom
npm install axios
+npm install d3
+npm install jwt-decode
npm install framer-motion
npm install react-datepicker
npm install date-fns
diff --git a/package.json b/package.json
index 5c4ae16..9cd9e7b 100644
--- a/package.json
+++ b/package.json
@@ -8,6 +8,8 @@
"@testing-library/react": "^16.2.0",
"@testing-library/user-event": "^13.5.0",
"axios": "^1.8.4",
+ "d3": "^7.9.0",
+ "jwt-decode": "^4.0.0",
"framer-motion": "^12.6.2",
"react": "^19.0.0",
"date-fns": "^4.1.0",
@@ -17,6 +19,7 @@
"react-router-dom": "^7.4.0",
"react-scripts": "^5.0.1",
"styled-components": "^6.1.16",
+ "styled-system": "^5.1.5",
"web-vitals": "^2.1.4"
},
"scripts": {
diff --git a/src/App.css b/src/App.css
index 9624b22..a86cb74 100644
--- a/src/App.css
+++ b/src/App.css
@@ -94,21 +94,42 @@ ul {
}
.content .panel .header {
+ display: flex;
+ flex-direction: row;
+ justify-content: flex-start;
+ align-items: center;
+ height: 20%;
+ margin-right: 5%;
+}
+
+.content .panel .header .info {
display: flex;
flex-direction: column;
- justify-content: center;
+ gap: 5px;
width: 100%;
- height: 20%;
}
-.content .panel .header .title {
+.content .panel .header .info .title {
font-weight: bold;
}
-.content .panel .header .title, .content .panel .header .greetings {
+.content .panel .header .info .title, .content .panel .header .greetings {
font-size: 21px;
}
+.content .panel .header .user-info {
+ display: flex;
+ flex-direction: row;
+ gap: 20px;
+ width: 100%;
+ align-items: center;
+ justify-content: flex-end;
+}
+
+.content .panel .header .user-info span {
+ font-size: 18px;
+}
+
.content .panel .container {
flex: 1;
display: flex;
diff --git a/src/App.js b/src/App.js
index 6132b5b..7095015 100644
--- a/src/App.js
+++ b/src/App.js
@@ -1,3 +1,4 @@
+// App.js
import React, { useState, useEffect } from "react";
import {
BrowserRouter as Router,
@@ -5,118 +6,227 @@ import {
Routes,
Link,
useLocation,
- Navigate
+ Navigate,
+ useNavigate,
} from "react-router-dom";
-import { ReactComponent as House } from './assets/icons/house-user_11269953 1.svg';
-import { ReactComponent as Room } from './assets/icons/workshop_14672030 1.svg';
-import { ReactComponent as User } from './assets/icons/User.svg';
-import Home from './pages/Home/Home.js';
+import { ReactComponent as House } from "./assets/icons/house-user_11269953 1.svg";
+import { ReactComponent as Room } from "./assets/icons/workshop_14672030 1.svg";
+import { ReactComponent as UserIcon } from "./assets/icons/User.svg";
+import { consultarUsuarioPorCorreo } from "./api/usuario";
+import LoginPage from "./pages/Login/LoginPage";
+import Home from "./pages/Home/Home";
+import AdministratorHome from "./pages/Administrator/AdministratorHome";
import GestionarSalones from './pages/Salones/GestionarSalones';
import GestionarUsuarios from './pages/Admin/GestionarUsuarios';
-
-import './App.css';
+import styled from "styled-components";
+import { jwtDecode } from "jwt-decode";
+import "./App.css";
const routesConfig = {
admin: [
{ path: "/administrador", name: "Panel de Control", icon: },
{ path: "/administrador/salones", name: "Gestión de Salones", icon: },
- { path: "/administrador/usuarios", name: "Gestión de Usuarios", icon: },
+ { path: "/administrador/usuarios", name: "Gestión de Usuarios", icon: },
],
profe: [
{ path: "/home", name: "Gestión de Reservas", icon: },
- ],
+ ]
};
-const Menu = ({ role }) => (
-
- {routesConfig[role]?.map((item, index) => (
-
-
- {item.icon}
- {item.name}
-
-
- ))}
-
-);
-
-/**
- * Componente de encabezado que muestra el título de la página y el saludo al usuario.
- */
-const Header = ({ role }) => {
- const location = useLocation();
- const currentPage = routesConfig[role]?.find(
- (route) => route.path === location.pathname
+const Menu = ({ user }) => {
+ if (!user) return null;
+ const userRoutes = routesConfig[user.isAdmin ? "admin" : "profe"] || [];
+
+ return (
+
+ {userRoutes.map((item, index) => (
+
+
+ {item.icon}
+ {item.name}
+
+
+ ))}
+
);
- const title = currentPage ? currentPage.name : "Elysium";
+};
+
+//
+// HEADER: muestra título, saludo, avatar y botón de logout
+//
+const Header = ({ user, onLogout }) => {
+ const location = useLocation();
+ const navigate = useNavigate();
+ let title = "Elysium";
useEffect(() => {
- document.title = title;
- }, [title]);
+ if (user) {
+ const currentPage = routesConfig[user.isAdmin ? "admin" : "profe"]?.find(
+ (route) => route.path === location.pathname
+ );
+ document.title = currentPage ? currentPage.name : "Elysium";
+ }
+ }, [location.pathname, user]);
+
+ if (!user) return null;
return (
-
{title}
-
- Buen día, {role === "admin" ? "Admin" : "Profe"}
-
-
- Gestiona las reservas que has agendado últimamente
-
+
+ {title}
+
+ Buen día, {user.isAdmin ? "Admin" : "Profe"} {user.nombre}
+
+
+ Gestiona las reservas que has agendado últimamente
+
+
+
+
+ {user.nombre} {user.apellido}
+ {
+ onLogout();
+ navigate("/");
+ }}
+ />
+
);
};
-/**
- * Componente principal de la aplicación.
- */
-function App() {
- const [role, setRole] = useState("");
+const UserAvatar = styled.img`
+ width: 40px;
+ height: 40px;
+ object-fit: contain;
+ border-radius: 50%;
+`;
+
+const LogoutIcon = styled.img`
+ width: 32px;
+ height: 32px;
+ cursor: pointer;
+ fill: var(--variable-collection-current-color);
+`;
+
+const obtenerCorreoDesdeToken = (token) => {
+ try {
+ const decoded = jwtDecode(token);
+ return decoded.sub;
+ } catch (error) {
+ return null;
+ }
+};
+
+
+
+//
+// Componente principal de rutas
+//
+function AppRoutes({ user, setUser }) {
+ const [loading, setLoading] = useState(true);
+ const navigate = useNavigate();
useEffect(() => {
- const randomRole = Math.random() > 0.5 ? "admin" : "profe"; // Simulación de login
- setRole(randomRole);
- const colorVariable =
- randomRole === "admin"
- ? "var(--variable-collection-user-admin)"
- : "var(--variable-collection-user-estandar)";
- document.documentElement.style.setProperty(
- "--variable-collection-current-color",
- colorVariable
- );
+ const fetchUser = async () => {
+ const token = localStorage.getItem("token");
+ if (token) {
+ try {
+ const correoGuardado = obtenerCorreoDesdeToken(token);
+ if (correoGuardado) {
+ const usuario = await consultarUsuarioPorCorreo(correoGuardado);
+ setUser(usuario);
+
+ if (usuario.isAdmin) {
+ document.documentElement.style.setProperty("--variable-collection-current-color", "var(--variable-collection-user-admin)");
+ navigate("/administrador");
+ } else {
+ document.documentElement.style.setProperty("--variable-collection-current-color", "var(--variable-collection-user-estandar)");
+ navigate("/home");
+ }
+ } else {
+ localStorage.removeItem("token");
+ }
+ } catch (error) {
+ console.error("Error obteniendo usuario:", error);
+ localStorage.removeItem("token");
+ }
+ }
+ setLoading(false);
+ };
+
+ fetchUser();
}, []);
+ const handleLogout = () => {
+ localStorage.removeItem("token");
+ setUser(null);
+ };
+
+ if (loading) return Cargando...
;
+
+ return (
+
+ {/* Si no hay usuario autenticado, se muestra LoginPage */}
+ {!user ? (
+ <>
+ } />
+ } />
+ >
+ ) : (
+ // Una vez autenticado, se muestra la aplicación completa (Header, Menu, contenido)
+
+
+
+
+
+
+
+
+ {user.isAdmin ? (
+ <>
+ } />
+ } />
+ } />
+ } />
+ >
+ ) : (
+ <>
+ } />
+ } />
+ >
+ )}
+
+
+
+
+ }
+ />
+ )}
+
+ );
+}
+
+//
+// Componente principal de la aplicación con en el nivel más alto
+//
+function App() {
+ const [user, setUser] = useState(null);
+
return (
-
-
-
-
-
-
-
-
- {role === "admin" ? (
- <>
- } />
- } />
- } />
-
-
- } />
- >
- ) : (
- <>
- } />
- } />
- >
- )}
-
-
-
-
+
);
}
+
export default App;
diff --git a/src/api/usuario/administrador.jsx b/src/api/administrador.jsx
similarity index 100%
rename from src/api/usuario/administrador.jsx
rename to src/api/administrador.jsx
diff --git a/src/api/auth.jsx b/src/api/auth.jsx
new file mode 100644
index 0000000..f37831e
--- /dev/null
+++ b/src/api/auth.jsx
@@ -0,0 +1,27 @@
+import axios from "axios";
+import { BASE_URL } from "../config/config.js";
+
+export async function login(correo="", contraseña="") {
+ if (correo === "" || contraseña === "") {
+ throw new Error("El correo y la contraseña son obligatorios.");
+ }
+ try {
+ const data = {
+ correoInstitucional: correo,
+ password: contraseña
+ }
+ const response = await axios.post(`${BASE_URL}/login`, data);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response ? error.response.data.message : error.message);
+ }
+}
+
+export async function register(usuario = {}) {
+ try {
+ const response = await axios.post(`${BASE_URL}/register`, usuario );
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response ? error.response.data.message : error.message);
+ }
+}
\ No newline at end of file
diff --git a/src/api/reserva.jsx b/src/api/reserva.jsx
index cbecd91..58f6ed4 100644
--- a/src/api/reserva.jsx
+++ b/src/api/reserva.jsx
@@ -15,9 +15,15 @@ const RESERVA_API = `${BASE_URL}/reserva`;
* @returns {Promise} Lista de reservas encontradas.
* @throws {Error} Si ocurre un error en la solicitud.
*/
-export async function getReservas (filtros) {
+export async function getReservas (filtros = {}, token) {
try {
- const response = await axios.get(RESERVA_API, { params: filtros });
+ const response = await axios.get(RESERVA_API, {
+ params: filtros,
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
@@ -30,9 +36,14 @@ export async function getReservas (filtros) {
* @returns {Promise} Datos de la reserva.
* @throws {Error} Si la reserva no existe o hay un error en la solicitud.
*/
-export async function consultarReserva (idReserva) {
+export async function consultarReserva (idReserva, token) {
try {
- const response = await axios.get(`${RESERVA_API}/${idReserva}/reserva`);
+ const response = await axios.get(`${RESERVA_API}/${idReserva}/reserva`, {
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
@@ -45,9 +56,15 @@ export async function consultarReserva (idReserva) {
* @returns {Promise} Mensaje de confirmación.
* @throws {Error} Si ocurre un error en la solicitud.
*/
-export async function crearReserva (reserva) {
+export async function crearReserva (reserva, token) {
try {
- const response = await axios.post(RESERVA_API, reserva);
+ const response = await axios.post(RESERVA_API, {
+ params: reserva,
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
@@ -61,9 +78,15 @@ export async function crearReserva (reserva) {
* @returns {Promise}
* @throws {Error} Si la reserva no existe o hay un error en la solicitud.
*/
-export async function actualizarReserva (idReserva, reserva) {
+export async function actualizarReserva (idReserva, reserva, token) {
try {
- await axios.patch(`${RESERVA_API}/${idReserva}`, reserva);
+ await axios.patch(`${RESERVA_API}/${idReserva}`, {
+ params: reserva,
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
} catch (error) {
throw new Error(error.response.data.message);
}
@@ -75,9 +98,14 @@ export async function actualizarReserva (idReserva, reserva) {
* @returns {Promise} Mensaje de confirmación.
* @throws {Error} Si la reserva no existe o hay un error en la solicitud.
*/
-export async function deleteReserva (idReserva) {
+export async function deleteReserva (idReserva, token) {
try {
- const response = await axios.put(`${RESERVA_API}/${idReserva}/inactivo`);
+ const response = await axios.put(`${RESERVA_API}/${idReserva}/inactivo`, {
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
diff --git a/src/api/salon.jsx b/src/api/salon.jsx
index 74d2085..a021bea 100644
--- a/src/api/salon.jsx
+++ b/src/api/salon.jsx
@@ -15,9 +15,15 @@ const SALON_API = `${BASE_URL}/salones`;
* @returns {Promise} Lista de salones.
* @throws {Error} Error al obtener los salones.
*/
-export async function getSalones(filtros = {}) {
+export async function getSalones(filtros = {}, token) {
try {
- const response = await axios.get(SALON_API, { params: filtros });
+ const response = await axios.get(SALON_API, {
+ params: filtros,
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
@@ -30,9 +36,14 @@ export async function getSalones(filtros = {}) {
* @returns {Promise} Datos del salón.
* @throws {Error} Error al obtener el salón.
*/
-export async function getSalonByMnemonico(mnemonico) {
+export async function getSalonByMnemonico(mnemonico, token) {
try {
- const response = await axios.get(`${SALON_API}/${mnemonico}`);
+ const response = await axios.get(`${SALON_API}/${mnemonico}`, {
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
@@ -45,9 +56,14 @@ export async function getSalonByMnemonico(mnemonico) {
* @returns {Promise} Estado de disponibilidad del salón.
* @throws {Error} Error al obtener la disponibilidad.
*/
-export async function getDisponible(mnemonico) {
+export async function getDisponible(mnemonico, token) {
try {
- const response = await axios.get(`${SALON_API}/${mnemonico}/disponible`);
+ const response = await axios.get(`${SALON_API}/${mnemonico}/disponible`, {
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
+ });
return response.data;
} catch (error) {
throw new Error(error.response.data.message);
@@ -66,10 +82,14 @@ export async function getDisponible(mnemonico) {
* @returns {Promise} Respuesta del servidor.
* @throws {Error} Error al agregar el salón.
*/
-export async function agregarSalon(salon) {
+export async function agregarSalon(salon, token) {
try {
- const response = await axios.post(SALON_API, salon, {
- headers: { "Content-Type": "application/json" }
+ const response = await axios.post(SALON_API, {
+ params: salon,
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
});
return response.data;
} catch (error) {
@@ -84,10 +104,14 @@ export async function agregarSalon(salon) {
* @returns {Promise} Respuesta del servidor.
* @throws {Error} Error al actualizar el salón.
*/
-export async function actualizarSalon(mnemonico, salon) {
+export async function actualizarSalon(mnemonico, salon, token) {
try {
- const response = await axios.patch(`${SALON_API}/${mnemonico}`, salon, {
- headers: { "Content-Type": "application/json" }
+ const response = await axios.patch(`${SALON_API}/${mnemonico}`, {
+ params: salon,
+ headers: {
+ "Authorization": `Bearer ${token}`,
+ "Content-Type": "application/json"
+ },
});
return response.data;
} catch (error) {
diff --git a/src/api/usuario.jsx b/src/api/usuario.jsx
new file mode 100644
index 0000000..d525e1e
--- /dev/null
+++ b/src/api/usuario.jsx
@@ -0,0 +1,129 @@
+import axios from "axios";
+import { BASE_URL } from "../config/config.js";
+
+const USUARIO_API = `${BASE_URL}/usuario`;
+
+/**
+ * Consulta usuarios con filtros opcionales.
+ *
+ * @param {Object} filtros - Filtros opcionales para la consulta.
+ * @param {boolean} [filtros.activo] - Filtrar usuarios activos/inactivos.
+ * @param {boolean} [filtros.isAdmin] - Filtrar usuarios administradores/no administradores.
+ * @returns {Promise} - Lista de usuarios filtrados.
+ * @throws {Error} - Si la consulta falla.
+ */
+export async function consultarUsuarios(filtros) {
+ try {
+ const response = await axios.get(`${USUARIO_API}/usuarios`, { params: filtros });
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
+
+/**
+ * Consulta un usuario por su correo institucional.
+ * @param {string} correo - Correo institucional del usuario.
+ * @returns {Promise} - Datos del usuario.
+ * @throws {Error} - Si la consulta falla.
+ */
+export async function consultarUsuarioPorCorreo(correo) {
+ try {
+ // Verifica la ruta y el parámetro "correo"
+ const response = await axios.get(`${USUARIO_API}/correo/${correo}`);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response?.data?.message || "Error consultando usuario");
+ }
+ }
+
+/**
+ * Agrega un nuevo usuario al sistema.
+ * @param {Object} usuario - Datos del usuario a agregar.
+ * @returns {Promise} Promesa resuelta si el usuario se agrega correctamente.
+ * @throws {Error} Si hay un problema al agregar el usuario.
+ */
+export async function agregarUsuario(usuario) {
+ try {
+ const response = await axios.post(`${USUARIO_API}/usuario`, usuario);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
+
+/**
+ * Actualiza parcialmente la información de un usuario.
+ * @param {number} id - ID del usuario a actualizar.
+ * @param {Object} actualizacion - Datos a actualizar.
+ * @returns {Promise} Promesa resuelta si la actualización es exitosa.
+ * @throws {Error} Si hay un problema al actualizar el usuario.
+ */
+export async function actualizarInformacionUsuario(id, actualizacion) {
+ try {
+ const response = await axios.patch(`${USUARIO_API}/usuario/${id}`, actualizacion);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
+
+/**
+ * Agrega un nuevo salón al sistema.
+ * @param {number} id - ID del administrador que agrega el salón.
+ * @param {Object} salon - Datos del salón a agregar.
+ * @returns {Promise} Promesa resuelta si el salón se agrega correctamente.
+ * @throws {Error} Si hay un problema al agregar el salón.
+ */
+export async function agregarSalon(id, salon) {
+ try {
+ const response = await axios.post(`${USUARIO_API}/${id}/salon`, salon);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
+
+/**
+ * Crea una nueva reserva en el sistema.
+ * @param {number} id - ID del usuario que realiza la reserva.
+ * @param {Object} reserva - Datos de la reserva a crear.
+ * @returns {Promise} Mensaje de éxito.
+ * @throws {Error} Si hay un problema al crear la reserva.
+ */
+export async function crearReserva(id, reserva) {
+ try {
+ const response = await axios.post(`${USUARIO_API}/${id}/reserva`, reserva);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
+
+/**
+ * Consulta un usuario por su ID.
+ * @param {number} id - ID del usuario a consultar.
+ * @returns {Promise} - Datos del usuario.
+ */
+export async function consultarUsuario(id) {
+ try {
+ const response = await axios.get(`${USUARIO_API}/${id}`);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
+
+/**
+ * Lista todas las reservas de un usuario.
+ * @param {number} id - ID del usuario.
+ * @returns {Promise} - Lista de reservas.
+ */
+export async function listarReservas(id) {
+ try {
+ const response = await axios.get(`${USUARIO_API}/${id}/reserva`);
+ return response.data;
+ } catch (error) {
+ throw new Error(error.response.data.message);
+ }
+}
\ No newline at end of file
diff --git a/src/api/usuario/estandar.jsx b/src/api/usuario/estandar.jsx
deleted file mode 100644
index c9e413c..0000000
--- a/src/api/usuario/estandar.jsx
+++ /dev/null
@@ -1,50 +0,0 @@
-import axios from "axios";
-import { BASE_URL } from "../../config/config.js";
-
-const ESTANDAR_API = `${BASE_URL}/estandar`;
-
-/**
- * Consulta un usuario por su ID.
- * @param {number} id - ID del usuario a consultar.
- * @returns {Promise} - Datos del usuario.
- */
-export async function consultarUsuario(id) {
- try {
- const response = await axios.get(`${ESTANDAR_API}/${id}`);
- return response.data;
- } catch (error) {
- throw new Error(error.response.data.message);
- }
-}
-
-
-/**
- * Crea una reserva para un usuario.
- * @param {number} id - ID del usuario que realiza la reserva.
- * @param {Object} reservaData - Datos de la reserva.
- * @returns {Promise} - Mensaje de éxito.
- */
-export async function crearReserva(id, reservaData) {
- try {
- const response = await axios.post(`${ESTANDAR_API}/${id}/reserva`, reservaData, {
- headers: { "Content-Type": "application/json" }
- });
- return response.data;
- } catch (error) {
- throw new Error(error.response.data.message);
- }
-}
-
-/**
- * Lista todas las reservas de un usuario.
- * @param {number} id - ID del usuario.
- * @returns {Promise} - Lista de reservas.
- */
-export async function listarReservas(id) {
- try {
- const response = await axios.get(`${ESTANDAR_API}/${id}/reserva`);
- return response.data;
- } catch (error) {
- throw new Error(error.response.data.message);
- }
-}
\ No newline at end of file
diff --git a/src/components/Admin/charts/DemandaChart.jsx b/src/components/Admin/charts/DemandaChart.jsx
new file mode 100644
index 0000000..b44e08a
--- /dev/null
+++ b/src/components/Admin/charts/DemandaChart.jsx
@@ -0,0 +1,78 @@
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const DemandaChart = ({ reservas }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ svg.selectAll("*").remove(); // Limpia el SVG
+
+ if (!reservas || reservas.length === 0) return;
+
+ const width = 600;
+ const height = 500;
+
+ svg.attr("width", width).attr("height", height);
+
+ // Agrupar reservas por salón
+ const reservasPorSalon = d3.rollup(
+ reservas,
+ (v) => v.length,
+ (d) => d.idSalon
+ );
+ // Convertir a arreglo de objetos
+ const data = Array.from(reservasPorSalon, ([salon, count]) => ({
+ salon,
+ count,
+ }));
+
+ // Usamos d3.hierarchy para crear una estructura para el pack layout.
+ const root = d3.hierarchy({ children: data }).sum((d) => d.count);
+
+ // Configurar el pack layout
+ const pack = d3.pack().size([width, height]).padding(10);
+
+ const nodes = pack(root).leaves();
+
+ // Escala de colores
+ const color = d3.scaleOrdinal(d3.schemeSet3);
+
+ // Dibujar las burbujas
+ const node = svg
+ .selectAll("g")
+ .data(nodes)
+ .enter()
+ .append("g")
+ .attr("transform", (d) => `translate(${d.x}, ${d.y})`);
+
+ node
+ .append("circle")
+ .attr("r", 0)
+ .attr("fill", (d, i) => color(i))
+ .transition()
+ .duration(800)
+ .attr("r", (d) => d.r);
+
+ // Agregar etiquetas dentro de cada burbuja (nombre del salón y cantidad)
+ node
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dy", "-0.3em")
+ .attr("font-size", (d) => Math.min((2 * d.r) / d.data.salon.length, 18))
+ .attr("fill", "#000")
+ .text((d) => d.data.salon);
+
+ node
+ .append("text")
+ .attr("text-anchor", "middle")
+ .attr("dy", "1em")
+ .attr("font-size", "12px")
+ .attr("fill", "#000")
+ .text((d) => d.data.count);
+ }, [reservas]);
+
+ return ;
+};
+
+export default DemandaChart;
\ No newline at end of file
diff --git a/src/components/Admin/charts/DiaSalonChart.jsx b/src/components/Admin/charts/DiaSalonChart.jsx
new file mode 100644
index 0000000..3f51430
--- /dev/null
+++ b/src/components/Admin/charts/DiaSalonChart.jsx
@@ -0,0 +1,68 @@
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const DiaSalonChart = ({ reservas }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ svg.selectAll("*").remove(); // Limpia el contenido del SVG
+
+ if (!reservas || reservas.length === 0) return;
+
+ const width = 500;
+ const height = 500;
+ const radius = Math.min(width, height) / 2;
+ svg.attr("width", width).attr("height", height);
+
+ const g = svg
+ .append("g")
+ .attr("transform", `translate(${width / 2}, ${height / 2})`);
+
+ // Agrupar reservas por salón
+ const reservasPorSalon = d3.rollup(
+ reservas,
+ (v) => v.length,
+ (d) => d.idSalon
+ );
+ const data = Array.from(reservasPorSalon, ([salon, count]) => ({ salon, count }));
+
+ // Configurar el pie chart
+ const pie = d3.pie().value((d) => d.count);
+ const arc = d3.arc().innerRadius(0).outerRadius(radius);
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
+
+ const arcs = g
+ .selectAll("arc")
+ .data(pie(data))
+ .enter()
+ .append("g")
+ .attr("class", "arc");
+
+ arcs
+ .append("path")
+ .attr("d", arc)
+ .attr("fill", (d, i) => color(i))
+ .transition()
+ .duration(800)
+ .attrTween("d", function (d) {
+ const i = d3.interpolate({ startAngle: 0, endAngle: 0 }, d);
+ return function (t) {
+ return arc(i(t));
+ };
+ });
+
+ // Agregar etiquetas para cada salón
+ arcs
+ .append("text")
+ .attr("transform", (d) => `translate(${arc.centroid(d)})`)
+ .attr("text-anchor", "middle")
+ .attr("font-size", "12px")
+ .attr("fill", "#fff")
+ .text((d) => d.data.salon);
+ }, [reservas]);
+
+ return ;
+};
+
+export default DiaSalonChart;
\ No newline at end of file
diff --git a/src/components/Admin/charts/EstadoChart.jsx b/src/components/Admin/charts/EstadoChart.jsx
new file mode 100644
index 0000000..2017bdc
--- /dev/null
+++ b/src/components/Admin/charts/EstadoChart.jsx
@@ -0,0 +1,80 @@
+// src/components/Admin/charts/EstadoChart.jsx
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const EstadoChart = ({ reservas }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ svg.selectAll("*").remove(); // Limpia el SVG
+
+ if (!reservas || reservas.length === 0) return;
+
+ const width = 500;
+ const height = 500;
+ const radius = Math.min(width, height) / 2;
+ svg.attr("width", width).attr("height", height);
+
+ const g = svg
+ .append("g")
+ .attr("transform", `translate(${width / 2}, ${height / 2})`);
+
+ // Agrupar reservas por estado (suponiendo que la propiedad 'estado' es una cadena, ej: "ACTIVA" o "INACTIVA")
+ const reservasPorEstado = d3.rollup(
+ reservas,
+ (v) => v.length,
+ (d) => d.estado
+ );
+ const data = Array.from(reservasPorEstado, ([estado, count]) => ({
+ estado,
+ count,
+ }));
+
+ // Configurar el pie/donut chart
+ const pie = d3.pie().value((d) => d.count);
+ const arc = d3
+ .arc()
+ .innerRadius(radius * 0.5) // Radio interior para efecto donut
+ .outerRadius(radius);
+
+ // Escala de colores con una paleta profesional (usamos d3.schemeSet2)
+ const color = d3
+ .scaleOrdinal()
+ .domain(data.map((d) => d.estado))
+ .range(d3.schemeSet2);
+
+ const arcs = g
+ .selectAll("arc")
+ .data(pie(data))
+ .enter()
+ .append("g")
+ .attr("class", "arc");
+
+ arcs
+ .append("path")
+ .attr("d", arc)
+ .attr("fill", (d) => color(d.data.estado))
+ .transition()
+ .duration(800)
+ .attrTween("d", function (d) {
+ const i = d3.interpolate({ startAngle: 0, endAngle: 0 }, d);
+ return function (t) {
+ return arc(i(t));
+ };
+ });
+
+ // Agregar etiquetas dentro del gráfico
+ arcs
+ .append("text")
+ .attr("transform", (d) => `translate(${arc.centroid(d)})`)
+ .attr("text-anchor", "middle")
+ .attr("font-size", "14px")
+ .attr("fill", "#000")
+ .text((d) => `${d.data.estado}: ${d.data.count}`);
+ }, [reservas]);
+
+ return ;
+};
+
+export default EstadoChart;
\ No newline at end of file
diff --git a/src/components/Admin/charts/MesSalonChart.jsx b/src/components/Admin/charts/MesSalonChart.jsx
new file mode 100644
index 0000000..5eeb06f
--- /dev/null
+++ b/src/components/Admin/charts/MesSalonChart.jsx
@@ -0,0 +1,73 @@
+// src/components/Admin/charts/MesSalonChart.jsx
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const MesSalonChart = ({ reservas }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ svg.selectAll("*").remove(); // Limpia el SVG
+
+ if (!reservas || reservas.length === 0) return;
+
+ const width = 600;
+ const height = 300;
+ const margin = { top: 20, right: 20, bottom: 60, left: 50 };
+
+ // Agrupar reservas por salón
+ const reservasPorSalon = d3.rollup(
+ reservas,
+ (v) => v.length,
+ (d) => d.idSalon
+ );
+ const data = Array.from(reservasPorSalon, ([salon, count]) => ({ salon, count }));
+
+ // Escala X: nombres de salón
+ const x = d3.scaleBand()
+ .domain(data.map(d => d.salon))
+ .range([margin.left, width - margin.right])
+ .padding(0.1);
+
+ // Escala Y: cantidad de reservas
+ const y = d3.scaleLinear()
+ .domain([0, d3.max(data, d => d.count)])
+ .nice()
+ .range([height - margin.bottom, margin.top]);
+
+ svg.attr("width", width).attr("height", height);
+
+ // Eje X
+ svg.append("g")
+ .attr("transform", `translate(0, ${height - margin.bottom})`)
+ .call(d3.axisBottom(x))
+ .selectAll("text")
+ .attr("transform", "rotate(-45)")
+ .style("text-anchor", "end");
+
+ // Eje Y
+ svg.append("g")
+ .attr("transform", `translate(${margin.left},0)`)
+ .call(d3.axisLeft(y));
+
+ // Dibujar barras con transición
+ svg.selectAll(".bar")
+ .data(data)
+ .enter()
+ .append("rect")
+ .attr("class", "bar")
+ .attr("x", d => x(d.salon))
+ .attr("y", y(0))
+ .attr("width", x.bandwidth())
+ .attr("height", 0)
+ .attr("fill", (d, i) => d3.schemeCategory10[i % 10])
+ .transition()
+ .duration(800)
+ .attr("y", d => y(d.count))
+ .attr("height", d => y(0) - y(d.count));
+ }, [reservas]);
+
+ return ;
+};
+
+export default MesSalonChart;
\ No newline at end of file
diff --git a/src/components/Admin/charts/PromedioPrioridadChart.jsx b/src/components/Admin/charts/PromedioPrioridadChart.jsx
new file mode 100644
index 0000000..2126e5a
--- /dev/null
+++ b/src/components/Admin/charts/PromedioPrioridadChart.jsx
@@ -0,0 +1,87 @@
+// src/components/Admin/charts/PromedioPrioridadChart.jsx
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const PromedioPrioridadChart = ({ data }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ // Limpia el contenido anterior
+ svg.selectAll("*").remove();
+
+ if (!data || data.length === 0) return;
+
+ const width = 600;
+ const height = 400;
+ const margin = { top: 20, right: 20, bottom: 50, left: 60 };
+
+ svg.attr("width", width).attr("height", height);
+
+ // Dominio de prioridades: se asume que siempre serán 1 a 5
+ const priorities = data.map((d) => d.priority);
+
+ // Escala X: categorías (prioridad)
+ const x = d3
+ .scaleBand()
+ .domain(priorities)
+ .range([margin.left, width - margin.right])
+ .padding(0.2);
+
+ // Escala Y: promedio de reservas
+ const y = d3
+ .scaleLinear()
+ .domain([0, d3.max(data, (d) => d.promedio)]).nice()
+ .range([height - margin.bottom, margin.top]);
+
+ // Dibujar eje X
+ svg
+ .append("g")
+ .attr("transform", `translate(0, ${height - margin.bottom})`)
+ .call(d3.axisBottom(x).tickFormat((d) => `Prioridad ${d}`));
+
+ // Dibujar eje Y
+ svg
+ .append("g")
+ .attr("transform", `translate(${margin.left}, 0)`)
+ .call(d3.axisLeft(y));
+
+ // Escala de colores para cada barra
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
+
+ // Dibujar las barras
+ svg
+ .selectAll(".bar")
+ .data(data)
+ .enter()
+ .append("rect")
+ .attr("class", "bar")
+ .attr("x", (d) => x(d.priority))
+ .attr("y", y(0))
+ .attr("width", x.bandwidth())
+ .attr("height", 0)
+ .attr("fill", (d, i) => color(i))
+ .transition()
+ .duration(800)
+ .attr("y", (d) => y(d.promedio))
+ .attr("height", (d) => y(0) - y(d.promedio));
+
+ // Agregar etiquetas con el promedio encima de cada barra
+ svg
+ .selectAll(".label")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("class", "label")
+ .attr("x", (d) => x(d.priority) + x.bandwidth() / 2)
+ .attr("y", (d) => y(d.promedio) - 5)
+ .attr("text-anchor", "middle")
+ .attr("font-size", "12px")
+ .attr("fill", "#000")
+ .text((d) => (d.promedio !== undefined ? d.promedio.toFixed(2) : "0.00"));
+ }, [data]);
+
+ return ;
+};
+
+export default PromedioPrioridadChart;
\ No newline at end of file
diff --git a/src/components/Admin/charts/RangoFechasChart.jsx b/src/components/Admin/charts/RangoFechasChart.jsx
new file mode 100644
index 0000000..e3978e2
--- /dev/null
+++ b/src/components/Admin/charts/RangoFechasChart.jsx
@@ -0,0 +1,82 @@
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const RangoFechasChart = ({ reservas }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
+
+ svg.selectAll("*").remove(); // Limpiar el SVG
+
+ if (!reservas || reservas.length === 0) return;
+
+ const width = 600;
+ const height = 400;
+ const margin = { top: 20, right: 20, bottom: 30, left: 100 };
+
+ // Agrupar reservas por salón
+ const reservasPorSalon = d3.rollup(
+ reservas,
+ (v) => v.length,
+ (d) => d.idSalon
+ );
+ const data = Array.from(reservasPorSalon, ([salon, count]) => ({
+ salon,
+ count,
+ }));
+
+ svg.attr("width", width).attr("height", height);
+
+ // Escala X: lineal para la cantidad
+ const x = d3.scaleLinear()
+ .domain([0, d3.max(data, d => d.count)])
+ .range([margin.left, width - margin.right]);
+
+ // Escala Y: banda para los nombres de los salones
+ const y = d3.scaleBand()
+ .domain(data.map(d => d.salon))
+ .range([margin.top, height - margin.bottom])
+ .padding(0.1);
+
+ // Dibujar eje X
+ svg.append("g")
+ .attr("transform", `translate(0, ${height - margin.bottom})`)
+ .call(d3.axisBottom(x));
+
+ // Dibujar eje Y
+ svg.append("g")
+ .attr("transform", `translate(${margin.left}, 0)`)
+ .call(d3.axisLeft(y));
+
+ // Dibujar las barras horizontales
+ svg.selectAll(".bar")
+ .data(data)
+ .enter()
+ .append("rect")
+ .attr("class", "bar")
+ .attr("y", d => y(d.salon))
+ .attr("x", margin.left)
+ .attr("height", y.bandwidth())
+ .attr("width", d => x(d.count) - margin.left)
+ .attr("fill", (d, i) => color(i)); // Asigna un color diferente a cada barra
+
+
+ // Agregar etiquetas de la cantidad al final de cada barra
+ svg.selectAll(".label")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("class", "label")
+ .attr("y", d => y(d.salon) + y.bandwidth() / 2 + 4)
+ .attr("x", d => x(d.count) + 5)
+ .text(d => d.count)
+ .attr("font-size", "12px")
+ .attr("fill", "#000");
+ }, [reservas]);
+
+ return ;
+};
+
+export default RangoFechasChart;
\ No newline at end of file
diff --git a/src/components/Admin/charts/TotalSalonChart.jsx b/src/components/Admin/charts/TotalSalonChart.jsx
new file mode 100644
index 0000000..21514f0
--- /dev/null
+++ b/src/components/Admin/charts/TotalSalonChart.jsx
@@ -0,0 +1,147 @@
+// src/components/Admin/charts/TotalSalonChart.jsx
+import React, { useEffect, useRef } from "react";
+import * as d3 from "d3";
+
+const TotalSalonChart = ({ reservas }) => {
+ const chartRef = useRef(null);
+
+ useEffect(() => {
+ const svg = d3.select(chartRef.current);
+ svg.selectAll("*").remove(); // Limpiar el contenido del SVG
+
+ if (!reservas || reservas.length === 0) return;
+
+ // Agrupar reservas por salón
+ const reservasPorSalon = d3.rollup(
+ reservas,
+ (v) => v.length,
+ (d) => d.idSalon
+ );
+ const data = Array.from(reservasPorSalon, ([salon, count]) => ({
+ salon,
+ count,
+ }));
+
+ // Dimensiones generales
+ const width = 600;
+ const height = 400;
+ svg.attr("width", width).attr("height", height);
+
+ if (data.length === 1) {
+ // Si solo hay un salón, mostrar gráfico de dona (donut chart)
+ const radius = Math.min(width, height) / 2 - 40; // Ajusta el margen interno
+
+ const g = svg
+ .append("g")
+ .attr("transform", `translate(${width / 2}, ${height / 2})`);
+
+ // Configurar el pie/donut chart
+ const pie = d3.pie().value((d) => d.count);
+ const arc = d3.arc().innerRadius(radius * 0.5).outerRadius(radius);
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
+
+ const arcs = g
+ .selectAll(".arc")
+ .data(pie(data))
+ .enter()
+ .append("g")
+ .attr("class", "arc");
+
+ arcs
+ .append("path")
+ .attr("d", arc)
+ .attr("fill", (d, i) => color(i))
+ .transition()
+ .duration(800)
+ .attrTween("d", function (d) {
+ const i = d3.interpolate({ startAngle: 0, endAngle: 0 }, d);
+ return function (t) {
+ return arc(i(t));
+ };
+ });
+
+ // Etiqueta en el centro con el total de reservas
+ g.append("text")
+ .attr("text-anchor", "middle")
+ .attr("font-size", "24px")
+ .attr("font-weight", "bold")
+ .attr("fill", "#000")
+ .text(`${data[0].count}`);
+
+ // Etiqueta debajo del gráfico para indicar el salón
+ svg
+ .append("text")
+ .attr("x", width / 2)
+ .attr("y", height - 20)
+ .attr("text-anchor", "middle")
+ .attr("font-size", "18px")
+ .attr("font-weight", "bold")
+ .attr("fill", "#000")
+ .text(`Salón: ${data[0].salon}`);
+ } else {
+ // Si hay más de un salón, mostrar gráfico de barras horizontal
+ const margin = { top: 20, right: 20, bottom: 50, left: 100 };
+
+ // Escala X: lineal para la cantidad de reservas
+ const x = d3
+ .scaleLinear()
+ .domain([0, d3.max(data, (d) => d.count)])
+ .range([margin.left, width - margin.right])
+ .nice();
+
+ // Escala Y: de banda para los nombres de los salones
+ const y = d3
+ .scaleBand()
+ .domain(data.map((d) => d.salon))
+ .range([margin.top, height - margin.bottom])
+ .padding(0.1);
+
+ // Dibujar eje X
+ svg
+ .append("g")
+ .attr("transform", `translate(0, ${height - margin.bottom})`)
+ .call(d3.axisBottom(x));
+
+ // Dibujar eje Y
+ svg
+ .append("g")
+ .attr("transform", `translate(${margin.left}, 0)`)
+ .call(d3.axisLeft(y));
+
+ const color = d3.scaleOrdinal(d3.schemeCategory10);
+
+ // Dibujar barras horizontales con transición
+ svg
+ .selectAll(".bar")
+ .data(data)
+ .enter()
+ .append("rect")
+ .attr("class", "bar")
+ .attr("y", (d) => y(d.salon))
+ .attr("x", margin.left)
+ .attr("height", y.bandwidth())
+ .attr("width", 0)
+ .attr("fill", (d, i) => color(i))
+ .transition()
+ .duration(800)
+ .attr("width", (d) => x(d.count) - margin.left);
+
+ // Agregar etiquetas de cantidad al final de cada barra
+ svg
+ .selectAll(".label")
+ .data(data)
+ .enter()
+ .append("text")
+ .attr("class", "label")
+ .attr("y", (d) => y(d.salon) + y.bandwidth() / 2 + 4)
+ .attr("x", (d) => x(d.count) + 5)
+ .text((d) => d.count)
+ .attr("font-size", "12px")
+ .attr("fill", "#000");
+ }
+ }, [reservas]);
+
+ return ;
+};
+
+export default TotalSalonChart;
\ No newline at end of file
diff --git a/src/components/Admin/filters/DiaSalonFilter.jsx b/src/components/Admin/filters/DiaSalonFilter.jsx
new file mode 100644
index 0000000..ff841b6
--- /dev/null
+++ b/src/components/Admin/filters/DiaSalonFilter.jsx
@@ -0,0 +1,70 @@
+// src/components/Admin/filters/DiaSalonFilter.jsx
+import React, { useState } from "react";
+import styled from "styled-components";
+
+const DiaSalonFilter = ({ onBuscar }) => {
+ const [dia, setDia] = useState("");
+
+ const handleBuscar = () => {
+ if (!dia) {
+ alert("Por favor selecciona un día");
+ return;
+ }
+ onBuscar({ dia });
+ };
+
+ return (
+
+
+ Día de la semana:
+ setDia(e.target.value)}>
+ -- Selecciona un día --
+ Lunes
+ Martes
+ Miércoles
+ Jueves
+ Viernes
+ Sábado
+ Domingo
+
+
+ Buscar Reservas
+
+ );
+};
+
+export default DiaSalonFilter;
+
+const Container = styled.div`
+ margin-bottom: 20px;
+`;
+
+const Row = styled.div`
+ margin-bottom: 10px;
+ display: flex;
+ align-items: center;
+ gap: 10px;
+`;
+
+const Label = styled.label`
+ font-size: 16px;
+ font-weight: bold;
+ color: #1e1e1e;
+`;
+
+const Select = styled.select`
+ padding: 8px 16px;
+ font-size: 16px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ appearance: none;
+`;
+
+const Button = styled.button`
+ background-color: #52b69a;
+ color: #fff;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+`;
\ No newline at end of file
diff --git a/src/components/Admin/filters/MesSalonFilter.jsx b/src/components/Admin/filters/MesSalonFilter.jsx
new file mode 100644
index 0000000..2ee0075
--- /dev/null
+++ b/src/components/Admin/filters/MesSalonFilter.jsx
@@ -0,0 +1,71 @@
+// src/components/Admin/filters/MesSalonFilter.jsx
+import React, { useState } from "react";
+import styled from "styled-components";
+
+const MesSalonFilter = ({ onBuscar }) => {
+ const [mes, setMes] = useState("");
+
+ const handleBuscar = () => {
+ if (!mes) return;
+ // El valor seleccionado ya viene en formato "YYYY-MM"
+ onBuscar({ mes });
+ };
+
+ return (
+
+
+ Mes:
+ setMes(e.target.value)}>
+ -- Selecciona un mes --
+ Enero 2025
+ Febrero 2025
+ Marzo 2025
+ Abril 2025
+ Mayo 2025
+ Junio 2025
+ Julio 2025
+ Agosto 2025
+ Septiembre 2025
+ Octubre 2025
+ Noviembre 2025
+ Diciembre 2025
+
+
+ Buscar Reservas
+
+ );
+};
+
+export default MesSalonFilter;
+
+/* Estilos */
+const Container = styled.div`
+ margin-bottom: 20px;
+`;
+const Row = styled.div`
+ display: flex;
+ align-items: center;
+ gap: 10px;
+ margin-bottom: 10px;
+`;
+const Label = styled.label`
+ font-size: 16px;
+ font-weight: bold;
+ color: #1e1e1e;
+`;
+const Select = styled.select`
+ padding: 8px 16px;
+ font-size: 16px;
+ font-weight: bold;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ appearance: none;
+`;
+const Button = styled.button`
+ background-color: #52b69a;
+ color: #fff;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+`;
\ No newline at end of file
diff --git a/src/components/Admin/filters/RangoFechasFilter.jsx b/src/components/Admin/filters/RangoFechasFilter.jsx
new file mode 100644
index 0000000..b5df384
--- /dev/null
+++ b/src/components/Admin/filters/RangoFechasFilter.jsx
@@ -0,0 +1,69 @@
+// src/components/Admin/filters/RangoFechasFilter.jsx
+import React, { useState } from "react";
+import styled from "styled-components";
+
+const RangoFechasFilter = ({ onBuscar }) => {
+ const [fechaInicio, setFechaInicio] = useState("");
+ const [fechaFin, setFechaFin] = useState("");
+
+ const handleBuscar = () => {
+ if (!fechaInicio || !fechaFin) {
+ alert("Por favor, selecciona ambas fechas.");
+ return;
+ }
+ onBuscar({ fechaInicio, fechaFin });
+ };
+
+ return (
+
+
+ Fecha Inicio:
+ setFechaInicio(e.target.value)}
+ />
+
+
+ Fecha Fin:
+ setFechaFin(e.target.value)}
+ />
+
+ Buscar Reservas
+
+ );
+};
+
+export default RangoFechasFilter;
+
+const Container = styled.div`
+ margin-bottom: 20px;
+`;
+const Row = styled.div`
+ display: flex;
+ align-items: center;
+ margin-bottom: 10px;
+ gap: 10px;
+`;
+const Label = styled.label`
+ font-size: 16px;
+ font-weight: bold;
+ color: #1e1e1e;
+`;
+const Input = styled.input`
+ padding: 8px 16px;
+ font-size: 16px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+`;
+const Button = styled.button`
+ background-color: #52b69a;
+ color: #fff;
+ padding: 8px 16px;
+ border: none;
+ border-radius: 8px;
+ cursor: pointer;
+`;
\ No newline at end of file
diff --git a/src/components/Admin/filters/TotalSalonFilter.jsx b/src/components/Admin/filters/TotalSalonFilter.jsx
new file mode 100644
index 0000000..7203e8a
--- /dev/null
+++ b/src/components/Admin/filters/TotalSalonFilter.jsx
@@ -0,0 +1,81 @@
+// src/components/Admin/filters/TotalSalonFilter.jsx
+import React, { useState, useEffect } from "react";
+import styled from "styled-components";
+import { getSalones } from "../../../api/salon"; // Ajusta la ruta según tu estructura
+
+/**
+ * Filtro para seleccionar un salón (opcional).
+ * Si se deja en "Todos", se interpretará que se desean reservas de todos los salones.
+ */
+const TotalSalonFilter = ({ onBuscar, token }) => {
+ const [salon, setSalon] = useState("");
+ const [listaSalones, setListaSalones] = useState([]);
+
+ useEffect(() => {
+ // Cargar la lista de salones activos
+ getSalones({ activo: true }, token)
+ .then((salones) => setListaSalones(salones))
+ .catch((err) => console.error("Error al obtener salones:", err));
+ }, []);
+
+ const handleBuscar = () => {
+ // Si no se selecciona ningún salón se enviará el filtro vacío para traer todas las reservas
+ onBuscar({ ...(salon && { idSalon: salon }) });
+ };
+
+ return (
+
+
+ Salón (opcional):
+ setSalon(e.target.value)}>
+ Todos
+ {listaSalones.map((s) => (
+
+ {s.nombre} ({s.mnemonico})
+
+ ))}
+
+
+ Buscar Reservas
+
+ );
+};
+
+export default TotalSalonFilter;
+
+const Container = styled.div`
+ margin-bottom: 20px;
+ text-align: center;
+`;
+
+const Row = styled.div`
+ margin-bottom: 10px;
+ display: flex;
+ justify-content: center;
+ align-items: center;
+ gap: 10px;
+`;
+
+const Label = styled.label`
+ font-size: 16px;
+ font-weight: bold;
+ color: #1e1e1e;
+`;
+
+const Select = styled.select`
+ padding: 8px 16px;
+ font-size: 16px;
+ border: 1px solid #ccc;
+ border-radius: 4px;
+ appearance: none;
+`;
+
+const Button = styled.button`
+ background-color: #52b69a;
+ color: #fff;
+ padding: 10px 20px;
+ border: none;
+ border-radius: 8px;
+ font-size: 16px;
+ cursor: pointer;
+`;
\ No newline at end of file
diff --git a/src/components/Login/FormInput.jsx b/src/components/Login/FormInput.jsx
new file mode 100644
index 0000000..38f5aef
--- /dev/null
+++ b/src/components/Login/FormInput.jsx
@@ -0,0 +1,58 @@
+import React from "react";
+import styled from "styled-components";
+
+const FormInput = ({ label, type, onChange, value }) => {
+ return (
+
+
+
+ {label}
+
+ );
+};
+
+const InputWrapper = styled.div`
+ margin-bottom: 60px; /* Ajusta el espacio vertical entre los campos */
+ width: 100%;
+ @media (max-width: 640px) {
+ margin-bottom: 40px;
+ }
+`;
+
+const InputLabel = styled.label`
+ color: rgb(107, 155, 61); /* Negro */
+ font-family: "Inter", sans-serif;
+ font-size: 14px;
+ font-weight: 400;
+ margin-top: 10px;
+ display: block;
+`;
+
+const StyledInput = styled.input`
+ width: 100%;
+ border: none;
+ background: transparent;
+ font-family: "Inter", sans-serif;
+ font-size: 16px; /* Tamaño mayor */
+ outline: none;
+ padding: 0;
+ color: #000; /* Texto en negro */
+
+ ::placeholder {
+ color: #999; /* Color del placeholder */
+ }
+`;
+
+const InputUnderline = styled.div`
+ width: 100%;
+ height: 1px;
+ margin-top: 10px;
+ background-color: #aaba70;
+`;
+
+export default FormInput;
\ No newline at end of file
diff --git a/src/components/Login/ImageSection.jsx b/src/components/Login/ImageSection.jsx
new file mode 100644
index 0000000..a24d864
--- /dev/null
+++ b/src/components/Login/ImageSection.jsx
@@ -0,0 +1,55 @@
+import React from "react";
+import styled from "styled-components";
+
+const ImageSection = () => {
+ return (
+
+
+
+
+ );
+};
+
+const ImageContainer = styled.section`
+ width: 50%;
+ position: relative;
+ overflow: hidden;
+ @media (max-width: 991px) {
+ width: 100%;
+ height: 400px;
+ }
+ @media (max-width: 640px) {
+ height: 300px;
+ }
+`;
+
+const BackgroundImage = styled.img`
+ width: 100%;
+ height: 100%;
+ object-fit: cover;
+ margin-bottom: 200px;
+ margin-left: 1px;
+`;
+
+const SchoolLogo = styled.img`
+ position: absolute;
+ bottom: 25px;
+ right: 25px;
+ width: 468px;
+ height: 240px;
+ @media (max-width: 991px) {
+ width: 300px;
+ height: auto;
+ }
+ @media (max-width: 640px) {
+ width: 200px;
+ }
+`;
+
+export default ImageSection;
\ No newline at end of file
diff --git a/src/components/Login/LoginForm.jsx b/src/components/Login/LoginForm.jsx
new file mode 100644
index 0000000..b55a4a3
--- /dev/null
+++ b/src/components/Login/LoginForm.jsx
@@ -0,0 +1,133 @@
+import React, { useState } from "react";
+import { useNavigate } from "react-router-dom";
+import styled from "styled-components";
+import FormInput from "./FormInput";
+import { login } from "../../api/auth";
+import { consultarUsuarioPorCorreo } from "../../api/usuario";
+
+function LoginForm({ onLogin }) {
+ const [correo, setCorreo] = useState("");
+ const [password, setPassword] = useState("");
+ const [errorMsg, setErrorMsg] = useState("");
+ const navigate = useNavigate();
+
+ const handleSubmit = async (e) => {
+ e.preventDefault();
+ setErrorMsg("");
+ try {
+ const token = await login(correo, password);
+
+ if (token) {
+ localStorage.setItem("token", token);
+
+ try {
+ const usuario = await consultarUsuarioPorCorreo(correo);
+ onLogin(usuario);
+
+ if (usuario.isAdmin) {
+ navigate("/administrador");
+ } else {
+ navigate("/home");
+ }
+ } catch (error) {
+
+ }
+ } else {
+ setErrorMsg("Usuario no encontrado o contraseña incorrecta");
+ }
+ } catch (error) {
+ setErrorMsg("Usuario no encontrado o contraseña incorrecta");
+ console.error(error);
+ }
+ };
+
+ return (
+
+
+
+ {errorMsg && {errorMsg} }
+
+
+ );
+};
+
+const FormContainer = styled.section`
+ display: flex;
+ flex-direction: column;
+ padding: 155px 153px;
+ width: 50%;
+ position: relative;
+ @media (max-width: 991px) {
+ width: 100%;
+ padding: 40px 20px;
+ }
+ @media (max-width: 640px) {
+ padding: 20px;
+ }
+`;
+
+const LogoImage = styled.img`
+ width: 226px;
+ height: 151px;
+ margin: 0 auto 137px;
+ @media (max-width: 640px) {
+ width: 180px;
+ height: 120px;
+ margin-bottom: 60px;
+ }
+`;
+
+const LoginButton = styled.button`
+ width: 246px;
+ height: 54px;
+ border-radius: 10px;
+ border: none;
+ color:white;
+ font-family: "Inter", sans-serif;
+ font-size: 19px;
+ font-weight: 700;
+ margin: 0 auto;
+ margin-top: 100px;
+ cursor: pointer;
+ background-color: rgb(107, 155, 61);
+ display: block;
+ @media (max-width: 640px) {
+ width: 100%;
+ }
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-size: 16px;
+ font-weight: bold;
+ text-align: center;
+ margin-top: 20px;
+`;
+
+const CvdsLogo = styled.img`
+ width: 59px;
+ height: 63px;
+ margin: 130px auto 0;
+`;
+
+export default LoginForm;
\ No newline at end of file
diff --git a/src/pages/Administrator/AdministratorHome.jsx b/src/pages/Administrator/AdministratorHome.jsx
new file mode 100644
index 0000000..0bffb6a
--- /dev/null
+++ b/src/pages/Administrator/AdministratorHome.jsx
@@ -0,0 +1,78 @@
+import React from "react";
+import styled from "styled-components";
+import ConsultaMesSalon from "./consultaModal/ConsultaMesSalon";
+import ConsultaDiaSalon from "./consultaModal/ConsultaDiaSalon";
+import ConsultaRangoFechas from "./consultaModal/ConsultaRangoFechas";
+import ConsultaEstado from "./consultaModal/ConsultaEstado";
+import ConsultaTotalSalon from "./consultaModal/ConsultaTotalSalon";
+import ConsultaPrioridad from "./consultaModal/ConsultaPrioridad";
+import ConsultaDemanda from "./consultaModal/ConsultaDemanda";
+
+const CONSULTAS = [
+ { id: 1, titulo: "Reservas por Mes y Salón", componente: (token) => },
+ { id: 2, titulo: "Reservas por Día y Salón", componente: (token) => },
+ { id: 3, titulo: "Reservas por Rango de Fechas", componente: (token) => },
+ { id: 4, titulo: "Comparativo Activas vs Inactivas", componente: (token) => },
+ { id: 5, titulo: "Reservas Totales por Salón", componente: (token) => },
+ { id: 6, titulo: "Promedio de Reservas por Prioridad", componente: (token) => },
+ { id: 7, titulo: "Demanda por Laboratorios", componente: (token) => },
+];
+
+const AdministratorHome = ({ token }) => {
+ return (
+
+ Centro de Insights
+
+ {CONSULTAS.map((consulta) => (
+
+ {consulta.titulo}
+ {consulta.componente(token)}
+
+ ))}
+
+
+ );
+};
+
+export default AdministratorHome;
+
+/* Estilos */
+const MainContainer = styled.main`
+ display: flex;
+ flex-direction: column;
+ width: 100%;
+ color: var(--variable-collection-current-color);
+`;
+
+const TitleSection = styled.h2`
+ font-size: 18px;
+ font-weight: 600;
+ text-align: flex-start;
+`;
+
+const GridContainer = styled.div`
+ display: grid;
+ grid-template-columns: repeat(3, 1fr);
+ gap: 20px;
+ margin-bottom: 20px;
+
+ @media (max-width: 768px) {
+ grid-template-columns: repeat(2, 1fr);
+ }
+ @media (max-width: 480px) {
+ grid-template-columns: 1fr;
+ }
+`;
+
+const ConsultaCard = styled.div`
+ padding: 15px;
+ border-radius: 10px;
+ font-weight: 600;
+ text-align: flex-start;
+ border: 1px solid #D9D9D9;
+ transition: background-color 0.3s ease;
+ box-shadow: 0px 4px 6px var(--variable-collection-shadow);
+`;
+
+const ContentContainer = styled.div`
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaDemanda.jsx b/src/pages/Administrator/consultaModal/ConsultaDemanda.jsx
new file mode 100644
index 0000000..1378683
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaDemanda.jsx
@@ -0,0 +1,54 @@
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+import DemandaChart from "../../../components/Admin/charts/DemandaChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaDemanda = ({ token }) => {
+ const [reservas, setReservas] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ useEffect(() => {
+ handleBuscar();
+ }, []);
+
+ const handleBuscar = async () => {
+ try {
+ setErrorMsg("");
+ setReservas([]);
+
+ // No se envían filtros; se obtiene el total de reservas
+ const data = await getReservas({}, token);
+ if (!data || data.length === 0) {
+ setErrorMsg("No se encontraron reservas para mostrar la demanda.");
+ } else {
+ setReservas(data);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando reservas");
+ }
+ };
+
+ return (
+
+
+ {errorMsg && {errorMsg} }
+
+
+
+ );
+};
+
+export default ConsultaDemanda;
+
+const Container = styled.div`
+`;
+
+const Body = styled.div`
+ margin-top: 10px;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaDiaSalon.jsx b/src/pages/Administrator/consultaModal/ConsultaDiaSalon.jsx
new file mode 100644
index 0000000..ead58e5
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaDiaSalon.jsx
@@ -0,0 +1,56 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import DiaSalonFilter from "../../../components/Admin/filters/DiaSalonFilter";
+import DiaSalonChart from "../../../components/Admin/charts/DiaSalonChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaDiaSalon = ({ token }) => {
+ const [reservas, setReservas] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ const handleBuscar = async (filtros) => {
+ try {
+ setErrorMsg("");
+ setReservas([]);
+ if (!filtros.dia) {
+ setErrorMsg("Por favor selecciona un día.");
+ return;
+ }
+ // Llama al endpoint de reservas filtrando solo por día de la semana
+ const data = await getReservas({diaSemana: filtros.dia}, token);
+ if (!data || data.length === 0) {
+ setErrorMsg("No se encontraron reservas para el día seleccionado.");
+ } else {
+ setReservas(data);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando reservas");
+ }
+ };
+
+ return (
+
+
+
+ {errorMsg && {errorMsg} }
+
+
+
+ );
+};
+
+export default ConsultaDiaSalon;
+
+/* Estilos del contenedor */
+const Container = styled.div`
+`;
+
+const Body = styled.div`
+ margin-top: 10px;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaEstado.jsx b/src/pages/Administrator/consultaModal/ConsultaEstado.jsx
new file mode 100644
index 0000000..05316af
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaEstado.jsx
@@ -0,0 +1,55 @@
+import React, { useEffect, useState } from "react";
+import styled from "styled-components";
+import EstadoChart from "../../../components/Admin/charts/EstadoChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaEstado = ({ token }) => {
+ const [reservas, setReservas] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+
+ useEffect(() => {
+ handleBuscar();
+ }, []);
+
+ const handleBuscar = async () => {
+ try {
+ setErrorMsg("");
+ setReservas([]);
+ // Llamar al endpoint de reservas sin filtro específico de estado
+ const data = await getReservas({}, token);
+ if (!data || data.length === 0) {
+ setErrorMsg("No se encontraron reservas.");
+ } else {
+ setReservas(data);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando reservas");
+ }
+ };
+
+ return (
+
+
+ {errorMsg && {errorMsg} }
+ {reservas.length > 0 && }
+
+
+ );
+};
+
+export default ConsultaEstado;
+
+/* Estilos */
+const Container = styled.div`
+`;
+
+const Body = styled.div`
+ margin-top: 10px;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaMesSalon.jsx b/src/pages/Administrator/consultaModal/ConsultaMesSalon.jsx
new file mode 100644
index 0000000..fbcd017
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaMesSalon.jsx
@@ -0,0 +1,56 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import MesSalonFilter from "../../../components/Admin/filters/MesSalonFilter";
+import MesSalonChart from "../../../components/Admin/charts/MesSalonChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaMesSalon = ({ token }) => {
+ const [reservas, setReservas] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ const handleBuscar = async (filtros) => {
+ try {
+ setErrorMsg("");
+ setReservas([]);
+
+ if (!filtros.mes) {
+ setErrorMsg("Por favor selecciona un mes.");
+ return;
+ }
+ const data = await getReservas({ mes: filtros.mes }, token);
+ if (!data || data.length === 0) {
+ setErrorMsg("No se encontraron reservas para el mes seleccionado.");
+ } else {
+ setReservas(data);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando reservas");
+ }
+ };
+
+ return (
+
+
+
+ {errorMsg && {errorMsg} }
+
+
+
+ );
+};
+
+export default ConsultaMesSalon;
+
+/* Estilos */
+const Container = styled.div`
+`;
+
+const Body = styled.div`
+ margin-top: 10px;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaPrioridad.jsx b/src/pages/Administrator/consultaModal/ConsultaPrioridad.jsx
new file mode 100644
index 0000000..9dd6823
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaPrioridad.jsx
@@ -0,0 +1,55 @@
+import React, { useState, useEffect } from "react";
+import styled from "styled-components";
+import PromedioPrioridadChart from "../../../components/Admin/charts/PromedioPrioridadChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaPrioridad = ({ token }) => {
+ const [data, setData] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ useEffect(() => {
+ handleBuscar();
+ }, []);
+
+ const handleBuscar = async () => {
+ try {
+ setErrorMsg("");
+ setData([]);
+ // Se asume que el endpoint interpreta el parámetro "consultarPorPrioridad"
+ const filtros = { consultarPorPrioridad: true };
+ const result = await getReservas(filtros, token);
+ if (!result || result.length === 0) {
+ setErrorMsg("No se encontraron datos para el promedio de reservas por prioridad.");
+ } else {
+ // Se asume que cada objeto viene con { priority, promedio }
+ setData(result);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando datos.");
+ }
+ };
+
+ return (
+
+
+ {errorMsg && {errorMsg} }
+
+
+
+ );
+};
+
+export default ConsultaPrioridad;
+
+const Container = styled.div`
+`;
+
+const Body = styled.div`
+ margin-top: 10px;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx b/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx
new file mode 100644
index 0000000..015b4ce
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx
@@ -0,0 +1,59 @@
+import React, { useState } from "react";
+import styled from "styled-components";
+import RangoFechasFilter from "../../../components/Admin/filters/RangoFechasFilter";
+import RangoFechasChart from "../../../components/Admin/charts/RangoFechasChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaRangoFechas = ({ token }) => {
+ const [reservas, setReservas] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ const handleBuscar = async (filtros) => {
+ try {
+ setErrorMsg("");
+ setReservas([]);
+ if (!filtros.fechaInicio || !filtros.fechaFin) {
+ setErrorMsg("Por favor, selecciona ambas fechas.");
+ return;
+ }
+ // Se llama al endpoint con los parámetros fechaInicio y fechaFin
+ const data = await getReservas({
+ fechaInicio: filtros.fechaInicio,
+ fechaFin: filtros.fechaFin
+ }, token);
+ if (!data || data.length === 0) {
+ setErrorMsg("No se encontraron reservas en el rango de fechas seleccionado.");
+ } else {
+ setReservas(data);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando reservas");
+ }
+ };
+
+ return (
+
+
+
+ {errorMsg && {errorMsg} }
+
+
+
+ );
+};
+
+export default ConsultaRangoFechas;
+
+/* Estilos */
+const Container = styled.div`
+`;
+
+const Body = styled.div`
+ margin-top: 10px;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Administrator/consultaModal/ConsultaTotalSalon.jsx b/src/pages/Administrator/consultaModal/ConsultaTotalSalon.jsx
new file mode 100644
index 0000000..08162f1
--- /dev/null
+++ b/src/pages/Administrator/consultaModal/ConsultaTotalSalon.jsx
@@ -0,0 +1,53 @@
+import React, { useState, useEffect } from "react";
+import styled from "styled-components";
+import TotalSalonFilter from "../../../components/Admin/filters/TotalSalonFilter";
+import TotalSalonChart from "../../../components/Admin/charts/TotalSalonChart";
+import { getReservas } from "../../../api/reserva";
+
+const ConsultaTotalSalon = ({token}) => {
+ const [reservas, setReservas] = useState([]);
+ const [errorMsg, setErrorMsg] = useState("");
+
+ useEffect(() => {
+ handleBuscar("");
+ }, []);
+
+ const handleBuscar = async (filtros) => {
+ try {
+ setErrorMsg("");
+ setReservas([]);
+ // Si se envía un filtro de idSalon, se usa; si no, se obtiene la data de todos los salones.
+ const data = await getReservas({ ...(filtros.idSalon && { idSalon: filtros.idSalon }) }, token);
+ if (!data || data.length === 0) {
+ setErrorMsg("No se encontraron reservas para el salón seleccionado.");
+ } else {
+ setReservas(data);
+ }
+ } catch (error) {
+ setErrorMsg(error.message || "Error consultando reservas");
+ }
+ };
+
+ return (
+
+
+ {errorMsg && {errorMsg} }
+ {reservas.length > 0 && }
+
+ );
+};
+
+export default ConsultaTotalSalon;
+
+/* Estilos */
+const Container = styled.div`
+ display: flex;
+ flex-direction: column;
+ align-items: flex-start;
+`;
+
+const ErrorMessage = styled.div`
+ color: red;
+ font-weight: bold;
+ margin: 10px 0;
+`;
\ No newline at end of file
diff --git a/src/pages/Login/LoginPage.jsx b/src/pages/Login/LoginPage.jsx
new file mode 100644
index 0000000..c07517b
--- /dev/null
+++ b/src/pages/Login/LoginPage.jsx
@@ -0,0 +1,25 @@
+import React from "react";
+import styled from "styled-components";
+import LoginForm from "../../components/Login/LoginForm";
+import ImageSection from "../../components/Login/ImageSection";
+
+function LoginPage({ onLogin }) {
+ return (
+
+
+
+
+ );
+};
+
+const PageContainer = styled.main`
+ display: flex;
+ width: 100%;
+ height: 100vh;
+ background-color:rgb(242, 245, 229);
+ @media (max-width: 991px) {
+ flex-direction: column;
+ }
+`;
+
+export default LoginPage;
\ No newline at end of file