From f50e35d6202bbd6e45426c31cafe3356f1b87d3c Mon Sep 17 00:00:00 2001 From: Elliot Axel Noriega Date: Tue, 1 Oct 2024 18:45:35 -0600 Subject: [PATCH 1/2] Creacion de funcionalidad de Modificar Perfil --- cosiap_api/cosiap_api/.env | 4 +- cosiap_frontend/src/App.jsx | 3 +- cosiap_frontend/src/api.js | 9 +- .../src/components/FormsValidations.jsx | 47 +++ .../src/components/common/base/FormInput.jsx | 5 +- .../src/components/common/base/FormSelect.jsx | 41 +++ .../src/components/common/ui/Alert.jsx | 47 +++ .../common/ui/Modals/ModalConfirmation.jsx | 29 ++ .../ui/SectionContainers/SectionContainer.jsx | 21 ++ .../common/utility/LoginRequiredRoute.jsx | 4 +- .../src/components/users/Perfil/Direccion.jsx | 309 ++++++++++++++++++ .../Perfil/InformacionIdentificacion.jsx | 216 ++++++++++++ .../users/Perfil/InformacionPersonal.jsx | 235 +++++++++++++ .../users/Perfil/Label_InputFile.jsx | 80 +++++ .../users/Perfil/Label_InputForm.jsx | 39 +++ .../users/Perfil/Label_SelectForm.jsx | 38 +++ .../src/components/users/Perfil/Perfil.jsx | 81 +++++ 17 files changed, 1200 insertions(+), 8 deletions(-) create mode 100644 cosiap_frontend/src/components/common/base/FormSelect.jsx create mode 100644 cosiap_frontend/src/components/common/ui/Alert.jsx create mode 100644 cosiap_frontend/src/components/common/ui/Modals/ModalConfirmation.jsx create mode 100644 cosiap_frontend/src/components/common/ui/SectionContainers/SectionContainer.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/Direccion.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/InformacionIdentificacion.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/InformacionPersonal.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/Label_InputFile.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/Label_InputForm.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/Label_SelectForm.jsx create mode 100644 cosiap_frontend/src/components/users/Perfil/Perfil.jsx diff --git a/cosiap_api/cosiap_api/.env b/cosiap_api/cosiap_api/.env index 1b8c473..1b59aba 100644 --- a/cosiap_api/cosiap_api/.env +++ b/cosiap_api/cosiap_api/.env @@ -14,8 +14,8 @@ DATABASES_DEFAULT_PORT="3306" EMAIL_HOST="sandbox.smtp.mailtrap.io" EMAIL_FROM="cosiap@example.com" -EMAIL_HOST_USER="3b48193365f615" -EMAIL_HOST_PASSWORD="37f89fc1d98f48" +EMAIL_HOST_USER="52e12fdcb768cc" +EMAIL_HOST_PASSWORD="03520623807dea" EMAIL_PORT="2525" EMAIL_USE_TLS=True diff --git a/cosiap_frontend/src/App.jsx b/cosiap_frontend/src/App.jsx index aa35a90..a5d5132 100644 --- a/cosiap_frontend/src/App.jsx +++ b/cosiap_frontend/src/App.jsx @@ -27,6 +27,7 @@ import Modalidades from "@/components/modalidades/Modalidades"; import CreateModalidad from "./components/modalidades/CrearModalidad"; import EditModalidad from "./components/modalidades/EditarModalidad"; import SolicitarModalidad from "./components/modalidades/Modalidad"; +import Perfil from '@/components/users/Perfil/Perfil'; function App() { const [viewPageLoader, setViewPageLoader] = useState(false); @@ -100,7 +101,7 @@ function RoutesApp({ setViewPageLoader }) { } /> } /> } /> - } /> + } /> {/* Solo administradores pueden acceder a estas url */} ax.get(`api/usuarios/solicitantes/${id}`), update: (id, data) => ax.put(`api/usuarios/solicitantes/${id}`, data), }, + + municipios: { + get: () => ax.get('api/usuarios/municipios'), + }, + estados: { + get: () => ax.get('api/usuarios/estados'), + } }, administracion: { //Aun por declarar diff --git a/cosiap_frontend/src/components/FormsValidations.jsx b/cosiap_frontend/src/components/FormsValidations.jsx index 626e4d9..a9ec178 100644 --- a/cosiap_frontend/src/components/FormsValidations.jsx +++ b/cosiap_frontend/src/components/FormsValidations.jsx @@ -3,12 +3,19 @@ import * as Yup from "yup"; // Variable de formato para validación de formato de CURP const CURP_REGEX = /^[A-Z]{1}[AEIOU]{1}[A-Z]{2}[0-9]{2}(0[1-9]|1[0-2])(0[1-9]|1[0-9]|2[0-9]|3[0-1])[HM]{1}(AS|BC|BS|CC|CH|CL|CM|DF|DG|GT|GR|HG|JC|MC|MN|MS|NT|NL|OC|PL|QT|QR|SP|SL|SR|TC|TS|TL|VZ|YN|ZS|NE)[B-DF-HJ-NP-TV-Z]{3}[0-9A-Z]{1}[0-9]{1}$/; +//Variable de formato para validación de campo de RFC +const RFC_REGEX = /^([A-ZÑ&]{3,4})?(?:-)?([0-9]{2})(?:-)?(0[1-9]|1[0-2])(?:-)?([0-2][0-9]|3[0-1])(?:-)?([A-Z\d]{2})([A\d])$/; // Validación de un campo CURP const CURP_VALIDATION = Yup.string() .required("La CURP es requerida") .matches(CURP_REGEX, "Formato de CURP inválido"); +// Validacion de un campo RFC +const RFC_VALIDATION = Yup.string() + .required("El RFC es requerido") + .matches(RFC_REGEX, "Formato de RFC inválido"); + const PASSWORD_CREATION_VALIDATION = Yup.string() .required("La contraseña es requerida") .min(8, "La contraseña debe tener al menos 8 caracteres") @@ -70,3 +77,43 @@ export const FormValidationSchema = Yup.object({ archivo: PDF_FileValidation, monto: MontoValidation, }); + +export const PersonalInformationValidationSchema = Yup.object().shape({ + nombre: Yup.string() + .required('El nombre es requerido'), + ap_paterno: Yup.string() + .required('El apellido paterno es requerido'), + ap_materno: Yup.string(), + sexo: Yup.string() + .required('El sexo es requerido'), + telefono: Yup.string() + .required('El telefono es requerido') + .matches(/^[0-9]{10}$/, 'El número debe tener 10 dígitos'), + email: Yup.string() + .required("El correo electronico es requerido") + .email("El correo electronico no es válido") +}); + +export const DirectionInformationValidationSchema = Yup.object().shape({ + direccion : Yup.string().required('La dirección es requerida'), + estado : Yup.string().required('El estado es requerido'), + municipio : Yup.string().required('El municipio es requerido'), + codigo_postal : Yup.string() + .required('El código postal es requerido') + .matches(/^[0-9]{5}$/, 'El código postal debe tener 5 dígitos'), + poblacion : Yup.string().required("La población es requerida") +}); + + +export const IdentificationValidationSchema = Yup.object().shape({ + RFC: RFC_VALIDATION, + curp: CURP_VALIDATION, + INE: Yup.mixed() + .test("required", "El archivo INE es requerido", (value) => value && value.length > 0) // Validar si el archivo está vacío + .test("fileFormat", "Formato de archivo no soportado", (value) => { + return value && value[0] && ["application/pdf"].includes(value[0].type); // Asegúrate de validar solo si hay un archivo + }) + .test("fileSize", "El archivo supera el tamaño máximo", (value) => { + return value && value[0] && value[0].size <= FILE_SIZE; + }) +}); \ No newline at end of file diff --git a/cosiap_frontend/src/components/common/base/FormInput.jsx b/cosiap_frontend/src/components/common/base/FormInput.jsx index d0ad401..51feab6 100644 --- a/cosiap_frontend/src/components/common/base/FormInput.jsx +++ b/cosiap_frontend/src/components/common/base/FormInput.jsx @@ -1,7 +1,7 @@ import PropTypes from 'prop-types'; import {ErrorDisplay} from '@/components/common/ui/ErrorDisplay' -export function FormInput({ id, name, type, placeholder, className, register, errors, onChange }) { +export function FormInput({ id, name, type, placeholder, className, register, errors, onChange, isDisabled }) { return ( <> @@ -25,6 +27,7 @@ FormInput.propTypes = { placeholder: PropTypes.string, className: PropTypes.string, onChange: PropTypes.func, + isDisabled: PropTypes.bool, register: PropTypes.func.isRequired, errors: PropTypes.oneOfType([ PropTypes.object, diff --git a/cosiap_frontend/src/components/common/base/FormSelect.jsx b/cosiap_frontend/src/components/common/base/FormSelect.jsx new file mode 100644 index 0000000..983b1c1 --- /dev/null +++ b/cosiap_frontend/src/components/common/base/FormSelect.jsx @@ -0,0 +1,41 @@ +import PropTypes from 'prop-types'; +import { ErrorDisplay } from '@/components/common/ui/ErrorDisplay'; + +export function FormSelect({ id, name, options, className, register, errors, onChange, isDisabled, value}) { + return ( + <> + + + + ); +} + +FormSelect.propTypes = { + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, + className: PropTypes.string, + onChange: PropTypes.func, + isDisabled: PropTypes.bool, + register: PropTypes.func.isRequired, + errors: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]), +}; \ No newline at end of file diff --git a/cosiap_frontend/src/components/common/ui/Alert.jsx b/cosiap_frontend/src/components/common/ui/Alert.jsx new file mode 100644 index 0000000..3ad5775 --- /dev/null +++ b/cosiap_frontend/src/components/common/ui/Alert.jsx @@ -0,0 +1,47 @@ +import { useState, useEffect } from "react"; + +export default function Alert({ message, type, duration, isVisible, setIsVisible }) { + useEffect(() => { + if (isVisible) { + const timer = setTimeout(() => { + setIsVisible(false); // Oculta la alerta después de la duración + }, duration); + + return () => clearTimeout(timer); // Limpia el temporizador cuando el componente se desmonta + } + }, [isVisible, duration, setIsVisible]); + + return ( +
+
+
+ + {type === 'success' ? 'task_alt': type === 'error' ? 'error': 'warning' } + +
+
+ + {message} + +
+
+ setIsVisible(false)} // Cerrar la alerta al hacer clic + className="material-symbols-outlined cursor-pointer font-bold" + > + close + +
+
+
+ ); +} diff --git a/cosiap_frontend/src/components/common/ui/Modals/ModalConfirmation.jsx b/cosiap_frontend/src/components/common/ui/Modals/ModalConfirmation.jsx new file mode 100644 index 0000000..efcca36 --- /dev/null +++ b/cosiap_frontend/src/components/common/ui/Modals/ModalConfirmation.jsx @@ -0,0 +1,29 @@ + + + +export default function ModalConfirmation( {nameIcon, title, description, children} ){ + return ( +
+
+
+
+ + {nameIcon} + +
+
+

+ {title} +

+

+ {description} +

+
+
+
+ {children} +
+
+
+ ); +} \ No newline at end of file diff --git a/cosiap_frontend/src/components/common/ui/SectionContainers/SectionContainer.jsx b/cosiap_frontend/src/components/common/ui/SectionContainers/SectionContainer.jsx new file mode 100644 index 0000000..9b23337 --- /dev/null +++ b/cosiap_frontend/src/components/common/ui/SectionContainers/SectionContainer.jsx @@ -0,0 +1,21 @@ +export default function SectionContainer( { title, children } ){ + return ( +
+
+
+

{title}

+
+
+
+
+
+
+
+
+ {children} +
+
+
+
+ ); +} \ No newline at end of file diff --git a/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx b/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx index b82f3dc..0a61304 100644 --- a/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx +++ b/cosiap_frontend/src/components/common/utility/LoginRequiredRoute.jsx @@ -1,12 +1,10 @@ import { useAutenticacion } from "@/components/common/utility/Autenticador"; import { useState, useEffect } from "react"; -import { Navigate, Outlet, useLocation } from "react-router-dom"; +import { Navigate, Outlet } from "react-router-dom"; import api from "@/api"; export const LoginRequiredRoute = () => { - const location = useLocation(); const { token } = useAutenticacion(); - console.log(location) if (token === null) { return ; } else if (token === undefined) { diff --git a/cosiap_frontend/src/components/users/Perfil/Direccion.jsx b/cosiap_frontend/src/components/users/Perfil/Direccion.jsx new file mode 100644 index 0000000..3706bc9 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/Direccion.jsx @@ -0,0 +1,309 @@ +import SectionContainer from "@/components/common/ui/SectionContainers/SectionContainer"; +import Label_InputForm from "@/components/users/Perfil/Label_InputForm"; +import Label_SelectForm from "./Label_SelectForm"; +import ModalConfirmation from "@/components/common/ui/Modals/ModalConfirmation"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useEffect, useState } from "react"; +import { DirectionInformationValidationSchema } from "@/components/FormsValidations"; +import api from "@/api"; + + +export default function Direccion({ datosSolicitante, setViewPageLoader, setShowAlertSuccesful }){ + // Estados + const [isEditing, setIsEditing] = useState(false); // Estado para controlar si está en modo edición + const [modifiedFields, setModifiedFields] = useState({}); // Campos que han sido modificados + const [showModalConfirmation, setShowModalConfirmation] = useState(false); // Control para mostrar el modal de confirmación + const [listaEstados, setListaEstados] = useState([]); + const [listaMunicipios, setListaMunicipios] = useState([]); + const [estadoSeleccionado, setEstadoSeleccionado] = useState(); + const [municipioSeleccionado, setMunicipioSeleccionado] = useState(); + // Hook para manejar el formulario + const { + register, // Registra los inputs en el formulario + handleSubmit, // Maneja el envío del formulario + reset, // Método para resetear los valores del formulario + formState: { errors, isSubmitting } // Estado del formulario: errores y si se está enviando + } = useForm({ + resolver: yupResolver(DirectionInformationValidationSchema), // Resolver para la validación con Yup + defaultValues: { + direccion: "", + estado: "", + municipio: "", + codigo_postal: "", + poblacion: "" + } + }); + + // Función para resetear los datos del formulario con los valores actuales del solicitante + const resetData = () => reset({ + direccion: datosSolicitante.direccion || "", + estado: estadoSeleccionado || "", + municipio: municipioSeleccionado || "", + codigo_postal: datosSolicitante.codigo_postal || "", + poblacion: datosSolicitante.poblacion || "" + }); + + const obtenerMunicipios = async () => { + setViewPageLoader(true) + try { + const response = await api.usuarios.municipios.get(); + setListaMunicipios(response.data); + } catch (error) { + console.error(error) + }finally{ + setViewPageLoader(false) + } + }; + + const obtenerEstados = async () => { + setViewPageLoader(true) + try { + const response = await api.usuarios.estados.get(); + setListaEstados(response.data); + } catch (error) { + console.error(error) + }finally{ + setViewPageLoader(false) + } + }; + + useEffect(() => { + obtenerEstados(); + obtenerMunicipios(); + }, []); + + // useEffect que se ejecuta cada vez que los datos del solicitante cambian + useEffect(() => { + resetData(); // Resetea los datos cuando cambia datosSolicitante + if (datosSolicitante.municipio){ + setEstadoSeleccionado(datosSolicitante.municipio.estado.id); + setMunicipioSeleccionado(datosSolicitante.municipio.id); + } + }, [datosSolicitante]); + + // Función que maneja el envío del formulario + const handleFormSubmission = async (data) => { + if (!isEditing) { + return; // Si no está en modo edición, no se envía el formulario + } + + //Eliminamos el estado en el data + delete data.estado; + + //Convertimos a entero el id del municipio + data.municipio = parseInt(data.municipio); + + setViewPageLoader(true); // Muestra un loader mientras se realiza la petición + try { + // Actualizar los datos del solicitante + const response = await api.usuarios.solicitantes.update(datosSolicitante.id, data); + // Cerrar modal de confirmación y deshabilitar la edición + setShowModalConfirmation(false); + setIsEditing(false); + setModifiedFields({}); // Vaciar los campos modificados + setShowAlertSuccesful(true) + } catch (error) { + console.error(error); // Manejar errores + } finally { + setViewPageLoader(false); // Quitar el loader al finalizar + } + }; + + // Función para manejar los cambios en los inputs + const handleInputChange = (e) => { + const { name, value } = e.target; + // Actualizar el estado de campos modificados + setModifiedFields((InputsPrev) => ({ + ...InputsPrev, + [name]: value !== datosSolicitante[name] // Solo marcar como modificado si el valor es diferente al original + })); + // Si el usuario selecciona un estado, guardamos el valor del estado seleccionado + if (name === "estado") { + setEstadoSeleccionado(value); // Guardar el estado seleccionado + setMunicipioSeleccionado(undefined) + } + // Si el usuario selecciona un municipio, guardamos el valor del municipio seleccionado + if (name === "municipio") { + setMunicipioSeleccionado(value); // Guardar el estado seleccionado + } + }; + // Función para cancelar la edición + const handleCancelEdition = () => { + setIsEditing(false); // Deshabilitar el modo edición + resetData(); // Restablecer los datos originales + setModifiedFields({}); // Limpiar los campos modificados + }; + + // Función para manejar el clic de "Guardar" con validación + const handleSaveClick = (data) => { + if (Object.values(modifiedFields).some((isModified) => isModified)) { + setShowModalConfirmation(true); // Mostrar el modal de confirmación si no hay errores + } + }; + + return ( + <> + +
+ + { + showModalConfirmation && ( + + + + + + ) + } + +
+
+ +
+
+ ({ + value: estado.id, + label: estado.nombre + })) + ]} + value={estadoSeleccionado ? estadoSeleccionado : ""} + register={register} + isDisabled={!isEditing} + errors={ + errors.estado ? errors.estado.message : undefined + } + onChange={handleInputChange} + /> +
+
+ municipio.estado === parseInt(estadoSeleccionado)) + .map((municipio) => ({ + value: municipio.id, + label: municipio.nombre + })) + : []) // Si no hay estado seleccionado, devolver solo la opción "Seleccione" + ]} + value={municipioSeleccionado ? municipioSeleccionado : ""} + register={register} + isDisabled={!isEditing} + errors={ + errors.municipio ? errors.municipio.message : undefined + } + onChange={handleInputChange} + /> +
+ +
+ +
+
+ +
+ +
+
+ { + isEditing && ( +
+ +
+ ) + } +
+ {/* Botón para habilitar edición o guardar */} + +
+
+
+
+ + ); +} \ No newline at end of file diff --git a/cosiap_frontend/src/components/users/Perfil/InformacionIdentificacion.jsx b/cosiap_frontend/src/components/users/Perfil/InformacionIdentificacion.jsx new file mode 100644 index 0000000..4f7cb78 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/InformacionIdentificacion.jsx @@ -0,0 +1,216 @@ +import SectionContainer from "@/components/common/ui/SectionContainers/SectionContainer"; +import Label_InputForm from "@/components/users/Perfil/Label_InputForm"; +import ModalConfirmation from "@/components/common/ui/Modals/ModalConfirmation"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useEffect, useState } from "react"; +import { IdentificationValidationSchema } from "@/components/FormsValidations"; +import api, { apiUrl } from "@/api"; +import Label_InputFile from "./Label_InputFile"; + +export default function InformacionIdentificacion( {datosSolicitante, setViewPageLoader, setShowAlertSuccesful} ){ + // Estados + const [isEditing, setIsEditing] = useState(false); // Estado para controlar si está en modo edición + const [modifiedFields, setModifiedFields] = useState({}); // Campos que han sido modificados + const [showModalConfirmation, setShowModalConfirmation] = useState(false); // Control para mostrar el modal de confirmación + const [urlFileIne, setUrlFileIne] = useState(null); + // Hook para manejar formularios + const { + register, // Registra los inputs en el formulario + handleSubmit, // Maneja el envío del formulario + reset, // Método para resetear los valores del formulario + formState: { errors, isSubmitting } // Estado del formulario: errores y si se está enviando + } = useForm({ + resolver: yupResolver(IdentificationValidationSchema), // Resolver para la validación con Yup + defaultValues: { + RFC: "", + curp: "", + INE: "", + } + }); + + // Función para resetear los datos del formulario con los valores actuales del solicitante + const resetData = () => reset({ + RFC: datosSolicitante.RFC || "", + curp: datosSolicitante.curp || "", + INE: "", + }); + + // useEffect que se ejecuta cada vez que los datos del solicitante cambian + useEffect(() => { + resetData(); // Resetea los datos cuando cambia datosSolicitante + if(datosSolicitante.INE){//Si el usuario ya tiene un archivo ine subido + setUrlFileIne(apiUrl + datosSolicitante.INE);//Declaramos la ruta con el archivo subido + } + }, [datosSolicitante, reset]); + + // Función que maneja el envío del formulario + const handleFormSubmission = async (data) => { + if (!isEditing) { + return; // Si no está en modo edición, no se envía el formulario + } + + setViewPageLoader(true); // Muestra un loader mientras se realiza la petición + + try { + // Crear una instancia de FormData + const formData = new FormData(); + + // Agregar los datos del formulario + formData.append("RFC", data.RFC); + formData.append("curp", data.curp); + + // Si hay un archivo INE, agregarlo + if (data.INE && data.INE.length > 0) { + formData.append("INE", data.INE[0]); // Archivo INE + } + + // Realizar la petición con el FormData + const response = await api.usuarios.solicitantes.update(datosSolicitante.id, formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + console.log(response) + // Cerrar modal de confirmación y deshabilitar la edición + setShowModalConfirmation(false); + setIsEditing(false); + setModifiedFields({}); // Vaciar los campos modificados + if(datosSolicitante.INE){ + setUrlFileIne(apiUrl + response.data.solicitante.INE);//Declaramos la ruta con el archivo subido + } + setShowAlertSuccesful(true); + } catch (error) { + console.error(error); // Manejar errores + } finally { + setViewPageLoader(false); // Quitar el loader al finalizar + } + }; + + // Función para manejar los cambios en los inputs + const handleInputChange = (e) => { + const { name, value } = e.target; + // Actualizar el estado de campos modificados + setModifiedFields((InputsPrev) => ({ + ...InputsPrev, + [name]: value !== datosSolicitante[name] // Solo marcar como modificado si el valor es diferente al original + })); + }; + + // Función para cancelar la edición + const handleCancelEdition = () => { + setIsEditing(false); // Deshabilitar el modo edición + resetData(); // Restablecer los datos originales + setModifiedFields({}); // Limpiar los campos modificados + if(datosSolicitante.INE){//Si el usuario ya tiene un archivo ine subido + setUrlFileIne(apiUrl + datosSolicitante.INE);//Declaramos la ruta con el archivo subido + } + }; + + // Función para manejar el clic de "Guardar" con validación + const handleSaveClick = (data) => { + if (Object.values(modifiedFields).some((isModified) => isModified)) { + setShowModalConfirmation(true); // Mostrar el modal de confirmación si no hay errores + } + }; + + return ( + +
+
+
+ +
+
+ +
+
+ +
+ +
+ +
+ {isEditing && ( +
+ +
+ )} +
+ {/* Botón para habilitar edición o guardar */} + +
+
+ + {showModalConfirmation && ( + + + + + )} +
+
+ ); +} \ No newline at end of file diff --git a/cosiap_frontend/src/components/users/Perfil/InformacionPersonal.jsx b/cosiap_frontend/src/components/users/Perfil/InformacionPersonal.jsx new file mode 100644 index 0000000..5277840 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/InformacionPersonal.jsx @@ -0,0 +1,235 @@ +import SectionContainer from "@/components/common/ui/SectionContainers/SectionContainer"; +import Label_InputForm from "@/components/users/Perfil/Label_InputForm"; +import Label_SelectForm from "./Label_SelectForm"; +import ModalConfirmation from "@/components/common/ui/Modals/ModalConfirmation"; +import { useForm } from "react-hook-form"; +import { yupResolver } from "@hookform/resolvers/yup"; +import { useEffect, useState } from "react"; +import { PersonalInformationValidationSchema } from "@/components/FormsValidations"; +import api from "@/api"; + +export default function InformacionPersonal({ datosSolicitante, setViewPageLoader, setShowAlertSuccesful }) { + // Estados + const [isEditing, setIsEditing] = useState(false); // Estado para controlar si está en modo edición + const [modifiedFields, setModifiedFields] = useState({}); // Campos que han sido modificados + const [showModalConfirmation, setShowModalConfirmation] = useState(false); // Control para mostrar el modal de confirmación + // Hook para manejar formularios + const { + register, // Registra los inputs en el formulario + handleSubmit, // Maneja el envío del formulario + reset, // Método para resetear los valores del formulario + formState: { errors, isSubmitting } // Estado del formulario: errores y si se está enviando + } = useForm({ + resolver: yupResolver(PersonalInformationValidationSchema), // Resolver para la validación con Yup + defaultValues: { + nombre: "", + ap_paterno: "", + ap_materno: "", + sexo: "", + telefono: "", + email: "" + } + }); + + // Función para resetear los datos del formulario con los valores actuales del solicitante + const resetData = () => reset({ + nombre: datosSolicitante.nombre || "", + ap_paterno: datosSolicitante.ap_paterno || "", + ap_materno: datosSolicitante.ap_materno || "", + sexo: datosSolicitante.sexo || "", + telefono: datosSolicitante.telefono || "", + email: datosSolicitante.email || "" + }); + + // useEffect que se ejecuta cada vez que los datos del solicitante cambian + useEffect(() => { + resetData(); // Resetea los datos cuando cambia datosSolicitante + }, [datosSolicitante, reset]); + + // Función que maneja el envío del formulario + const handleFormSubmission = async (data) => { + if (!isEditing) { + return; // Si no está en modo edición, no se envía el formulario + } + + setViewPageLoader(true); // Muestra un loader mientras se realiza la petición + try { + // Actualizar los datos del solicitante + const response = await api.usuarios.solicitantes.update(datosSolicitante.id, data); + console.log(response); + + // Cerrar modal de confirmación y deshabilitar la edición + setShowModalConfirmation(false); + setIsEditing(false); + setModifiedFields({}); // Vaciar los campos modificados + setShowAlertSuccesful(true) + } catch (error) { + console.error(error); // Manejar errores + } finally { + setViewPageLoader(false); // Quitar el loader al finalizar + } + }; + + // Función para manejar los cambios en los inputs + const handleInputChange = (e) => { + const { name, value } = e.target; + + // Actualizar el estado de campos modificados + setModifiedFields((InputsPrev) => ({ + ...InputsPrev, + [name]: value !== datosSolicitante[name] // Solo marcar como modificado si el valor es diferente al original + })); + }; + + // Función para cancelar la edición + const handleCancelEdition = () => { + setIsEditing(false); // Deshabilitar el modo edición + resetData(); // Restablecer los datos originales + setModifiedFields({}); // Limpiar los campos modificados + }; + + // Función para manejar el clic de "Guardar" con validación + const handleSaveClick = (data) => { + if (Object.values(modifiedFields).some((isModified) => isModified)) { + setShowModalConfirmation(true); // Mostrar el modal de confirmación si no hay errores + } + }; + + return ( + +
+
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+
+ +
+ {isEditing && ( +
+ +
+ )} +
+ {/* Botón para habilitar edición o guardar */} + +
+
+ + {showModalConfirmation && ( + + + + + )} +
+
+ ); +} diff --git a/cosiap_frontend/src/components/users/Perfil/Label_InputFile.jsx b/cosiap_frontend/src/components/users/Perfil/Label_InputFile.jsx new file mode 100644 index 0000000..3b40423 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/Label_InputFile.jsx @@ -0,0 +1,80 @@ +import PropTypes from 'prop-types'; +import { useState, useEffect } from 'react'; // Importamos useRef para manejar el campo de archivo +import { FormInput } from "@/components/common/base/FormInput"; + +export default function Label_InputFile({urlFile, label, id, name, type, placeholder, className, register, errors, onChange, isDisabled, message }) { + const [selectedFile, setSelectedFile] = useState(null); // Estado para almacenar el archivo seleccionado + console.log(urlFile) + const handleFileChange = (e) => { + const file = e.target.files[0]; // Capturamos el archivo seleccionado + // Si no hay archivo seleccionado, restablecemos el estado + if (file.type === 'application/pdf') { + // Verificamos que el archivo sea de tipo PDF + setSelectedFile(file); // Almacenamos el archivo si es PDF + + } else { + setSelectedFile(null); // Si no es PDF, lo descartamos + } + + if (onChange) { + onChange(e); // Llamamos al onChange pasado como parametro si está definido + } + }; + + useEffect(() => { + setSelectedFile(null) + }, [isDisabled]); //Cada vez que cambia la variable isDisabled + + return ( +
+ + +
{message}
+ + {/* Aquí mostramos el área de vista previa si hay un archivo seleccionado */} + {selectedFile && !isDisabled && ( //Cuando hay un archivo seleccionado y esta habilitado el campo +
+ +
+ )} + + {urlFile && ( + + )} +
+ ); +} + +Label_InputFile.propTypes = { + label: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + placeholder: PropTypes.string, + className: PropTypes.string, + onChange: PropTypes.func, + isDisabled: PropTypes.bool, + register: PropTypes.func.isRequired, + message: PropTypes.string, + errors: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array, + PropTypes.string + ]) +}; diff --git a/cosiap_frontend/src/components/users/Perfil/Label_InputForm.jsx b/cosiap_frontend/src/components/users/Perfil/Label_InputForm.jsx new file mode 100644 index 0000000..c39fa30 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/Label_InputForm.jsx @@ -0,0 +1,39 @@ +import PropTypes from 'prop-types'; +import { FormInput } from "@/components/common/base/FormInput"; + +export default function Label_InputForm( {label, id, name, type, placeholder, className, register, errors, onChange, isDisabled} ) { + return ( +
+ + +
+ ); +} + +Label_InputForm.PropTypes = { + label: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + type: PropTypes.string.isRequired, + placeholder: PropTypes.string, + className: PropTypes.string, + onChange: PropTypes.func, + isDisabled: PropTypes.bool, + register: PropTypes.func.isRequired, + errors: PropTypes.oneOfType([ + PropTypes.object, + PropTypes.array, + PropTypes.string + ]) +} + diff --git a/cosiap_frontend/src/components/users/Perfil/Label_SelectForm.jsx b/cosiap_frontend/src/components/users/Perfil/Label_SelectForm.jsx new file mode 100644 index 0000000..0437756 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/Label_SelectForm.jsx @@ -0,0 +1,38 @@ +import PropTypes from 'prop-types'; +import { FormSelect } from "@/components/common/base/FormSelect"; + +export default function Label_SelectForm({ label, id, name, options, className, register, errors, onChange, isDisabled, value }) { + return ( +
+ + +
+ ); +} + +Label_SelectForm.propTypes = { + label: PropTypes.string.isRequired, + id: PropTypes.string.isRequired, + name: PropTypes.string.isRequired, + options: PropTypes.arrayOf( + PropTypes.shape({ + value: PropTypes.string.isRequired, + label: PropTypes.string.isRequired, + }) + ).isRequired, + className: PropTypes.string, + onChange: PropTypes.func, + isDisabled: PropTypes.bool, + register: PropTypes.func.isRequired, + errors: PropTypes.oneOfType([PropTypes.object, PropTypes.array, PropTypes.string]), +}; diff --git a/cosiap_frontend/src/components/users/Perfil/Perfil.jsx b/cosiap_frontend/src/components/users/Perfil/Perfil.jsx new file mode 100644 index 0000000..4b80460 --- /dev/null +++ b/cosiap_frontend/src/components/users/Perfil/Perfil.jsx @@ -0,0 +1,81 @@ +import InformacionPersonal from "@/components/users/Perfil/InformacionPersonal"; +import Direccion from "@/components/users/Perfil/Direccion"; +import InformacionIdentificacion from "@/components/users/Perfil/InformacionIdentificacion"; +import Alert from "@/components/common/ui/Alert"; +import api from "@/api"; +import { useAutenticacion } from "@/components/common/utility/Autenticador"; +import { useEffect, useState } from "react"; + +export default function Perfil( {setViewPageLoader} ){ + const { uid } = useAutenticacion(); + const [datosSolicitante, setDatosSolicitante] = useState([]); + const [showAlertSuccesful, setShowAlertSuccesful] = useState(false); // Control de mostrar la alerta + + const crearSolicitante = async () => { + try { + const response = await api.usuarios.solicitantes.post(); + console.log("Solicitante creado: ", response.data); + //Tratamos de obtener el solicitante con el id + const responseObtain = await api.usuarios.solicitantes.getById(uid); + //Declaramos los datos del solicitante + setDatosSolicitante(responseObtain.data); + } catch (error) { + console.log(error) + } + }; + + const obtenerInformacionSolicitante = async () =>{ + if (uid){ + setViewPageLoader(true); + try { + //Tratamos de obtener el solicitante con el id + const responseObtain = await api.usuarios.solicitantes.getById(uid); + //Declaramos los datos del solicitante + setDatosSolicitante(responseObtain.data); + console.log(responseObtain.data) + } catch (error) { // Si da error significa que es su primera vez ingresando + crearSolicitante() + }finally{ + setViewPageLoader(false); + } + } + }; + useEffect(() => { + obtenerInformacionSolicitante(); + },[uid]); + + return ( + <> + +
+
+
+
+ + + +
+
+
+
+ + ); +} -- GitLab From aadeb73a3584bba3d6a1ad27905912a55d97fb91 Mon Sep 17 00:00:00 2001 From: Elliot Axel Noriega Date: Tue, 1 Oct 2024 19:51:07 -0600 Subject: [PATCH 2/2] Ajuste de estilos, resolucion de movimiento de desplazamiento a parte superior de dash --- .../src/components/common/layouts/LayoutBaseNavigation.jsx | 4 ++-- .../common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx | 2 +- .../common/layouts/LayoutsNavigation/Navbar/Navbar.jsx | 4 ++-- .../layouts/LayoutsNavigation/Navbar/Notifications.jsx | 2 +- .../common/layouts/LayoutsNavigation/Settings.jsx | 2 +- .../common/layouts/LayoutsNavigation/Sidebar/MobileMenu.jsx | 2 +- .../layouts/LayoutsNavigation/Sidebar/Notifications.jsx | 2 +- .../common/layouts/LayoutsNavigation/Sidebar/Sidebar.jsx | 4 ++-- cosiap_frontend/src/components/users/Perfil/Direccion.jsx | 2 +- .../components/users/Perfil/InformacionIdentificacion.jsx | 6 +++--- 10 files changed, 15 insertions(+), 15 deletions(-) diff --git a/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx b/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx index d0e3cab..d24a7f6 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx @@ -144,7 +144,7 @@ export default function LayoutBaseNavigation() {
setViewSettings(!viewSettings)} > @@ -153,7 +153,7 @@ export default function LayoutBaseNavigation() { )} -
+
diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx index 78089a0..3fe95cf 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/MobileMenu.jsx @@ -12,7 +12,7 @@ export default function MobileMenu({ menuRef, linksItems }) { return ( <> {/* Contenedor principal del menú móvil */} -
+
{ // Si existe linksItems, mapea cada uno para generar enlaces diff --git a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx index 7350992..b1cbe2d 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutsNavigation/Navbar/Navbar.jsx @@ -50,10 +50,10 @@ export default function Navbar({ linksItems, viewMenu, setViewMenu }) { <>