Agregar estudios complementarios en HC

- Agregar tabla estudiosComplementarios en SQLite y API
- Agregar interface EstudioComplementario en tipos
- Agregar funciones en store para gestionar estudios
- Nuevo tab en HistoriaClinica con filtro por internacion/todos
- Fix tema dark mode (colores black en lugar de blue)
This commit is contained in:
2026-04-16 01:22:15 -03:00
parent 60c123fac6
commit 97adfd08b7
6 changed files with 274 additions and 54 deletions
+24 -1
View File
@@ -121,6 +121,15 @@ db.exec(`
estado TEXT DEFAULT 'NAF/Pendiente',
observaciones TEXT
);
CREATE TABLE IF NOT EXISTS estudiosComplementarios (
id TEXT PRIMARY KEY,
pacienteId TEXT NOT NULL,
internacionId TEXT,
fecha TEXT,
tipo TEXT,
resultado TEXT
);
`);
try {
@@ -129,6 +138,12 @@ try {
// La columna ya existe
}
try {
db.exec("ALTER TABLE laboratorios ADD COLUMN internacionId TEXT");
} catch (e) {
// La columna ya existe
}
const app = express();
app.use(cors());
app.use(express.json({ limit: '50mb' }));
@@ -177,6 +192,7 @@ app.get('/api/state', (req, res) => {
laboratorios: queryAll('SELECT * FROM laboratorios').map(l => ({ ...l, resultados: typeof l.resultados === 'string' ? JSON.parse(l.resultados) : l.resultados })),
acidosBase: queryAll('SELECT * FROM acidosbase'),
cultivos: queryAll('SELECT * FROM cultivos'),
estudiosComplementarios: queryAll('SELECT * FROM estudiosComplementarios'),
vistaActual: 'dashboard',
currentInternacionId: null
};
@@ -245,12 +261,19 @@ app.put('/api/state', (req, res) => {
}
if (state.cultivos?.length) {
const stmt = db.prepare('INSERT INTO cultivos (id, pacienteId, internacionId, fechaToma, protocolo, tipoMuestra, observaciones, estado, fechaResultado, germen, sensible, resistente) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
const stmt = db.prepare('INSERT INTO cultivos (id, pacienteId, internacionId, fechaToma, protocolo, tipoMuestra, observaciones, estado, fechaResultado, germen, culpable, resistente) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)');
for (const c of state.cultivos) {
stmt.run(c.id, c.pacienteId, c.internacionId, c.fechaToma, c.protocolo, c.tipoMuestra, c.observaciones, c.estado, c.fechaResultado, c.germen, c.sensible, c.resistente);
}
}
if (state.estudiosComplementarios?.length) {
const stmt = db.prepare('INSERT INTO estudiosComplementarios (id, pacienteId, internacionId, fecha, tipo, resultado) VALUES (?, ?, ?, ?, ?, ?)');
for (const e of state.estudiosComplementarios) {
stmt.run(e.id, e.pacienteId, e.internacionId || null, e.fecha, e.tipo, e.resultado);
}
}
res.json({ ok: true });
});
+3
View File
@@ -143,6 +143,7 @@ function AppContent() {
laboratorios={store.getLaboratoriosByPaciente(paciente.id)}
acidosBase={store.getAcidosBaseByPaciente(paciente.id)}
cultivos={store.getCultivosByPaciente(paciente.id)}
estudiosComplementarios={store.estudiosComplementarios.filter(e => e.pacienteId === paciente.id)}
onAgregarEvolucion={store.agregarEvolucion}
onActualizarEvolucion={store.actualizarEvolucion}
onEliminarEvolucion={store.eliminarEvolucion}
@@ -154,6 +155,8 @@ function AppContent() {
onAgregarCultivo={(c) => store.agregarCultivo({ ...c, internacionId: internacion.id })}
onActualizarCultivo={store.actualizarCultivo}
onEliminarCultivo={store.eliminarCultivo}
onAgregarEstudioComplementario={(e) => store.agregarEstudioComplementario({ ...e, internacionId: internacion.id })}
onEliminarEstudioComplementario={store.eliminarEstudioComplementario}
onActualizarInternacion={store.actualizarInternacion}
onActualizarCama={store.actualizarCama}
onVolver={() => store.setVista('internaciones')}
+33
View File
@@ -8,6 +8,7 @@ import type {
Laboratorio,
AcidoBase,
Cultivo,
EstudioComplementario,
Vista
} from '@/types';
@@ -33,6 +34,7 @@ interface HospitalState {
laboratorios: Laboratorio[];
acidosBase: AcidoBase[];
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
vistaActual: Vista;
currentInternacionId?: string | null;
}
@@ -48,6 +50,7 @@ const defaultState = (): HospitalState => ({
laboratorios: [],
acidosBase: [],
cultivos: [],
estudiosComplementarios: [],
vistaActual: 'dashboard',
camas: []
});
@@ -312,6 +315,33 @@ const finalizarInternacion = useCallback((internacionId: string, datos: {
}));
}, []);
// Acciones de estudios complementarios
const agregarEstudioComplementario = useCallback((estudio: Omit<EstudioComplementario, 'id'>) => {
const nuevoEstudio: EstudioComplementario = {
...estudio,
id: generateUUID(),
};
setState(prev => ({
...prev,
estudiosComplementarios: [...prev.estudiosComplementarios, nuevoEstudio],
}));
return nuevoEstudio.id;
}, []);
const actualizarEstudioComplementario = useCallback((id: string, datos: Partial<EstudioComplementario>) => {
setState(prev => ({
...prev,
estudiosComplementarios: prev.estudiosComplementarios.map(e => e.id === id ? { ...e, ...datos } : e),
}));
}, []);
const eliminarEstudioComplementario = useCallback((id: string) => {
setState(prev => ({
...prev,
estudiosComplementarios: prev.estudiosComplementarios.filter(e => e.id !== id),
}));
}, []);
// Acciones de areas
const agregarArea = useCallback((area: Omit<Area, 'id'>) => {
const nuevaArea: Area = { ...area, id: generateUUID() };
@@ -431,6 +461,9 @@ const getEstadisticas = useCallback(() => {
agregarCultivo,
actualizarCultivo,
eliminarCultivo,
agregarEstudioComplementario,
actualizarEstudioComplementario,
eliminarEstudioComplementario,
agregarArea,
actualizarArea,
eliminarArea,
+46 -46
View File
@@ -5,62 +5,62 @@
@layer base {
:root {
--background: 0 0% 100%;
--foreground: 240 10% 3.9%;
--foreground: 222.2 84% 4.9%;
--card: 0 0% 100%;
--card-foreground: 240 10% 3.9%;
--card-foreground: 222.2 84% 4.9%;
--popover: 0 0% 100%;
--popover-foreground: 240 10% 3.9%;
--primary: 240 5.9% 10%;
--primary-foreground: 0 0% 98%;
--secondary: 240 4.8% 95.9%;
--secondary-foreground: 240 5.9% 10%;
--muted: 240 4.8% 95.9%;
--muted-foreground: 240 3.8% 46.1%;
--accent: 240 4.8% 95.9%;
--accent-foreground: 240 5.9% 10%;
--popover-foreground: 222.2 84% 4.9%;
--primary: 222.2 47.4% 11.2%;
--primary-foreground: 210 40% 98%;
--secondary: 210 40% 96.1%;
--secondary-foreground: 222.2 47.4% 11.2%;
--muted: 210 40% 96.1%;
--muted-foreground: 215.4 16.3% 46.9%;
--accent: 210 40% 96.1%;
--accent-foreground: 222.2 47.4% 11.2%;
--destructive: 0 84.2% 60.2%;
--destructive-foreground: 0 0% 98%;
--border: 240 5.9% 90%;
--input: 240 5.9% 90%;
--ring: 240 5.9% 10%;
--destructive-foreground: 210 40% 98%;
--border: 214.3 31.8% 91.4%;
--input: 214.3 31.8% 91.4%;
--ring: 222.2 84% 4.9%;
--radius: 0.625rem;
--sidebar-background: 0 0% 98%;
--sidebar-foreground: 240 5.3% 26.1%;
--sidebar-primary: 240 5.9% 10%;
--sidebar-primary-foreground: 0 0% 98%;
--sidebar-accent: 240 4.8% 95.9%;
--sidebar-accent-foreground: 240 5.9% 10%;
--sidebar-border: 220 13% 91%;
--sidebar-foreground: 222.2 84% 4.9%;
--sidebar-primary: 222.2 47.4% 11.2%;
--sidebar-primary-foreground: 210 40% 98%;
--sidebar-accent: 210 40% 96.1%;
--sidebar-accent-foreground: 222.2 47.4% 11.2%;
--sidebar-border: 214.3 31.8% 91.4%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
.dark {
--background: 240 10% 3.9%;
--foreground: 0 0% 98%;
--card: 240 10% 3.9%;
--card-foreground: 0 0% 98%;
--popover: 240 10% 3.9%;
--popover-foreground: 0 0% 98%;
--primary: 0 0% 98%;
--primary-foreground: 240 5.9% 10%;
--secondary: 240 3.7% 15.9%;
--secondary-foreground: 0 0% 98%;
--muted: 240 3.7% 15.9%;
--muted-foreground: 240 5% 64.9%;
--accent: 240 3.7% 15.9%;
--accent-foreground: 0 0% 98%;
--background: 222.2 84% 4.9%;
--foreground: 210 40% 98%;
--card: 222.2 84% 4.9%;
--card-foreground: 210 40% 98%;
--popover: 222.2 84% 4.9%;
--popover-foreground: 210 40% 98%;
--primary: 210 40% 98%;
--primary-foreground: 222.2 47.4% 11.2%;
--secondary: 217.2 32.6% 17.5%;
--secondary-foreground: 210 40% 98%;
--muted: 217.2 32.6% 17.5%;
--muted-foreground: 215 20.2% 65.1%;
--accent: 217.2 32.6% 17.5%;
--accent-foreground: 210 40% 98%;
--destructive: 0 62.8% 30.6%;
--destructive-foreground: 0 0% 98%;
--border: 240 3.7% 15.9%;
--input: 240 3.7% 15.9%;
--ring: 240 4.9% 83.9%;
--sidebar-background: 240 5.9% 10%;
--sidebar-foreground: 240 4.8% 95.9%;
--sidebar-primary: 224.3 76.3% 48%;
--sidebar-primary-foreground: 0 0% 100%;
--sidebar-accent: 240 3.7% 15.9%;
--sidebar-accent-foreground: 240 4.8% 95.9%;
--sidebar-border: 240 3.7% 15.9%;
--destructive-foreground: 210 40% 98%;
--border: 217.2 32.6% 17.5%;
--input: 217.2 32.6% 17.5%;
--ring: 212.7 26.8% 83.9%;
--sidebar-background: 222.2 84% 4.9%;
--sidebar-foreground: 210 40% 98%;
--sidebar-primary: 210 40% 98%;
--sidebar-primary-foreground: 222.2 47.4% 11.2%;
--sidebar-accent: 217.2 32.6% 17.5%;
--sidebar-accent-foreground: 210 40% 98%;
--sidebar-border: 217.2 32.6% 17.5%;
--sidebar-ring: 217.2 91.2% 59.8%;
}
}
+156 -4
View File
@@ -39,7 +39,7 @@ import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
import type { Laboratorio, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama } from '@/types';
import type { Laboratorio, AcidoBase, Cultivo, Paciente, ResultadoLaboratorio, Internacion, Evolucion, Cama, EstudioComplementario } from '@/types';
import { formatDateDDMMYYYY } from '@/lib/utils';
interface HistoriaClinicaProps {
@@ -51,6 +51,7 @@ interface HistoriaClinicaProps {
laboratorios: Laboratorio[];
acidosBase: AcidoBase[];
cultivos: Cultivo[];
estudiosComplementarios: EstudioComplementario[];
onAgregarEvolucion: (evolucion: Omit<Evolucion, 'id'>) => void;
onActualizarEvolucion: (id: string, datos: Partial<Evolucion>) => void;
onEliminarEvolucion: (id: string) => void;
@@ -62,6 +63,8 @@ interface HistoriaClinicaProps {
onAgregarCultivo: (cultivo: Omit<Cultivo, 'id'>) => void;
onActualizarCultivo: (id: string, datos: Partial<Cultivo>) => void;
onEliminarCultivo: (id: string) => void;
onAgregarEstudioComplementario: (estudio: Omit<EstudioComplementario, 'id'>) => void;
onEliminarEstudioComplementario: (id: string) => void;
onActualizarInternacion: (id: string, datos: Partial<Internacion>) => void;
onActualizarCama: (id: string, datos: Partial<Cama>) => void;
onVolver: () => void;
@@ -148,6 +151,7 @@ export function HistoriaClinica({
laboratorios,
acidosBase,
cultivos,
estudiosComplementarios,
onAgregarEvolucion,
onActualizarEvolucion,
onEliminarEvolucion,
@@ -159,6 +163,8 @@ export function HistoriaClinica({
onAgregarCultivo,
onActualizarCultivo,
onEliminarCultivo,
onAgregarEstudioComplementario,
onEliminarEstudioComplementario,
onActualizarInternacion,
onActualizarCama,
onVolver,
@@ -509,6 +515,12 @@ export function HistoriaClinica({
<span className="sm:hidden">Cult</span>
({cultivos.length})
</TabsTrigger>
<TabsTrigger value="estudios" className="flex-shrink-0 flex items-center gap-1 text-xs sm:text-sm">
<ClipboardList className="h-3 w-3 sm:h-4 sm:w-4" />
<span className="hidden sm:inline">Estudios</span>
<span className="sm:hidden">Est</span>
({estudiosComplementarios.length})
</TabsTrigger>
</TabsList>
<TabsContent value="laboratorios" className="mt-4">
@@ -526,6 +538,16 @@ export function HistoriaClinica({
<TabsContent value="cultivos" className="mt-4">
<SeccionCultivos cults={cultivos} patient={paciente} add={onAgregarCultivo} update={onActualizarCultivo} del={onEliminarCultivo} />
</TabsContent>
<TabsContent value="estudios" className="mt-4">
<SeccionEstudiosComplementarios
estudios={estudiosComplementarios}
internacionId={internacion.id}
pacienteId={paciente.id}
add={onAgregarEstudioComplementario}
del={onEliminarEstudioComplementario}
/>
</TabsContent>
</Tabs>
</div>
);
@@ -1011,8 +1033,11 @@ if (!esUnidadSiguiente) {
</DialogContent>
</Dialog>
{labs.length === 0 ? <div className="text-center py-8 text-gray-400"><FlaskConical className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay laboratorios</p></div> :
<div className="overflow-x-auto min-w-full">
{labs.length === 0 ? (
<div className="text-center py-8 text-gray-400"><FlaskConical className="h-12 w-12 mx-auto mb-2 opacity-50" /><p>No hay laboratorios</p></div>
) : (
<div className="relative w-full overflow-auto grid grid-cols-1">
<Table>
<TableHeader>
<TableRow>
@@ -1086,7 +1111,8 @@ if (!esUnidadSiguiente) {
</TableBody>
</Table>
</div>
}
)}
</div>
);
}
@@ -1658,3 +1684,129 @@ const resetRes = () => {
</div>
);
}
function SeccionEstudiosComplementarios({ estudios, internacionId, pacienteId, add, del }: {
estudios: EstudioComplementario[];
internacionId: string;
pacienteId: string;
add: (e: Omit<EstudioComplementario, 'id'>) => void;
del: (id: string) => void;
}) {
const [dialog, setDialog] = useState(false);
const [filtro, setFiltro] = useState<'todos' | 'internacion'>('internacion');
const [fecha, setFecha] = useState(new Date().toISOString().split('T')[0]);
const [tipo, setTipo] = useState('');
const [resultado, setResultado] = useState('');
const estudiosFiltrados = filtro === 'internacion'
? estudios.filter(e => e.internacionId === internacionId)
: estudios;
const reset = () => {
setFecha(new Date().toISOString().split('T')[0]);
setTipo('');
setResultado('');
};
const handleGuardar = () => {
if (!tipo || !resultado) return;
add({
pacienteId,
internacionId,
fecha,
tipo,
resultado,
});
setDialog(false);
reset();
};
return (
<div className="space-y-4">
<div className="flex flex-col sm:flex-row justify-between items-start sm:items-center gap-4">
<div className="flex gap-2">
<Button variant="outline" onClick={() => { reset(); setDialog(true); }}>
<Plus className="h-4 w-4 mr-2" />
Nuevo Estudio
</Button>
</div>
<Select value={filtro} onValueChange={(v: 'todos' | 'internacion') => setFiltro(v)}>
<SelectTrigger className="w-[180px]">
<SelectValue placeholder="Filtrar" />
</SelectTrigger>
<SelectContent>
<SelectItem value="internacion">Solo esta internación</SelectItem>
<SelectItem value="todos">Todos los estudios</SelectItem>
</SelectContent>
</Select>
</div>
<Dialog open={dialog} onOpenChange={setDialog}>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Nuevo Estudio Complementario</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Fecha</Label>
<Input type="date" value={fecha} onChange={e => setFecha(e.target.value)} />
</div>
<div>
<Label>Tipo de Estudio</Label>
<Input
value={tipo}
onChange={e => setTipo(e.target.value)}
placeholder="Ej: Radiografía de tórax, ECG, Tomografía, etc."
/>
</div>
<div>
<Label>Resultado</Label>
<textarea
className="w-full p-2 border rounded-md text-sm min-h-[100px]"
value={resultado}
onChange={e => setResultado(e.target.value)}
placeholder="Ingrese el resultado del estudio..."
/>
</div>
</div>
<div className="flex justify-end gap-2 mt-4">
<Button variant="outline" onClick={() => setDialog(false)}>Cancelar</Button>
<Button onClick={handleGuardar} disabled={!tipo || !resultado}>
<Save className="h-4 w-4 mr-2" />
Guardar
</Button>
</div>
</DialogContent>
</Dialog>
{estudiosFiltrados.length === 0 ? (
<div className="text-center py-8 text-gray-400">
<ClipboardList className="h-12 w-12 mx-auto mb-2 opacity-50" />
<p>No hay estudios complementarios</p>
</div>
) : (
<div className="space-y-3">
{estudiosFiltrados.sort((a, b) => new Date(b.fecha).getTime() - new Date(a.fecha).getTime()).map(e => (
<Card key={e.id} className="hover:shadow-md transition-shadow">
<CardContent className="p-4">
<div className="flex flex-col sm:flex-row sm:items-start sm:justify-between gap-3">
<div className="flex-1">
<div className="flex items-center gap-2 mb-2">
<Calendar className="h-4 w-4 text-gray-500" />
<span className="text-sm font-medium">{e.fecha}</span>
</div>
<h4 className="font-semibold text-gray-900 dark:text-white">{e.tipo}</h4>
<p className="text-sm text-gray-600 dark:text-gray-300 mt-1 whitespace-pre-wrap">{e.resultado}</p>
</div>
<Button size="sm" variant="outline" className="text-red-600 self-start" onClick={() => del(e.id)}>
<Trash2 className="h-4 w-4" />
</Button>
</div>
</CardContent>
</Card>
))}
</div>
)}
</div>
);
}
+9
View File
@@ -141,4 +141,13 @@ export interface Cultivo {
observaciones?: string;
}
export interface EstudioComplementario {
id: string;
pacienteId: string;
internacionId?: string;
fecha: string;
tipo: string;
resultado: string;
}
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso';