diff --git a/.github/workflows/ci-cd.yml b/.github/workflows/ci-cd-production.yml similarity index 91% rename from .github/workflows/ci-cd.yml rename to .github/workflows/ci-cd-production.yml index ce44b67..a5c8eff 100644 --- a/.github/workflows/ci-cd.yml +++ b/.github/workflows/ci-cd-production.yml @@ -1,4 +1,4 @@ -name: CI/CD Pipeline ELysium +name: CI/CD Pipeline ELysium Production on: push: @@ -6,13 +6,12 @@ on: - main pull_request: branches: - - develop - main env: - AZURE_WEBAPP_NAME: elysiumFrontEnd + AZURE_WEBAPP_NAME: Eros AZURE_WEBAPP_PACKAGE_PATH: '.' - NODE_VERSION: '18.x' + NODE_VERSION: '22.x' jobs: build-and-deploy: diff --git a/.github/workflows/ci-cd-testing.yml b/.github/workflows/ci-cd-testing.yml new file mode 100644 index 0000000..800b2b7 --- /dev/null +++ b/.github/workflows/ci-cd-testing.yml @@ -0,0 +1,57 @@ +name: CI/CD Pipeline ELysium Testing + +on: + push: + branches: + - develop + pull_request: + branches: + - develop + +env: + AZURE_WEBAPP_NAME: cicero + AZURE_WEBAPP_PACKAGE_PATH: '.' + NODE_VERSION: '22.x' + +jobs: + build-and-deploy: + runs-on: ubuntu-latest + steps: + - name: Checkout repository + uses: actions/checkout@v4 + + - uses: azure/login@v1 + with: + creds: ${{ secrets.AZURE_TEST_ENVIRONMENT }} + + - name: Cache Node.js modules + uses: actions/cache@v3 + with: + path: ~/.npm + key: ${{ runner.os }}-node-${{ hashFiles('**/package-lock.json') }} + restore-keys: | + ${{ runner.os }}-node- + + - name: Setup Node.js + uses: actions/setup-node@v4 + with: + node-version: ${{ env.NODE_VERSION }} + cache: 'npm' + + - name: Install dependencies + run: npm install + + - name: Run tests + run: npm run test --if-present + + - name: Build project + run: npm run build --if-present + + - name: Deploy to Azure Web Apps + uses: azure/webapps-deploy@v3 + with: + app-name: ${{ env.AZURE_WEBAPP_NAME }} + package: ./build + + - name: Logout from Azure + run: az logout diff --git a/src/components/Admin/charts/RangoFechasChart.jsx b/src/components/Admin/charts/RangoFechasChart.jsx index 4c55bef..e6c9522 100644 --- a/src/components/Admin/charts/RangoFechasChart.jsx +++ b/src/components/Admin/charts/RangoFechasChart.jsx @@ -9,8 +9,6 @@ const RangoFechasChart = ({ reservas }) => { const svg = d3.select(container); svg.selectAll("*").remove(); // Limpiar el SVG - if (!reservas || reservas.length === 0) return; - // Ajustar tamaño dinámico según el contenedor const containerWidth = container.clientWidth || 600; const containerHeight = containerWidth * 0.6; // Mantener proporción @@ -21,6 +19,17 @@ const RangoFechasChart = ({ reservas }) => { const margin = { top: 20, right: 20, bottom: 30, left: 100 }; + if (reservas.length === 0) { + svg.append("text") + .attr("x", containerWidth / 2) + .attr("y", containerHeight / 2) + .attr("text-anchor", "middle") + .attr("font-size", "16px") + .attr("fill", "#666") + .text("No hay datos disponibles"); + 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 })); diff --git a/src/components/Table/UserRow.jsx b/src/components/Table/UserRow.jsx index efe003b..73c111d 100644 --- a/src/components/Table/UserRow.jsx +++ b/src/components/Table/UserRow.jsx @@ -39,7 +39,7 @@ const RoleBadge = styled.span` background-color: ${props => props.$isAdmin ? '#e3f2fd' : '#f5f5f5'}; color: ${props => props.$isAdmin ? '#1976d2' : '#616161'}; `; -function UserRow({ user, onUpdateUser, onEditUser }) { +function UserRow({ user, onEditUser }) { // Extraemos las propiedades del usuario const {idInstitucional, nombre, apellido, correoInstitucional, isAdmin, activo } = user; diff --git a/src/components/Table/UserTable.jsx b/src/components/Table/UserTable.jsx index 8c06c09..7c3bd13 100644 --- a/src/components/Table/UserTable.jsx +++ b/src/components/Table/UserTable.jsx @@ -56,7 +56,6 @@ function UserTable({ users, onUpdateUser }) { ))} diff --git a/src/components/popup/CRUDSalonModal/AddSalonModal.jsx b/src/components/popup/CRUDSalonModal/AddSalonModal.jsx index 9a1438f..f7f33b4 100644 --- a/src/components/popup/CRUDSalonModal/AddSalonModal.jsx +++ b/src/components/popup/CRUDSalonModal/AddSalonModal.jsx @@ -4,14 +4,24 @@ import CRUDSalonForm from "./CRUDSalonForm"; function AddSalonModal({ onClose, newSalon, setNewSalon, handleAddSalon }) { const [tempSalon, setTempSalon] = useState({ + mnemonico: newSalon?.mnemonico || "", nombre: newSalon?.nombre || "", descripcion: newSalon?.descripcion || "", - mnemonico: newSalon?.mnemonico || "", ubicacion: newSalon?.ubicacion || "", capacidad: newSalon?.capacidad || 0, recursos: newSalon?.recursos || [{ nombre: "", cantidad: 1, especificaciones: [], activo: true }], }); + const isFormComplete = () => { + return ( + tempSalon.mnemonico.trim() !== "" && + tempSalon.nombre.trim() !== "" && + tempSalon.descripcion.trim() !== "" && + tempSalon.ubicacion.trim() !== "" && + tempSalon.capacidad > 0 + ); + }; + const handleGuardar = () => { setNewSalon({ ...tempSalon, @@ -32,7 +42,7 @@ function AddSalonModal({ onClose, newSalon, setNewSalon, handleAddSalon }) { />
- +
diff --git a/src/components/popup/CRUDSalonModal/CRUDSalonModal.css b/src/components/popup/CRUDSalonModal/CRUDSalonModal.css index 37f541c..5d40549 100644 --- a/src/components/popup/CRUDSalonModal/CRUDSalonModal.css +++ b/src/components/popup/CRUDSalonModal/CRUDSalonModal.css @@ -78,6 +78,14 @@ cursor: pointer; } + .popup-overlay .salon-modal .save-button:disabled { + background: #ddd; + color: #999; + cursor: not-allowed; + opacity: 0.6; + } + + .popup-overlay .modal-content .capacity-container { display: flex; flex-direction: column; diff --git a/src/components/popup/CRUDSalonModal/EditarSalonModal.jsx b/src/components/popup/CRUDSalonModal/EditarSalonModal.jsx index 86aebfe..4d839fc 100644 --- a/src/components/popup/CRUDSalonModal/EditarSalonModal.jsx +++ b/src/components/popup/CRUDSalonModal/EditarSalonModal.jsx @@ -3,25 +3,40 @@ import "./CRUDSalonModal.css"; import CRUDSalonForm from "./CRUDSalonForm"; function EditarSalonModal({ onClose, newSalon, setNewSalon, handleEdit }) { - const defaultSalon = { - nombre: "", - descripcion: "", - mnemonico: "", - ubicacion: "", - capacidad: 0, - recursos: [], + const [isInitialized, setIsInitialized] = useState(false); + const [tempSalon, setTempSalon] = useState({ + mnemonico: newSalon?.mnemonico || "", + nombre: newSalon?.nombre || "", + descripcion: newSalon?.descripcion || "", + ubicacion: newSalon?.ubicacion || "", + capacidad: newSalon?.capacidad || 0, + recursos: newSalon?.recursos || [{ nombre: "", cantidad: 1, especificaciones: [], activo: true }], + }); + + const isFormComplete = () => { + return ( + tempSalon.mnemonico.trim() !== "" && + tempSalon.nombre.trim() !== "" && + tempSalon.descripcion.trim() !== "" && + tempSalon.ubicacion.trim() !== "" && + tempSalon.capacidad > 0 + ); }; - const [tempSalon, setTempSalon] = useState(newSalon || defaultSalon); useEffect(() => { - if (newSalon) { + if (!isInitialized && newSalon) { setTempSalon(newSalon); + setIsInitialized(true); } - }, [newSalon]); + }, [newSalon, isInitialized]); const handleGuardar = () => { - setNewSalon(tempSalon); - handleEdit(); + const updatedSalon = { + ...tempSalon, + recursos: tempSalon.recursos?.length > 0 ? tempSalon.recursos : [{ nombre: "", cantidad: 1, especificaciones: [], activo: true }], + }; + setNewSalon(updatedSalon); + handleEdit(updatedSalon); }; return ( @@ -36,7 +51,7 @@ function EditarSalonModal({ onClose, newSalon, setNewSalon, handleEdit }) { />
- +
diff --git a/src/config/config.js b/src/config/config.js index 57b0bf8..838ed60 100644 --- a/src/config/config.js +++ b/src/config/config.js @@ -1 +1 @@ -export const BASE_URL = "http://localhost:8080/api"; \ No newline at end of file +export const BASE_URL = "https://hades-g4apbhdua4gtbbf5.canadacentral-01.azurewebsites.net/api"; \ No newline at end of file diff --git a/src/pages/Admin/AddUserModal.jsx b/src/pages/Admin/AddUserModal.jsx index 2bbae05..6579acb 100644 --- a/src/pages/Admin/AddUserModal.jsx +++ b/src/pages/Admin/AddUserModal.jsx @@ -219,7 +219,7 @@ function AddUserModal({ onClose, onAdd }) { try { // Crear nuevo usuario - const nuevoUsuario = await agregarUsuario({ + await agregarUsuario({ idInstitucional: parseInt(formData.idInstitucional), nombre: formData.nombre, apellido: formData.apellido, @@ -230,7 +230,7 @@ function AddUserModal({ onClose, onAdd }) { // Notificar al componente padre sobre la creación exitosa if (onAdd) { - onAdd(nuevoUsuario); + onAdd(); } onClose(); // Cerrar modal tras guardar diff --git a/src/pages/Admin/EditUserModal.jsx b/src/pages/Admin/EditUserModal.jsx index 41c1e48..0a4751d 100644 --- a/src/pages/Admin/EditUserModal.jsx +++ b/src/pages/Admin/EditUserModal.jsx @@ -209,7 +209,7 @@ function EditUserModal({ user, onClose, onUpdate }) { try { // Solo enviamos los campos que deseamos actualizar - const usuarioActualizado = await actualizarInformacionUsuario( + await actualizarInformacionUsuario( formData.idInstitucional, { nombre: formData.nombre, @@ -222,7 +222,7 @@ function EditUserModal({ user, onClose, onUpdate }) { // Notificar al componente padre sobre la actualización exitosa if (onUpdate) { - onUpdate(usuarioActualizado || { ...user, ...formData }); + onUpdate(); } onClose(); // Cerrar modal tras guardar cambios diff --git a/src/pages/Admin/GestionarUsuarios.jsx b/src/pages/Admin/GestionarUsuarios.jsx index dc87715..e9ac699 100644 --- a/src/pages/Admin/GestionarUsuarios.jsx +++ b/src/pages/Admin/GestionarUsuarios.jsx @@ -1,4 +1,4 @@ -import React, { useState, useEffect } from 'react'; +import React, { useState, useEffect, useCallback } from 'react'; import styled from 'styled-components'; import { consultarUsuarios } from '../../api/usuario'; import UserFilters from '../../components/UserFilters'; @@ -130,38 +130,39 @@ function GestionarUsuarios() { isAdmin: null // null = sin filtro, true = admins, false = no admins }); - // Efecto para cargar usuarios con los filtros aplicados - useEffect(() => { - const loadUsers = async () => { - setLoading(true); - setError(null); + // Manejador para cargar usuarios + const loadUsers = useCallback(async () => { + setLoading(true); + setError(null); + + try { + // Obtener usuarios con filtros + const data = await consultarUsuarios(filters); - try { - // Obtener usuarios con filtros - const data = await consultarUsuarios(filters); - - // Filtrar por término de búsqueda si existe - let filteredUsers = data || []; - if (searchTerm) { - const searchLower = searchTerm.toLowerCase(); - filteredUsers = filteredUsers.filter(user => - user.nombre?.toLowerCase().includes(searchLower) || - user.apellido?.toLowerCase().includes(searchLower) || - user.correoInstitucional?.toLowerCase().includes(searchLower) - ); - } - - setUsers(filteredUsers); - } catch (err) { - console.error("Error al cargar usuarios:", err); - setError(err.message || "No se pudieron cargar los usuarios"); - } finally { - setLoading(false); + // Filtrar por término de búsqueda si existe + let filteredUsers = data || []; + if (searchTerm) { + const searchLower = searchTerm.toLowerCase(); + filteredUsers = filteredUsers.filter(user => + user.nombre?.toLowerCase().includes(searchLower) || + user.apellido?.toLowerCase().includes(searchLower) || + user.correoInstitucional?.toLowerCase().includes(searchLower) + ); } - }; - + + setUsers(filteredUsers); + } catch (err) { + console.error("Error al cargar usuarios:", err); + setError(err.message || "No se pudieron cargar los usuarios"); + } finally { + setLoading(false); + } + }, [filters, searchTerm]); + + // Efecto para cargar usuarios con los filtros aplicados + useEffect(() => { loadUsers(); - }, [filters, searchTerm]); // Re-fetch cuando cambian los filtros o el término de búsqueda + }, [loadUsers]); // Manejador para la búsqueda const handleSearch = (e) => { @@ -170,19 +171,21 @@ function GestionarUsuarios() { }; // Manejador para añadir un nuevo usuario - const handleAddUser = (newUser) => { - setUsers(prevUsers => [...prevUsers, newUser]); + const handleAddUser = async () => { + try { + await loadUsers(); + } catch (err) { + console.error("Error actualizando usuarios tras añadir:", err); + } }; // Manejador para actualizar un usuario existente - const handleUpdateUser = (updatedUser) => { - setUsers(prevUsers => - prevUsers.map(user => - user.idInstitucional === updatedUser.idInstitucional - ? updatedUser - : user - ) - ); + const handleUpdateUser = async () => { + try { + await loadUsers(); + } catch (err) { + console.error("Error actualizando usuarios tras editar:", err); + } }; return ( diff --git a/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx b/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx index d8c7ab1..a03ec4f 100644 --- a/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx +++ b/src/pages/Administrator/consultaModal/ConsultaRangoFechas.jsx @@ -16,16 +16,30 @@ const ConsultaRangoFechas = () => { 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 - }); - if (!data || data.length === 0) { - setErrorMsg("No se encontraron reservas en el rango de fechas seleccionado."); - } else { - setReservas(data); + + const fechaInicio = new Date(filtros.fechaInicio); + const fechaFin = new Date(filtros.fechaFin); + + if (fechaInicio > fechaFin) { + setErrorMsg("La fecha de inicio no puede ser mayor a la fecha de fin."); + return; } + + let currentDate = new Date(fechaInicio); + const fechasConsulta = []; + while (currentDate <= fechaFin) { + fechasConsulta.push(currentDate.toISOString().split("T")[0]); + currentDate.setDate(currentDate.getDate() + 1); + } + + const reservasPromises = fechasConsulta.map(fecha => + getReservas({ fecha }) + ); + const resultadosDiarios = await Promise.all(reservasPromises); + + const reservasAcumuladas = resultadosDiarios.flat(); + + setReservas(reservasAcumuladas); } catch (error) { setErrorMsg(error.message || "Error consultando reservas"); } diff --git a/src/pages/Salones/GestionarSalones.jsx b/src/pages/Salones/GestionarSalones.jsx index 55c6a9b..6720fc4 100644 --- a/src/pages/Salones/GestionarSalones.jsx +++ b/src/pages/Salones/GestionarSalones.jsx @@ -13,19 +13,19 @@ function GestionarSalones({ user }) { const [searchTerm, setSearchTerm] = useState(""); const [popup, setPopup] = useState({tipo: ""}); const [newSalon, setNewSalon] = useState({ - mnemonic: "", - name: "", - description: "", - location: "", - capacity: 0, - resources: [] + mnemonico: "", + nombre: "", + descripcion: "", + ubicacion: "", + capacidad: 0, + recursos: [] }); const abrirPopup = (tipo, salon = null) => { if (tipo === "editar-salon" && salon) { setNewSalon(salon); } else if (tipo === "agregar-salon") { - setNewSalon({ mnemonic: "", name: "", description: "", location: "", capacity: 0, resources: [] }); + setNewSalon({ mnemonico: "", nombre: "", descripcion: "", ubicacion: "", capacidad: 0, recursos: [] }); } setPopup({ tipo }); }; @@ -89,16 +89,22 @@ function GestionarSalones({ user }) { }; // edita un nuevo salón - const handleEdit = async () => { - if (newSalon.nombre.trim() && newSalon.descripcion.trim()) { + const handleEdit = async (salon) => { + if (salon.nombre.trim() && salon.descripcion.trim()) { try { - await actualizarSalon(newSalon.mnemonico, newSalon); - - const salonActualizado = await getSalonByMnemonico(newSalon.mnemonico); + const formattedSalon = { + mnemonic: salon.mnemonico, + name: salon.nombre, + description: salon.descripcion, + location: salon.ubicacion, + capacity: salon.capacidad, + resources: salon.recursos || [], + }; + await actualizarSalon(salon.mnemonico, formattedSalon); + const salonActualizado = await getSalonByMnemonico(salon.mnemonico); if (salonActualizado) { - setSalones((prevSalones) => prevSalones.map(s => s.mnemonico === newSalon.mnemonico ? salonActualizado : s)); + setSalones((prevSalones) => prevSalones.map(s => s.mnemonico === salon.mnemonico ? salonActualizado : s)); } - cerrarPopup(); } catch (error) { console.error("Error al editar el salón", error);