From 6c560de2bdec76e6b7584fbb7b63b12d5df4e98f Mon Sep 17 00:00:00 2001 From: Javier Date: Wed, 25 Mar 2026 11:59:07 +0100 Subject: [PATCH] feat: migrate protocols to MongoDB and implement admin list view --- .../http/routes/protocol.routes.ts | 36 +++-- .../persistence/MongoProtocolRepository.ts | 37 +++++ frontend/src/App.tsx | 2 + frontend/src/components/layout/MenuSheet.tsx | 1 + .../src/pages/admin/ProtocolListAdmin.tsx | 144 ++++++++++++++++++ 5 files changed, 208 insertions(+), 12 deletions(-) create mode 100644 backend/src/infrastructure/persistence/MongoProtocolRepository.ts create mode 100644 frontend/src/pages/admin/ProtocolListAdmin.tsx diff --git a/backend/src/infrastructure/http/routes/protocol.routes.ts b/backend/src/infrastructure/http/routes/protocol.routes.ts index 6b6fb0c4..81aca8ad 100644 --- a/backend/src/infrastructure/http/routes/protocol.routes.ts +++ b/backend/src/infrastructure/http/routes/protocol.routes.ts @@ -2,10 +2,10 @@ import { Router } from 'express'; import { ProtocolController } from '../controllers/ProtocolController'; import { GetAllProtocolsUseCase } from '../../../application/usecases/GetAllProtocolsUseCase'; import { GetProtocolByIdUseCase } from '../../../application/usecases/GetProtocolByIdUseCase'; -import { StaticProtocolRepository } from '../../persistence/StaticProtocolRepository'; +import { MongoProtocolRepository } from '../../persistence/MongoProtocolRepository'; // manual dependency injection for routes -const repository = new StaticProtocolRepository(); +const repository = new MongoProtocolRepository(); const getAllUseCase = new GetAllProtocolsUseCase(repository); const getByIdUseCase = new GetProtocolByIdUseCase(repository); const controller = new ProtocolController(getAllUseCase, getByIdUseCase); @@ -15,20 +15,32 @@ const router = Router(); router.get('/', controller.getAll); router.get('/:id', controller.getById); -// Placeholders for future endpoints matching the original content.ts routes -router.post('/', (_req, res) => { - res.status(201).json({ - message: 'Create content endpoint - to be implemented', - id: 'new-content-id' - }); +// CRUD endpoints for Admin +router.post('/', async (req, res) => { + try { + await repository.save(req.body); + res.status(201).json({ message: 'Protocolo creado correctamente' }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } }); -router.put('/:id', (req, res) => { - res.json({ message: `Update content ${req.params.id} - to be implemented` }); +router.put('/:id', async (req, res) => { + try { + await repository.save({ ...req.body, id: req.params.id }); + res.json({ message: 'Protocolo actualizado correctamente' }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } }); -router.delete('/:id', (req, res) => { - res.json({ message: `Delete content ${req.params.id} - to be implemented` }); +router.delete('/:id', async (req, res) => { + try { + await repository.delete(req.params.id); + res.json({ message: 'Protocolo eliminado correctamente' }); + } catch (error: any) { + res.status(500).json({ error: error.message }); + } }); export default router; diff --git a/backend/src/infrastructure/persistence/MongoProtocolRepository.ts b/backend/src/infrastructure/persistence/MongoProtocolRepository.ts new file mode 100644 index 00000000..c9618537 --- /dev/null +++ b/backend/src/infrastructure/persistence/MongoProtocolRepository.ts @@ -0,0 +1,37 @@ +import { IProtocolRepository } from '../../domain/repositories/IProtocolRepository'; +import { TelephoneProtocol, ProtocolCategory, AgeGroup } from '../../domain/entities/TelephoneProtocol'; +import { ClinicalProtocol } from '../../domain/entities/ClinicalProtocol'; +import { ProtocolModel } from '../database/models/ProtocolModel'; + +export class MongoProtocolRepository implements IProtocolRepository { + async findAll(): Promise { + return await ProtocolModel.find().lean(); + } + + async findById(id: string): Promise { + return await ProtocolModel.findOne({ id }).lean(); + } + + async findByCategory(category: ProtocolCategory): Promise { + return await ProtocolModel.find({ categoria: category }).lean(); + } + + async findByAgeGroup(ageGroup: AgeGroup): Promise { + // Nota: El modelo de ClinicalProtocol maneja grupos_edad de forma distinta + return await ProtocolModel.find({ + $or: [{ 'grupos_edad.id': ageGroup }, { 'grupos_edad': { $exists: false } }] + }).lean(); + } + + async save(protocol: any): Promise { + await ProtocolModel.findOneAndUpdate( + { id: protocol.id }, + protocol, + { upsert: true, new: true } + ); + } + + async delete(id: string): Promise { + await ProtocolModel.findOneAndDelete({ id }); + } +} diff --git a/frontend/src/App.tsx b/frontend/src/App.tsx index f8185731..d16871e8 100644 --- a/frontend/src/App.tsx +++ b/frontend/src/App.tsx @@ -42,6 +42,7 @@ const TriageStart = lazy(() => import("./pages/TriageStart")); const BurnCalculator = lazy(() => import("./pages/BurnCalculator")); const DoseCalculator = lazy(() => import("./pages/DoseCalculator")); const MediaAdmin = lazy(() => import("./pages/admin/MediaAdmin")); +const ProtocolListAdmin = lazy(() => import("./pages/admin/ProtocolListAdmin")); // Wrapper para pasar props a Home const HomeWrapper = ({ onSearchClick }: { onSearchClick: () => void }) => { @@ -101,6 +102,7 @@ const App = () => { } /> } /> } /> + } /> } /> diff --git a/frontend/src/components/layout/MenuSheet.tsx b/frontend/src/components/layout/MenuSheet.tsx index 61b575dd..932b15d0 100644 --- a/frontend/src/components/layout/MenuSheet.tsx +++ b/frontend/src/components/layout/MenuSheet.tsx @@ -33,6 +33,7 @@ const MenuSheet = ({ isOpen, onClose }: MenuSheetProps) => { items: [ { to: "/manual", label: "Manual del TES (Wiki)", icon: }, { to: "/admin/media", label: "Gestión de Medios", icon: }, + { to: "/admin/protocols", label: "Editor de Protocolos", icon: }, { to: "/guia-refuerzo", label: "Guías de Refuerzo", icon: }, { to: "/galeria", label: "Galería de Técnicas", icon: }, ] diff --git a/frontend/src/pages/admin/ProtocolListAdmin.tsx b/frontend/src/pages/admin/ProtocolListAdmin.tsx new file mode 100644 index 00000000..1cf0ae6c --- /dev/null +++ b/frontend/src/pages/admin/ProtocolListAdmin.tsx @@ -0,0 +1,144 @@ +import React, { useState, useEffect } from 'react'; +import { useNavigate } from 'react-router-dom'; +import { Plus, Edit3, Trash2, Search, FileText } from 'lucide-react'; + +interface ProtocolSummary { + id: string; + titulo: string; + categoria: string; + version: string; + urgencia: string; +} + +const ProtocolListAdmin: React.FC = () => { + const [protocols, setProtocols] = useState([]); + const [loading, setLoading] = useState(true); + const [searchTerm, setSearchTerm] = useState(''); + const navigate = useNavigate(); + + const fetchProtocols = async () => { + try { + const response = await fetch('/api/content'); + const data = await response.json(); + setProtocols(data); + } catch (err) { + console.error('Error fetching protocols:', err); + } finally { + setLoading(false); + } + }; + + useEffect(() => { + fetchProtocols(); + }, []); + + const handleDelete = async (id: string) => { + if (!confirm(`¿Estás seguro de eliminar el protocolo ${id}?`)) return; + try { + const response = await fetch(`/api/content/${id}`, { method: 'DELETE' }); + if (response.ok) fetchProtocols(); + } catch (err) { + alert('Error al eliminar'); + } + }; + + const filteredProtocols = protocols.filter(p => + p.titulo.toLowerCase().includes(searchTerm.toLowerCase()) || + p.id.toLowerCase().includes(searchTerm.toLowerCase()) + ); + + return ( +
+
+
+

Editor de Protocolos

+

// Gestión clínica en tiempo real (MongoDB)

+
+ + +
+ +
+ + setSearchTerm(e.target.value)} + /> +
+ +
+ + + + + + + + + + + + {filteredProtocols.map((p) => ( + + + + + + + + ))} + +
ID / TituloCategoríaVersiónUrgenciaAcciones
+
{p.titulo}
+
{p.id}
+
+ + {p.categoria} + + v{p.version} + + {p.urgencia} + + +
+ + +
+
+ + {loading && ( +
+ Cargando protocolos... +
+ )} + + {!loading && filteredProtocols.length === 0 && ( +
+ + No se encontraron protocolos +
+ )} +
+
+ ); +}; + +export default ProtocolListAdmin;