diff --git a/cosiap_api/archivo.zip b/cosiap_api/archivo.zip deleted file mode 100644 index a3155ad5cecbb102769971edbd7ba39aa20e6a35..0000000000000000000000000000000000000000 Binary files a/cosiap_api/archivo.zip and /dev/null differ diff --git a/cosiap_api/common/views.py b/cosiap_api/common/views.py index 6d51bb9831a78754c36d29e7de5eae133a7cfaf5..cad3adfaa31734b0685a06cf8ae15c40d3c5725e 100644 --- a/cosiap_api/common/views.py +++ b/cosiap_api/common/views.py @@ -2,7 +2,10 @@ from django.shortcuts import render from rest_framework.views import APIView from rest_framework.permissions import AllowAny, IsAuthenticated from users.permisos import es_admin -from django.http import HttpResponse, Http404 +import mimetypes +from django.http import FileResponse, JsonResponse +import os +from django.conf import settings class BasePermissionAPIView(APIView): """ @@ -41,14 +44,29 @@ class BasePermissionAPIView(APIView): def serve_file(file_url): """ Lógica para servir un archivo de forma segura. - `file_url` es la ruta en el servidor del archivo. """ + # Asegúrate de que la ruta base está bien configurada + prefix = os.path.join(settings.MEDIA_ROOT) # Cambia 'media/' a MEDIA_ROOT si es necesario + file_path = os.path.join(prefix, file_url) + + # Verifica si el archivo existe + if not os.path.isfile(file_path): + print(f"Archivo no encontrado: {file_path}") + raise Http404("Archivo no encontrado.") + + # Determina el tipo MIME del archivo + mime_type, _ = mimetypes.guess_type(file_path) + mime_type = mime_type or "application/octet-stream" - prefix = 'media/' try: - with open(prefix+file_url, 'rb') as file: - response = HttpResponse(file.read(), content_type="application/octet-stream") - response['Content-Disposition'] = f'attachment; filename={file_url.split("/")[-1]}' + print("Intentando abrir el archivo...") + with open(file_path, 'rb') as file: + response = FileResponse(file, content_type=mime_type) + filename = os.path.basename(file_url) + response['Content-Disposition'] = f'attachment; filename="{filename}"' + response['X-Sendfile'] = 'o' # Esta línea puede ayudar en algunos servidores (Apache, Nginx, etc.) + print(f"Archivo {filename} preparado para la descarga.") return response - except FileNotFoundError: - raise Http404("Archivo no encontrado.") \ No newline at end of file + except Exception as e: + print(f"Error al servir el archivo: {str(e)}") + raise Http404(f"Error al servir el archivo: {str(e)}") \ No newline at end of file diff --git a/cosiap_api/dynamic_formats/urls.py b/cosiap_api/dynamic_formats/urls.py index 5c7db51d5df5e8a157b57d32d99f297437d29f5b..3c9d4dfd9fcf74949d611a0e6b202ecfb498163c 100644 --- a/cosiap_api/dynamic_formats/urls.py +++ b/cosiap_api/dynamic_formats/urls.py @@ -5,6 +5,9 @@ app_name = 'dynamic_formats' urlpatterns = [ path('', views.FormatoAPIView.as_view(), name='formatos'), + path('convenio/', views.FormatoConvenio.as_view(), name='formato_convenio'), + path('minuta/', views.FormatoMinuta.as_view(), name='formato_minuta'), path('/', views.FormatoAPIView.as_view(), name='formatos_pk'), path('download//', views.DescargarFormatoView.as_view(), name='descargar_formato'), + path('descargar-formato/', views.descargar_formato, name='descargar_formato_admin'), ] \ No newline at end of file diff --git a/cosiap_api/dynamic_formats/views.py b/cosiap_api/dynamic_formats/views.py index c515eea9ae2eceecd43d65bf1f24781baa542b32..8d5b1c7db55d232138c7f4cfdd0b9618f65280f0 100644 --- a/cosiap_api/dynamic_formats/views.py +++ b/cosiap_api/dynamic_formats/views.py @@ -15,6 +15,94 @@ import re from notificaciones.mensajes import Mensaje import logging + +class FormatoConvenio(BasePermissionAPIView): + ''' + Clase para obtener y actualizar el formato default del convenio + ''' + + serializer_class = DynamicFormatSerializer + + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_update = [IsAuthenticated, es_admin] + + + def get(self, request, *args, **kwargs): + ''' Obtenemos la plantilla actual ''' + + data = {} + try: + plantilla = DynamicFormat.objects.get(nombre="formato_convenio_default") + serializer = self.serializer_class(plantilla) + data["formato"] = serializer.data + return Response(data, status = status.HTTP_200_OK) + except Exception: + Mensaje.error(data, 'Formato no encontrado.') + return Response(data, status = status.HTTP_400_BAD_REQUEST) + + + def put(self, request, *args, **kwargs): + ''' actualizamos la plantilla ''' + formato = DynamicFormat.objects.filter(nombre="formato_convenio_default").first() + if formato: + formato = DynamicFormat.objects.get(nombre="formato_convenio_default") + serializer = self.serializer_class(formato, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + else: + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + +class FormatoMinuta(BasePermissionAPIView): + ''' + Clase para obtener y actualizar el formato default del convenio + ''' + + serializer_class = DynamicFormatSerializer + + permission_classes_list = [IsAuthenticated, es_admin] + permission_classes_update = [IsAuthenticated, es_admin] + + + def get(self, request, *args, **kwargs): + ''' Obtenemos la plantilla actual ''' + + data = {} + try: + plantilla = DynamicFormat.objects.get(nombre="formato_minuta_default") + serializer = self.serializer_class(plantilla) + data["formato"] = serializer.data + return Response(data, status = status.HTTP_200_OK) + except Exception: + Mensaje.error(data, 'Formato no encontrado.') + return Response(data, status = status.HTTP_400_BAD_REQUEST) + + + def put(self, request, *args, **kwargs): + ''' actualizamos la plantilla ''' + formato = DynamicFormat.objects.filter(nombre="formato_minuta_default").first() + if formato: + formato = DynamicFormat.objects.get(nombre="formato_minuta_default") + serializer = self.serializer_class(formato, data=request.data, partial=True) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data) + else: + serializer = self.serializer_class(data=request.data) + if serializer.is_valid(): + serializer.save() + return Response(serializer.data, status=status.HTTP_201_CREATED) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + return Response(serializer.errors, status=status.HTTP_400_BAD_REQUEST) + + + class FormatoAPIView(BasePermissionAPIView): ''' Clase para manejar los formatos dinámicos @@ -139,4 +227,17 @@ class DescargarFormatoView(BasePermissionAPIView): text = text.replace(key, str(value)) - return text \ No newline at end of file + return text + + +def descargar_formato(request, pk): + """ + Vista para descargar un formato según su ID. + """ + try: + formato = DynamicFormat.objects.get(id=pk) + response = HttpResponse(formato.template, content_type='application/vnd.openxmlformats-officedocument.wordprocessingml.document') + response['Content-Disposition'] = f'attachment; filename="{formato.nombre}"' + return response + except DynamicFormat.DoesNotExist: + raise Http404("El formato solicitado no existe.") \ No newline at end of file diff --git a/cosiap_frontend/src/App.css b/cosiap_frontend/src/App.css index 82cb9c049931514efb6d50a0fdf019d485872e32..b090b5c59c21e10f39c27f2fb08ea721a47016e8 100644 --- a/cosiap_frontend/src/App.css +++ b/cosiap_frontend/src/App.css @@ -222,6 +222,25 @@ input[type="checkbox"] { padding: 0 20px; } +.table-button-container { + display: flex; + gap: 8px; + justify-content: flex-start; + flex-wrap: wrap; + padding: 10px; +} + +.table-button-container button { + padding: 8px 12px; + border-radius: 4px; + cursor: pointer; + transition: background-color 0.3s; +} + +.table-button-container button:hover { + background-color: #e0e0e0; +} + .preview-image { width: 100px; height: 100px; diff --git a/cosiap_frontend/src/App.jsx b/cosiap_frontend/src/App.jsx index 7ea95e073135bf46cbbc9c4c953dd68f90648ebb..1bcca9cd90d3a9e7527140eede49408db3a064c5 100644 --- a/cosiap_frontend/src/App.jsx +++ b/cosiap_frontend/src/App.jsx @@ -37,6 +37,8 @@ import ListaUsuarios from "./components/admin/TablaUsuarios"; import ListaSolicitudesSolicitante from "./components/admin/HistorialAdmin"; import ListaAdmins from "./components/admin/TablaAdministradores"; import CrearAdmin from "./components/admin/CrearAdministrador"; +import ListaFormatos from "./components/plantillas/ListaFormatos"; +import CrearFormato from "./components/plantillas/CrearPlantilla"; function App() { const [viewPageLoader, setViewPageLoader] = useState(false); @@ -128,6 +130,8 @@ function RoutesApp({ setViewPageLoader }) { } /> } /> } /> + } /> + } /> diff --git a/cosiap_frontend/src/api.js b/cosiap_frontend/src/api.js index 4153462f302e40338fcbf3ba66bc6f1ff12daa42..3a680a4215701d69336ea038825d395b62e29a66 100644 --- a/cosiap_frontend/src/api.js +++ b/cosiap_frontend/src/api.js @@ -183,7 +183,14 @@ const api = { }, formatos: { get: () => ax.get('api/plantillas'), - getById: (id) => ax.get(`api/plantillas/download/${id}`,{ responseType: 'blob' }) + post: (data) => ax.post('api/plantillas/', data), + delete: (id) => ax.delete(`api/plantillas/${id}`), + download: (id) => ax.get(`api/plantillas/descargar-formato/${id}`, { responseType: 'blob' }), + getById: (id) => ax.get(`api/plantillas/download/${id}`,{ responseType: 'blob' }), + getMinuta: () => ax.get('api/plantillas/minuta'), + updateMinuta: (data) => ax.put('api/plantillas/minuta/',data), + getConvenio: () => ax.get('api/plantillas/convenio'), + updateConvenio: (data) => ax.put('api/plantillas/convenio/',data), } }; diff --git a/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx b/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx index 0cebc363cca7e0f622900aa768204248e92278d2..fa94ef28e4ba933d1bec62b9454f7ff61cc2f05b 100644 --- a/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx +++ b/cosiap_frontend/src/components/common/layouts/LayoutBaseNavigation.jsx @@ -25,6 +25,12 @@ const linksItemsAdmin=[ navigate: '/solicitudes', isSelected: false }, + { + nameIcon: 'description', + nameItem: 'Formatos', + navigate: '/formatos', + isSelected: false + }, ]; const linksItemsSolicitante = [ diff --git a/cosiap_frontend/src/components/plantillas/CrearPlantilla.jsx b/cosiap_frontend/src/components/plantillas/CrearPlantilla.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8d37d6af28fc697817fb39386d9779b28067d282 --- /dev/null +++ b/cosiap_frontend/src/components/plantillas/CrearPlantilla.jsx @@ -0,0 +1,104 @@ +import { useState } from "react"; +import { useNavigate } from "react-router-dom"; +import api from '../../api'; +import MainContainer from "../common/utility/MainContainer"; +import '@/App.css'; + +const CrearFormato = () =>{ + const [nombre, setNombre] = useState(''); + const [template, setTemplate] = useState(null); + const [alertMessage, setAlertMessage] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + const navigate = useNavigate(); + + const handleFileChange = (e) => { + setTemplate(e.target.files[0]); + }; + + const showAlert = (message, isSuccess) => { + setAlertMessage(message); + setIsSuccess(isSuccess); + + setTimeout(() => { + setAlertMessage(''); + }, 3000); + }; + + const handleSubmit = async (e) => { + e.preventDefault(); + + // Validación básica + if (!nombre || !template) { + showAlert("Por favor, llena todos los campos.", false); + return; + } + + // Validar que el template sea un archivo .docx + const validExtension = template.name.split('.').pop().toLowerCase(); + if (validExtension !== 'docx') { + showAlert("Por favor, sube un archivo con formato .docx.", false); + return; + } + + // Creación del formData para enviar el archivo y los datos + const formData = new FormData(); + console.log("Nombre a enviar:", nombre) + console.log("Template a enviar:", template) + formData.append('nombre', nombre); + formData.append('template', template); + + try { + // Petición POST a la API + await api.formatos.post(formData, { + headers: { + 'Content-Type': 'multipart/form-data', + }, + }); + navigate('/formatos'); + } catch (error) { + showAlert('Hubo un problema al guardar el formato.', false); + } + }; + + return ( + + {alertMessage && ( +
+ {alertMessage} +
+ )} +
+
+ + setNombre(e.target.value)} + required + /> + + +
+ +
+ + +
+
+
+ ); +}; + +export default CrearFormato; \ No newline at end of file diff --git a/cosiap_frontend/src/components/plantillas/ListaFormatos.jsx b/cosiap_frontend/src/components/plantillas/ListaFormatos.jsx new file mode 100644 index 0000000000000000000000000000000000000000..8b501f1fb19c62b791635f72fb51b87edd49baec --- /dev/null +++ b/cosiap_frontend/src/components/plantillas/ListaFormatos.jsx @@ -0,0 +1,290 @@ +import { useState, useEffect } from "react"; +import api from '../../api'; +import MainContainer from "../common/utility/MainContainer"; +import '@/App.css'; +import { useNavigate } from "react-router-dom"; +import Tabla from "../common/utility/ReusableTable"; + +const ListaFormatos = () => { + const [formatos, setFormatos] = useState([]); + const [formatoConvenio, setFormatoConvenio] = useState(null); + const [formatoMinuta, setFormatoMinuta] = useState(null); + const [isAddingConvenio, setIsAddingConvenio] = useState(false); + const [isAddingMinuta, setIsAddingMinuta] = useState(false); + const [selectedFileConvenio, setSelectedFileConvenio] = useState(null); + const [selectedFileMinuta, setSelectedFileMinuta] = useState(null); + const [alertMessage, setAlertMessage] = useState(''); + const [isSuccess, setIsSuccess] = useState(false); + const [isConfirmingDelete, setIsConfirmingDelete] = useState(false); + const [selectedRow, setSelectedRow] = useState(null); + const navigate = useNavigate(); + + + useEffect(() => { + const fetchFormatos = async () => { + try { + const response = await api.formatos.get(); + const filteredFormatos = response.data.filter( + (formato) => + formato.nombre !== "formato_convenio_default" && + formato.nombre !== "formato_minuta_default" + ); + setFormatos(filteredFormatos); + } catch (error) { + setFormatos([]); + } + }; + fetchFormatos(); + }, []); + + useEffect(() => { + const fetchFormatoConvenio = async () => { + try { + const response = await api.formatos.getConvenio(); + setFormatoConvenio(response.data.formato) + } catch (error) { + setFormatoConvenio([]); + } + }; + fetchFormatoConvenio(); + }, []); + + + useEffect(() => { + const fetchFormatoMinuta = async () => { + try { + const response = await api.formatos.getMinuta(); + setFormatoMinuta(response.data.formato) + } catch (error) { + setFormatoMinuta([]); + } + }; + fetchFormatoMinuta(); + }, []); + + // Definimos las columnas a mostrar en la tabla + const columnas = [ + { + label: "Nombre formato", + render: (fila) => fila.nombre + }, + { + label: "Acciones", + render: (fila) => ( +
+ + +
+ ) + } + ]; + + const handleCloseMenu = () => { + setIsConfirmingDelete(false); + }; + + const handleDeleteConfirm = (id) => { + setIsConfirmingDelete(id); + setSelectedRow(id); + }; + + const handleDelete = async (id) => { + const response = await api.formatos.delete(id); + if (response.status === 204){ + showAlert('Formato eliminado exitosamente', true); + } + const formatos = await api.formatos.get(); + const filteredFormatos = formatos.data.filter( + (formato) => + formato.nombre !== "formato_convenio_default" && + formato.nombre !== "formato_minuta_default" + ); + setFormatos(filteredFormatos); + setSelectedRow(null); + handleCloseMenu(); + }; + + const handleSaveConvenio = async () =>{ + + const formData = new FormData(); + formData.append("nombre", "formato_convenio_default"); + formData.append("template", selectedFileConvenio); + + try { + await api.formatos.updateConvenio(formData); + + // Actualizar el convenio mostrado + const response = await api.formatos.getConvenio(); + setFormatoConvenio(response.data.formato); + setIsAddingConvenio(false) + setSelectedFileConvenio(null) + showAlert("Formato para convenios actualizado exitosamente.", true) + } catch (error) { + console.log(error) + } + }; + + const handleSaveMinuta = async () =>{ + const formData = new FormData(); + formData.append("nombre", "formato_minuta_default"); + formData.append("template", selectedFileMinuta); + + try { + await api.formatos.updateMinuta(formData); + + // Actualizar la minuta mostrada + const response = await api.formatos.getMinuta(); + setFormatoMinuta(response.data.formato); + setIsAddingMinuta(false) + setSelectedFileMinuta(null) + showAlert("Formato para minutas actualizado exitosamente.", true) + } catch (error) { + console.log(error) + } + }; + + const handleConvenioChange = (e) => { + setIsAddingConvenio(true) + setSelectedFileConvenio(e.target.files[0]); + }; + + const handleMinutaChange = (e) => { + setIsAddingMinuta(true) + setSelectedFileMinuta(e.target.files[0]); + }; + + const handleDownload = async (id) => { + try { + const response = await api.formatos.download(id, { responseType: 'blob' }); + + const contentType = response.headers['content-type']; + if (contentType !== 'application/vnd.openxmlformats-officedocument.wordprocessingml.document') { + throw new Error('Tipo de archivo inesperado'); + } + + const blob = new Blob([response.data], { type: contentType }); + const url = window.URL.createObjectURL(blob); + const link = document.createElement('a'); + link.href = url; + link.setAttribute('download', `formato.docx`); + + document.body.appendChild(link); + link.click(); + window.URL.revokeObjectURL(url); + } catch (error) { + console.error('Error al descargar el formato:', error); + } + }; + + const showAlert = (message, isSuccess) => { + setAlertMessage(message); + setIsSuccess(isSuccess); + + setTimeout(() => { + setAlertMessage(''); + }, 3000); + }; + + + return ( + + {alertMessage && ( +
+ {alertMessage} +
+ )} +
+
+

Formatos predeterminados

+
+
+

Formato Convenios

+ {formatoConvenio ? ( + + ) : ( +

No hay formato disponible

+ )} + + {isAddingConvenio ? ( +
+ +
+ ) : null} +
+
+

Formato Minutas

+ {formatoMinuta ? ( + + ) : ( +

No hay formato disponible

+ )} + + {isAddingMinuta ? ( +
+ +
+ ) : null} +
+
+
+
+
+

Formatos Registrados

+
+ +
+
+ + {isConfirmingDelete && ( +
+

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

+ + +
+ )} +
+
+
+ ); +}; + +export default ListaFormatos; \ No newline at end of file