From a700c40d20114a4967486f9a51fb3eccfd4b53f1 Mon Sep 17 00:00:00 2001 From: snlavaise Date: Tue, 11 Aug 2026 20:20:04 +0000 Subject: [PATCH] Agregar tab de pendientes, categoria procedimiento y fecha/hora programada --- server/api-mongodb.js | 39 +++ server/db-mongodb.js | 37 +++ src/hooks/useHospitalStore.ts | 62 +++++ src/sections/HistoriaClinica.tsx | 445 ++++++++++++++++++++++++++++++- src/types/index.ts | 18 ++ 5 files changed, 599 insertions(+), 2 deletions(-) diff --git a/server/api-mongodb.js b/server/api-mongodb.js index 25f9151..a6a1c0c 100644 --- a/server/api-mongodb.js +++ b/server/api-mongodb.js @@ -16,6 +16,7 @@ import { initDb, getAllAtb, createAtb, updateAtb, deleteAtb, getAllIndicaciones, createIndicacion, updateIndicacion, deleteIndicacion, getAllMovimientosIndicaciones, createMovimientoIndicacion, + getAllPendientes, createPendiente, updatePendiente, deletePendiente, getValue, setValue } from './db-mongodb.js'; @@ -63,6 +64,7 @@ app.get('/api/state', async (req, res) => { atb: await getAllAtb(), indicaciones: await getAllIndicaciones(), movimientosIndicaciones: await getAllMovimientosIndicaciones(), + pendientes: await getAllPendientes(), usuarios: usuariosList.map(({ passwordHash, ...u }) => u), vistaActual: 'dashboard', currentInternacionId: null @@ -656,6 +658,43 @@ app.post('/api/movimientos-indicaciones', async (req, res) => { } }); +// ========== PENDIENTES ========== +app.get('/api/pendientes', async (req, res) => { + try { + res.json(await getAllPendientes()); + } catch (err) { + res.status(500).json({ error: 'Error al obtener pendientes' }); + } +}); + +app.post('/api/pendientes', async (req, res) => { + try { + const pendiente = { ...req.body, id: req.body.id || generateUUID() }; + await createPendiente(pendiente); + res.json(pendiente); + } catch (err) { + res.status(500).json({ error: 'Error al crear pendiente' }); + } +}); + +app.put('/api/pendientes/:id', async (req, res) => { + try { + await updatePendiente(req.params.id, req.body); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al actualizar pendiente' }); + } +}); + +app.delete('/api/pendientes/:id', async (req, res) => { + try { + await deletePendiente(req.params.id); + res.json({ ok: true }); + } catch (err) { + res.status(500).json({ error: 'Error al eliminar pendiente' }); + } +}); + // ========== AUTH ========== app.post('/api/auth/login', async (req, res) => { try { diff --git a/server/db-mongodb.js b/server/db-mongodb.js index d2613e2..3144467 100644 --- a/server/db-mongodb.js +++ b/server/db-mongodb.js @@ -64,6 +64,7 @@ const memStore = { atb: [], indicaciones: [], movimientos_indicaciones: [], + pendientes: [], kv: {} }; @@ -935,6 +936,42 @@ export async function createMovimientoIndicacion(mov) { } } +// ========== PENDIENTES ========== +export async function getAllPendientes() { + if (db) { + const pendientes = await db.collection('pendientes').find().toArray(); + return cleanDocs(pendientes); + } + return [...memStore.pendientes]; +} + +export async function createPendiente(pendiente) { + if (db) { + await db.collection('pendientes').insertOne(pendiente); + } else { + memStore.pendientes.push(pendiente); + } +} + +export async function updatePendiente(id, datos) { + if (db) { + await db.collection('pendientes').updateOne({ id }, { $set: datos }); + } else { + const idx = memStore.pendientes.findIndex(p => p.id === id); + if (idx !== -1) { + memStore.pendientes[idx] = { ...memStore.pendientes[idx], ...datos }; + } + } +} + +export async function deletePendiente(id) { + if (db) { + await db.collection('pendientes').deleteOne({ id }); + } else { + memStore.pendientes = memStore.pendientes.filter(p => p.id !== id); + } +} + // ========== KV STORE ========== export async function getValue(key) { if (db) { diff --git a/src/hooks/useHospitalStore.ts b/src/hooks/useHospitalStore.ts index b1317eb..6bf23d2 100644 --- a/src/hooks/useHospitalStore.ts +++ b/src/hooks/useHospitalStore.ts @@ -16,6 +16,7 @@ import type { ATB, Indicacion, MovimientoIndicacion, + Pendiente, Vista, Usuario } from '@/types'; @@ -48,6 +49,7 @@ interface HospitalState { atb: ATB[]; indicaciones: Indicacion[]; movimientosIndicaciones: MovimientoIndicacion[]; + pendientes: Pendiente[]; vistaActual: Vista; currentInternacionId?: string | null; usuarios: Usuario[]; @@ -73,6 +75,7 @@ const defaultState = (): HospitalState => ({ vistaActual: 'dashboard', indicaciones: [], movimientosIndicaciones: [], + pendientes: [], camas: [], usuarios: [], currentUser: null, @@ -104,6 +107,7 @@ export function useHospitalStore() { atb: body.atb || [], glucemias: body.glucemias || [], movimientosIndicaciones: body.movimientosIndicaciones || [], + pendientes: body.pendientes || [], currentInternacionId: storedInternacionId || body.currentInternacionId || null, currentUser, isAuthenticated: !!currentUser, @@ -1293,6 +1297,60 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P .sort((a, b) => new Date(b.fechaToma).getTime() - new Date(a.fechaToma).getTime()); }, [state.cultivos]); + const agregarPendiente = useCallback(async (datos: Omit) => { + const nuevoPendiente: Pendiente = { + ...datos, + id: generateUUID(), + fechaCreacion: datos.fechaCreacion || new Date().toISOString().split('T')[0], + horaCreacion: datos.horaCreacion || new Date().toTimeString().slice(0, 5), + estado: datos.estado || 'pendiente', + prioridad: datos.prioridad || 'media', + }; + try { + await apiCall('POST', '/pendientes', nuevoPendiente); + setState(prev => ({ + ...prev, + pendientes: [...(prev.pendientes || []), nuevoPendiente], + })); + return nuevoPendiente.id; + } catch (err) { + console.error('Error al agregar pendiente:', err); + throw err; + } + }, [apiCall]); + + const actualizarPendiente = useCallback(async (id: string, datos: Partial) => { + try { + await apiCall('PUT', `/pendientes/${id}`, datos); + setState(prev => ({ + ...prev, + pendientes: (prev.pendientes || []).map(p => p.id === id ? { ...p, ...datos } : p), + })); + } catch (err) { + console.error('Error al actualizar pendiente:', err); + throw err; + } + }, [apiCall]); + + const eliminarPendiente = useCallback(async (id: string) => { + try { + await apiCall('DELETE', `/pendientes/${id}`); + setState(prev => ({ + ...prev, + pendientes: (prev.pendientes || []).filter(p => p.id !== id), + })); + } catch (err) { + console.error('Error al eliminar pendiente:', err); + throw err; + } + }, [apiCall]); + + const getPendientesByPaciente = useCallback((pacienteId: string) => { + return (state.pendientes || []) + .filter(p => p.pacienteId === pacienteId) + .sort((a, b) => new Date(`${b.fechaCreacion}T${b.horaCreacion || '00:00'}`).getTime() - new Date(`${a.fechaCreacion}T${a.horaCreacion || '00:00'}`).getTime()); + }, [state.pendientes]); + const getEstadisticas = useCallback(() => { const camasEnArea = state.camas.filter(c => !isCamaFueraDeGrupo(c)); const camasFueraDeArea = state.camas.filter(c => isCamaFueraDeGrupo(c)); @@ -1434,6 +1492,10 @@ const actualizarInternacion = useCallback(async (internacionId: string, datos: P getGlucemiasByInternacion, getAcidosBaseByInternacion, getCultivosByInternacion, + agregarPendiente, + actualizarPendiente, + eliminarPendiente, + getPendientesByPaciente, setCurrentInternacion, getEstadisticas, login, diff --git a/src/sections/HistoriaClinica.tsx b/src/sections/HistoriaClinica.tsx index 3316ff2..b847337 100644 --- a/src/sections/HistoriaClinica.tsx +++ b/src/sections/HistoriaClinica.tsx @@ -37,13 +37,20 @@ import { TrendingUp, Activity, Pill, - Users + Users, + ListTodo, + CheckSquare, + Square, + Copy, + Clock, + Search } from 'lucide-react'; import { Card, CardContent } from '@/components/ui/card'; import { Badge } from '@/components/ui/badge'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; +import { Textarea } from '@/components/ui/textarea'; import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select'; import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs'; @@ -51,7 +58,7 @@ import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@ import { ScrollArea, ScrollBar } from "@/components/ui/scroll-area" import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu'; import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts'; -import type { Laboratorio, Glucemia, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta, ATB, Indicacion, MovimientoIndicacion } from '@/types'; +import type { Laboratorio, Glucemia, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario, Interconsulta, ATB, Indicacion, MovimientoIndicacion, Pendiente } from '@/types'; import { formatDateDDMMYYYY } from '@/lib/utils'; import { SeccionIndicaciones } from './SeccionIndicaciones'; @@ -241,7 +248,9 @@ export function HistoriaClinica({ canEdit, }: HistoriaClinicaProps) { const [tabActivo, setTabActivo] = useState('evoluciones'); + const { pendientes } = useHospitalStore(); const indicacionesActivas = (indicadores || []).filter(i => i.estado === 'Activa'); + const pendientesActivos = (pendientes || []).filter(p => p.pacienteId === paciente.id && p.estado === 'pendiente'); const calcularEdad = (fechaNacimiento: string) => { @@ -554,6 +563,11 @@ export function HistoriaClinica({ Interconsultas ({interconsultas.length}) + + + Pendientes + ({pendientesActivos.length}) + @@ -630,6 +644,14 @@ export function HistoriaClinica({ canEdit={canEdit} /> + + + + @@ -2984,4 +3006,423 @@ function SeccionATB({ atb, internacionId, pacienteId, add, update, del, canEdit )} ); +} + +function SeccionPendientes({ pacienteId, internacionId, canEdit }: { pacienteId: string; internacionId: string; canEdit: boolean }) { + const { pendientes, agregarPendiente, actualizarPendiente, eliminarPendiente, currentUser } = useHospitalStore(); + const [modalAbierto, setModalAbierto] = useState(false); + const [pendienteEditar, setPendienteEditar] = useState(null); + + // Form fields + const [descripcion, setDescripcion] = useState(''); + const [categoria, setCategoria] = useState('General'); + const [prioridad, setPrioridad] = useState('media'); + const [observaciones, setObservaciones] = useState(''); + const [fechaProgramada, setFechaProgramada] = useState(''); + const [horaProgramada, setHoraProgramada] = useState(''); + + // Filters + const [filtroEstado, setFiltroEstado] = useState<'todos' | 'pendiente' | 'realizado' | 'cancelado'>('pendiente'); + const [filtroCategoria, setFiltroCategoria] = useState('todas'); + const [busqueda, setBusqueda] = useState(''); + + const misPendientes = (pendientes || []).filter(p => p.pacienteId === pacienteId || p.internacionId === internacionId); + + const pendientesFiltrados = misPendientes.filter(p => { + if (filtroEstado !== 'todos' && p.estado !== filtroEstado) return false; + if (filtroCategoria !== 'todas' && p.categoria !== filtroCategoria) return false; + if (busqueda.trim()) { + const query = busqueda.toLowerCase(); + const descMatch = p.descripcion.toLowerCase().includes(query); + const obsMatch = (p.observaciones || '').toLowerCase().includes(query); + const profMatch = (p.profesional || '').toLowerCase().includes(query); + if (!descMatch && !obsMatch && !profMatch) return false; + } + return true; + }).sort((a, b) => { + if (a.estado === 'pendiente' && b.estado !== 'pendiente') return -1; + if (a.estado !== 'pendiente' && b.estado === 'pendiente') return 1; + const prioWeight = { alta: 3, media: 2, baja: 1 }; + if (prioWeight[b.prioridad] !== prioWeight[a.prioridad]) { + return prioWeight[b.prioridad] - prioWeight[a.prioridad]; + } + return new Date(`${b.fechaCreacion}T${b.horaCreacion || '00:00'}`).getTime() - new Date(`${a.fechaCreacion}T${a.horaCreacion || '00:00'}`).getTime(); + }); + + const resetForm = () => { + setDescripcion(''); + setCategoria('General'); + setPrioridad('media'); + setObservaciones(''); + setFechaProgramada(''); + setHoraProgramada(''); + setPendienteEditar(null); + }; + + const openEditar = (p: Pendiente) => { + setPendienteEditar(p); + setDescripcion(p.descripcion); + setCategoria(p.categoria); + setPrioridad(p.prioridad); + setObservaciones(p.observaciones || ''); + setFechaProgramada(p.fechaProgramada || ''); + setHoraProgramada(p.horaProgramada || ''); + setModalAbierto(true); + }; + + const handleGuardar = async () => { + if (!descripcion.trim()) { + toast.error('La descripción del pendiente es obligatoria'); + return; + } + + try { + const payload: Partial = { + descripcion, + categoria, + prioridad, + observaciones: observaciones.trim() || undefined, + fechaProgramada: (categoria === 'Estudio' || categoria === 'Procedimiento') ? (fechaProgramada || undefined) : undefined, + horaProgramada: (categoria === 'Estudio' || categoria === 'Procedimiento') ? (horaProgramada || undefined) : undefined, + }; + + if (pendienteEditar) { + await actualizarPendiente(pendienteEditar.id, payload); + toast.success('Pendiente actualizado con éxito'); + } else { + const profesional = currentUser ? getNombreProfesional(currentUser) : 'Sistema'; + await agregarPendiente({ + pacienteId, + internacionId, + ...payload, + estado: 'pendiente', + fechaCreacion: new Date().toISOString().split('T')[0], + horaCreacion: new Date().toTimeString().slice(0, 5), + profesional, + }); + toast.success('Pendiente añadido con éxito'); + } + setModalAbierto(false); + resetForm(); + } catch { + toast.error('Error al guardar el pendiente'); + } + }; + + const handleToggleEstado = async (p: Pendiente) => { + const nuevoEstado = p.estado === 'pendiente' ? 'realizado' : 'pendiente'; + try { + await actualizarPendiente(p.id, { + estado: nuevoEstado, + fechaRealizado: nuevoEstado === 'realizado' ? new Date().toISOString().split('T')[0] : undefined, + usuarioRealizado: nuevoEstado === 'realizado' && currentUser ? getNombreProfesional(currentUser) : undefined, + }); + toast.success(nuevoEstado === 'realizado' ? 'Marcado como realizado' : 'Marcado como pendiente'); + } catch { + toast.error('Error al cambiar estado'); + } + }; + + const handleEliminar = async (id: string) => { + try { + await eliminarPendiente(id); + toast.success('Pendiente eliminado'); + } catch { + toast.error('Error al eliminar'); + } + }; + + const handleCopiarPendientes = () => { + const activos = misPendientes.filter(p => p.estado === 'pendiente'); + if (activos.length === 0) { + toast.info('No hay pendientes activos para copiar'); + return; + } + const texto = activos.map((p, idx) => { + let line = `${idx + 1}. [${p.categoria.toUpperCase()}] ${p.descripcion}`; + if (p.fechaProgramada) line += ` (Programado: ${formatDateDDMMYYYY(p.fechaProgramada)}${p.horaProgramada ? ' ' + p.horaProgramada : ''})`; + if (p.prioridad === 'alta') line += ' (ALTA PRIORIDAD)'; + if (p.observaciones) line += ` - Obs: ${p.observaciones}`; + return line; + }).join('\n'); + + navigator.clipboard.writeText(`PENDIENTES DEL PACIENTE:\n${texto}`); + toast.success('Pendientes copiados al portapapeles'); + }; + + const getPriorityBadge = (prio: Pendiente['prioridad']) => { + switch (prio) { + case 'alta': + return Alta; + case 'media': + return Media; + case 'baja': + return Baja; + } + }; + + const getCategoryBadge = (cat: Pendiente['categoria']) => { + return {cat}; + }; + + return ( +
+ {/* Header controls */} +
+
+
+ + setBusqueda(e.target.value)} + className="pl-8 h-9 text-xs" + /> +
+ + + + +
+ +
+ + {canEdit && ( + + )} +
+
+ + {/* List of Pendientes */} + {pendientesFiltrados.length === 0 ? ( + + +

No se encontraron registros de pendientes

+

+ {misPendientes.length === 0 + ? 'Utilice el botón "Nuevo Pendiente" para agregar una tarea pendiente para este paciente.' + : 'Pruebe cambiar los filtros para ver otros registros.'} +

+
+ ) : ( +
+ {pendientesFiltrados.map(p => ( + + + {canEdit && ( + + )} + +
+
+ + {p.descripcion} + + {getPriorityBadge(p.prioridad)} + {getCategoryBadge(p.categoria)} + {p.estado === 'realizado' && ( + + Realizado + + )} +
+ + {p.fechaProgramada && ( +
+ + Programado: {formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada} hs` : ''} +
+ )} + + {p.observaciones && ( +

+ {p.observaciones} +

+ )} + +
+ + + Creado: {formatDateDDMMYYYY(p.fechaCreacion)} {p.horaCreacion || ''} + + {p.profesional && Por: {p.profesional}} + {p.fechaRealizado && ( + + Realizado el {formatDateDDMMYYYY(p.fechaRealizado)} {p.usuarioRealizado ? `por ${p.usuarioRealizado}` : ''} + + )} +
+
+ + {canEdit && ( +
+ + +
+ )} +
+
+ ))} +
+ )} + + {/* Modal Crear/Editar */} + + + + + + {pendienteEditar ? 'Editar Pendiente' : 'Nuevo Pendiente de Paciente'} + + + +
+
+ +