diff --git a/cosiap_api/users/admin_views.py b/cosiap_api/users/admin_views.py index 2fcbbd5ed7853521ea03e2e46c6c26370a190ef8..a508d1eb7d7d660f9b81fd8c66af66ed22abadff 100644 --- a/cosiap_api/users/admin_views.py +++ b/cosiap_api/users/admin_views.py @@ -22,6 +22,7 @@ class AdminAPIView(BasePermissionAPIView): ''' permission_classes_list = [IsAuthenticated, es_admin] permission_classes_create = [IsAuthenticated, es_admin] + permission_classes_update = [IsAuthenticated, es_admin] def get(self, request, *args, **kwargs): @@ -56,4 +57,25 @@ class AdminAPIView(BasePermissionAPIView): return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) else: Mensaje.error(response_data, 'Este email ya esta en uso por otro usuario.') - return Response(response_data, status = status.HTTP_400_BAD_REQUEST) \ No newline at end of file + return Response(response_data, status = status.HTTP_400_BAD_REQUEST) + + def put(self, request, *args, **kwargs): + ''' + Método put para la actualziación de un admin + ''' + + data = {} + try: + id = kwargs['pk'] + admin = Usuario.objects.get(pk = id) + serializer = AdminSerializer(admin, request.data, partial = True) + if serializer.is_valid(): + serializer.save() + Mensaje.success(data, 'Administrador actualizado exitosamente.') + return Response(data, status=status.HTTP_200_OK) + else: + # Si la validación falla, devolvemos el error con el estado 400 + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + except Exception as e: + Mensaje.error(data, str(e)) + return Response(data, status = status.HTTP_400_BAD_REQUEST) diff --git a/cosiap_api/users/serializers.py b/cosiap_api/users/serializers.py index e71bed61253441ef51e28a9d5107bac3c28ed5ce..3b6f5c1413d6e016e1e79884b6b27a8a107b8bdd 100644 --- a/cosiap_api/users/serializers.py +++ b/cosiap_api/users/serializers.py @@ -7,37 +7,49 @@ from .models import Usuario, Solicitante, DatosBancarios, Municipio, Estado class AdminSerializer(serializers.ModelSerializer): - # campo para la confirmación del password - confirmar_password = serializers.CharField(write_only=True) + confirmar_password = serializers.CharField(write_only=True, required=False) class Meta: - # Indicamos que se usará el modelo de Usuario model = Usuario - # Indicamos que campos se le mostrarán en el formulario - fields = ['nombre', 'curp', 'email', 'password', 'confirmar_password'] - # Indicamos el password como write only (para que no sea legible) y el nombre sea nulleable - extra_kwargs = {'password': {'write_only': True}, 'nombre': {'required': False}} + fields = ['pk', 'nombre', 'curp', 'email', 'password', 'confirmar_password'] + extra_kwargs = { + 'password': {'write_only': True, 'required': False}, + 'nombre': {'required': False}, + } - # validamos que ambas contraseñas enviadas coincidan def validate(self, data): - # realizamos la comparación de las contraseñas - if data['password'] != data['confirmar_password']: - # Si las contraseñas no coinciden se muestra un error - raise serializers.ValidationError("Atención: Las contraseñas no coinciden.") + # Validamos sólo si ambos campos están presentes + password = data.get('password') + confirmar_password = data.get('confirmar_password') + + if password or confirmar_password: + if password != confirmar_password: + raise serializers.ValidationError("Atención: Las contraseñas no coinciden.") return data - # funcion para la creacion del admin def create(self, validated_data): - # creamos al administrador usando el metodo create_superuser() de la clase Usuario user = Usuario.objects.create_superuser( - email = validated_data['email'], - curp= validated_data['curp'], - nombre= validated_data['nombre'], + email=validated_data['email'], + curp=validated_data['curp'], + nombre=validated_data.get('nombre', ''), password=validated_data['password'] ) - # retornamos el usuario creado return user + def update(self, instance, validated_data): + instance.curp = validated_data.get('curp', instance.curp) + instance.email = validated_data.get('email', instance.email) + instance.nombre = validated_data.get('nombre', instance.nombre) + + # Verificamos si se envía el password para actualizarlo + password = validated_data.get('password', None) + if password: + instance.set_password(password) + + instance.save() + return instance + + # serializer para el usuario solicitante class UsuarioSerializer(serializers.ModelSerializer): # Creamos un campo de confirmación de la contraseña del solicitante diff --git a/cosiap_api/users/urls.py b/cosiap_api/users/urls.py index f2564a200e66c3493b7e3f3d0b841b0325895a67..a417ce94ef89b7df273b82de8edd052ec8c8e975 100644 --- a/cosiap_api/users/urls.py +++ b/cosiap_api/users/urls.py @@ -26,6 +26,7 @@ urlpatterns = [ path('verificar-correo///', views.VerificarCorreo.as_view(), name='verificar_correo'), path('restablecer-password/', views.ResetPassword.as_view(), name='reset_password'), path('nueva-password///', views.NuevaPassword.as_view(), name='nueva_password'), - path('administradores/', AdminAPIView.as_view() , name = 'administrador_list_create'), + path('administradores/', AdminAPIView.as_view() , name = 'administradores'), + path('administradores/', AdminAPIView.as_view() , name = 'administradores_pk'), path('descargar-archivo/', views.FileDownloadAPIView.as_view() , name = 'descargar_archivo'), ] \ No newline at end of file diff --git a/cosiap_frontend/src/App.css b/cosiap_frontend/src/App.css index 7daa58425813b3c4cb117621af6fad711de8e3b1..66ef1ca4fbb6f18949ac4c6015b91be05475c14d 100644 --- a/cosiap_frontend/src/App.css +++ b/cosiap_frontend/src/App.css @@ -1055,3 +1055,20 @@ th, td { .close-btn:hover { color: black; } + +.button-users { + background-color: brown; + color: white; + border: none; + padding: 12px 18px; + font-size: 14px; + border-radius: 16px; + cursor: pointer; + margin-top: 10px; + margin-right: 10px; + display: flex; + align-items: center; + justify-content: center; + gap: 8px; + transition: background-color 0.3s ease; +} \ No newline at end of file diff --git a/cosiap_frontend/src/App.jsx b/cosiap_frontend/src/App.jsx index 7a5f4bdb85bafe2f7e6f905a229090cee4a3f731..7e885586a4558ab52cd189df459b83ef7f0574f9 100644 --- a/cosiap_frontend/src/App.jsx +++ b/cosiap_frontend/src/App.jsx @@ -36,6 +36,8 @@ import RecepcionApoyo from "./components/users/RecepcionApoyo/RecepcionApoyo"; import Solicitudes from "./components/SolicitudesAdmin/Solicitudes"; import ListaUsuarios from "./components/admin/TablaUsuarios"; import ListaSolicitudesSolicitante from "./components/admin/HistorialAdmin"; +import ListaAdmins from "./components/admin/TablaAdministradores"; +import CrearAdmin from "./components/admin/CrearAdministrador"; function App() { const [viewPageLoader, setViewPageLoader] = useState(false); @@ -126,6 +128,8 @@ function RoutesApp({ setViewPageLoader }) { } /> } /> } /> + } /> + } /> diff --git a/cosiap_frontend/src/api.js b/cosiap_frontend/src/api.js index 0bc346dd1b731415d5f8ccfd3b2a9606a7431d46..819387c6d297ca60f482ce6efc7aded2ab877d48 100644 --- a/cosiap_frontend/src/api.js +++ b/cosiap_frontend/src/api.js @@ -67,8 +67,10 @@ const api = { }, administradores: { - get: () => ax.get('api/usuarios/administradores'), + get: (params) => ax.get('api/usuarios/administradores', params), + update: (id, data) => ax.put(`api/usuarios/administradores/${id}`, data), post: (data) => ax.post('api/usuarios/administradores/', data), + delete: (id) => ax.delete(`api/usuarios/administradores/${id}`), }, // Endpoints del submodulo token diff --git a/cosiap_frontend/src/components/admin/CrearAdministrador.jsx b/cosiap_frontend/src/components/admin/CrearAdministrador.jsx new file mode 100644 index 0000000000000000000000000000000000000000..38c2c230438186947dbe3d941313791e19da53c7 --- /dev/null +++ b/cosiap_frontend/src/components/admin/CrearAdministrador.jsx @@ -0,0 +1,119 @@ +import { useState } from "react"; +import api from '../../api'; +import MainContainer from "../common/utility/MainContainer"; +import '@/App.css'; +import { useNavigate } from "react-router-dom"; + +const CrearAdmin = () =>{ + const [nombre, setNombre] = useState(''); + const [curp, setCurp] = useState(''); + const [email, setEmail] = useState(''); + const [password, setPassword] = useState(''); + const [passwordConfirm, setPasswordConfirm] = useState(''); + const [alertMessage, setAlertMessage] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + const navigate = useNavigate(); + + const showAlert = (message, isSuccess) => { + setAlertMessage(message); + setIsSuccess(isSuccess); + + setTimeout(() => { + setAlertMessage(''); + }, 3000); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + if (password !== passwordConfirm) { + showAlert("Las contraseñas no coinciden.", false); + return; + } + + try { + const data = { + nombre: nombre, + curp: curp, + email: email, + password: password, + confirmar_password: passwordConfirm, + }; + await api.usuarios.administradores.post(data) + showAlert('Cuenta de administrador creada correctamente.', true) + navigate('/administradores'); + } catch (error) { + const errorData = error.response.data; + for (const key in errorData) { + if (errorData.hasOwnProperty(key)) { + showAlert(errorData[key], false); + } + } + } + }; + + return ( + + {alertMessage && ( +
+ {alertMessage} +
+ )} +
+
+
+ setNombre(e.target.value)} + required + /> + setCurp(e.target.value)} + required + /> + setEmail(e.target.value)} + required + /> +
+ +
+ setPassword(e.target.value)} + required + /> + setPasswordConfirm(e.target.value)} + required + /> +
+ +
+ + +
+
+
+
+ ); +}; + +export default CrearAdmin; \ No newline at end of file diff --git a/cosiap_frontend/src/components/admin/TablaAdministradores.jsx b/cosiap_frontend/src/components/admin/TablaAdministradores.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8e694e6593ab8bf0689fc6d318a0ff87450455d6 --- /dev/null +++ b/cosiap_frontend/src/components/admin/TablaAdministradores.jsx @@ -0,0 +1,240 @@ +import { useState, useEffect, useRef } from "react"; +import api from '../../api'; +import Tabla from "../common/utility/ReusableTable"; +import MainContainer from "../common/utility/MainContainer"; +import { useNavigate } from "react-router-dom"; +import '@/App.css'; +import UserIcon from "@/components/common/utility/UserIcon"; + +const ListaAdmins = () =>{ + const [admins, setAdmins] = useState([]); + const [editRow, setEditRow] = useState(null); + const [registerChange, setRegisterChange] = useState({}); + const inputRefs = useRef({}); + const tableContainerRef = useRef(null); + const [alertMessage, setAlertMessage] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + const [selectedRow, setSelectedRow] = useState(null); + const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + const [showCard, setShowCard] = useState(false); + const [curpActual, setCurpActual] = useState(''); + const [nombreActual, setNombreActual] = useState(''); + const [emailActual, setEmailActual] = useState(''); + const navigate = useNavigate(); + + + + useEffect(() => { + const fetchAdmins = async () => { + try { + const response = await api.usuarios.administradores.get(); + setAdmins(response.data); + } catch (error) { + console.error("Error al recuperar la lista de usuarios.", error); + setAdmins([]); + } + }; + fetchAdmins(); + }, []); + + const handleSingleClick = (fila) => { + setSelectedRow(fila.pk); + }; + + const handleCloseMenu = () => { + setSelectedRow(null); + setIsConfirmingDelete(false); + }; + + const handleEditRow = (id) => { + setEditRow(id); + handleCloseMenu(); + }; + + const handleDeleteConfirm = (id) => { + setIsConfirmingDelete(id); // Abre el menú de confirmación + }; + + const handleDelete = async (id) => { + try { + await api.usuarios.delete(id); + const response = await api.usuarios.administradores.get(); + setAdmins(response.data); + showAlert("Usuario eliminado con éxito", true); + } catch (error) { + console.error("Error al eliminar usuario", error); + showAlert("Error al eliminar el usuario", false); + } + handleCloseMenu(); + }; + + const handleCloseInfo = () => { + setShowCard(false) + setCurpActual(null) + setNombreActual(null) + setEmailActual(null) + }; + + const handleClickIcon = (curp, nombre, email) => { + setShowCard(true) + setCurpActual(curp) + setNombreActual(nombre) + setEmailActual(email) + } + + useEffect(() => { + const handleClickOutside = (event) => { + if (tableContainerRef.current?.contains(event.target)) { + return; + } + + const isOutside = !Object.values(inputRefs.current).some((input) => + input?.contains(event.target) + ); + + if (isOutside && editRow !== null) { + console.log("Variable editRow: ", editRow) + handleUpdate(editRow); + setEditRow(null); + setRegisterChange({}); + } + handleCloseMenu(); + }; + + document.addEventListener("mousedown", handleClickOutside); + return () => document.removeEventListener("mousedown", handleClickOutside); + }, [editRow, registerChange]); + + const handleChange = (e, rowId, key) => { + const value = e.target.value; + + setRegisterChange((prev) => ({ + ...prev, + [rowId]: { + ...prev[rowId], + [key]: value === '' ? '' : value // Permitir valores vacíos + }, + })); + }; + + const columnas = [ + { + label: "", + render: (fila) => , + }, + { label: "CURP", render: (fila) => renderCell(fila, "curp") }, + { label: "Nombre", render: (fila) => renderCell(fila, "nombre") }, + { label: "E-mail", render: (fila) => renderCell(fila, "email") }, + ]; + + const renderCell = (fila, key) => { + const isEditing = editRow === fila.pk; + return isEditing ? ( + handleChange(e, fila.pk, key)} + ref={(el) => (inputRefs.current[fila.pk + key] = el)} + style={{ + fontSize: "14px", + padding: "2px 4px", + borderRadius: "4px", + height: "1.6em", + lineHeight: "1", + border: "1px solid #d3d3d3", + display: "inline-block", + width: "auto", + maxWidth: "100px", + }} + autoFocus + /> + ) : ( + handleSingleClick(fila)} + onDoubleClick={() => handleEditRow(fila.pk)} + style={{ cursor: "pointer" }} + > + {fila[key]} + + ); + }; + + const showAlert = (message, isSuccess) => { + setAlertMessage(message); + setIsSuccess(isSuccess); + + setTimeout(() => { + setAlertMessage(''); + }, 3000); + }; + + const handleUpdate = async (id) => { + console.log(registerChange[id]); + try { + await api.usuarios.administradores.update(id, registerChange[id]) + const response = await api.usuarios.administradores.get(); + setAdmins(response.data); + } catch (error) { + const errorData = error.response.data; + for (const key in errorData) { + if (errorData.hasOwnProperty(key)) { + showAlert(errorData[key], false); + } + } + } + }; + + return ( + + {alertMessage && ( +
+ {alertMessage} +
+ )} +
+ + +
+
+ + {selectedRow && !isConfirmingDelete && ( +
+ +
+ )} + {isConfirmingDelete && ( +
+

¿Estás seguro de que deseas eliminar este usuario?

+ + +
+ )} + {/* Tarjeta emergente */} + {showCard && ( +
+ +

Información del Usuario

+

Nombre: {nombreActual}

+

CURP: {curpActual}

+

Email: {emailActual}

+
+ )} +
+
+ ); +}; + +export default ListaAdmins; \ No newline at end of file diff --git a/cosiap_frontend/src/components/admin/TablaUsuarios.jsx b/cosiap_frontend/src/components/admin/TablaUsuarios.jsx index 6ed5335c086b78851294bbfece142efe742f8da0..28746089c0dbf697c5b90ba8b33b5e0e8d1404c4 100644 --- a/cosiap_frontend/src/components/admin/TablaUsuarios.jsx +++ b/cosiap_frontend/src/components/admin/TablaUsuarios.jsx @@ -211,7 +211,16 @@ const ListaUsuarios = () => { {alertMessage} )} +
+ +
{selectedRow && !isConfirmingDelete && (