Files
administracionHospital/src/sections/MapaCamas.tsx
T
snlavaise 56402563fb Add area assignment to internaciones and permission controls
- Add areaId field to Internacion type and database schema
- Add area selection in NuevoIngreso, EditIngreso, and Internaciones forms
- Filter out 'Fuera de area' from area selection dropdowns
- Add permission controls for users to only edit internaciones in their area
- Add canEdit check to hide 'Editar Ingreso' button in HistoriaClinica
- Fix getInternacionAreaId to use new areaId field first, fallback to cama area
- Add INSERT OR IGNORE to prevent duplicate errors
2026-04-23 17:49:07 -03:00

619 lines
28 KiB
TypeScript

import { useState } from 'react';
import { Bed, CheckCircle2, Clock, Wrench, User, Plus, Trash, Edit2, Save, X } from 'lucide-react';
import { Card, CardContent } from '@/components/ui/card';
import { Badge } from '@/components/ui/badge';
import { Button } from '@/components/ui/button';
import { formatDateDDMMYYYY } from '@/lib/utils';
import { Dialog, DialogContent, DialogHeader, DialogTitle, DialogTrigger } from '@/components/ui/dialog';
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Label } from '@/components/ui/label';
import { Input } from '@/components/ui/input';
import type { Cama, Paciente, Internacion, Area } from '@/types';
import { useHospitalStore } from '@/hooks/useHospitalStore';
interface MapaCamasProps {
camas: Cama[];
areas: Area[];
pacientes: Paciente[];
internaciones: Internacion[];
onActualizarCama: (id: string, datos: Partial<Cama>) => void;
onAgregarCama: (cama: Omit<Cama, 'id'>) => string;
onEliminarCama: (id: string) => void;
onIniciarInternacion: (internacion: Omit<Internacion, 'id' | 'activa'>) => void;
onAgregarArea: (area: Omit<Area, 'id'>) => string;
onActualizarArea: (id: string, datos: Partial<Area>) => void;
onEliminarArea: (id: string) => void;
getPacienteById: (id: string) => Paciente | undefined;
getInternacionById: (id: string) => Internacion | undefined;
}
export function MapaCamas({
camas,
areas,
pacientes,
internaciones,
onActualizarCama,
onAgregarCama,
onEliminarCama,
onIniciarInternacion,
onAgregarArea,
onActualizarArea,
onEliminarArea,
getPacienteById,
getInternacionById
}: MapaCamasProps) {
const { canEditCama, canEditInArea } = useHospitalStore();
const [filtroSala, setFiltroSala] = useState<string>('todas');
const [filtroTipo, setFiltroTipo] = useState<string>('todos');
const [filtroEstado, setFiltroEstado] = useState<string>('todos');
const [camaSeleccionada, setCamaSeleccionada] = useState<Cama | null>(null);
const [areaSeleccionada, setAreaSeleccionada] = useState<string>('');
const [pacienteSeleccionado, setPacienteSeleccionado] = useState<string>('');
const [diagnostico, setDiagnostico] = useState('');
const [enfermedadActual, setEnfermedadActual] = useState('');
const [antecedentesEnfermedadActual, setAntecedentesEnfermedadActual] = useState('');
const [medico, setMedico] = useState('');
const [dialogoAbierto, setDialogoAbierto] = useState(false);
// Area dialog state
const [areaDialogOpen, setAreaDialogOpen] = useState(false);
const [areaNombre, setAreaNombre] = useState('');
const [editingAreaId, setEditingAreaId] = useState<string | null>(null);
// Cama dialog state
const [bedDialogOpen, setBedDialogOpen] = useState(false);
const [editingBed, setEditingBed] = useState<Cama | null>(null);
const [bedNumero, setBedNumero] = useState('');
const [bedTipo, setBedTipo] = useState<Cama['tipo']>('General');
const [bedAreaId, setBedAreaId] = useState<string | undefined>(undefined);
const salas = Array.from(new Set(camas.map(c => c.areaId).filter(Boolean))) as string[];
const areaById = Object.fromEntries(areas.map(a => [a.id, a.nombre]));
const tipos = Array.from(new Set(camas.map(c => c.tipo)));
const camasFiltradas = camas.filter(cama => {
if (filtroSala !== 'todas' && cama.areaId !== filtroSala) return false;
if (filtroTipo !== 'todos' && cama.tipo !== filtroTipo) return false;
if (filtroEstado !== 'todos' && cama.estado !== filtroEstado) return false;
return true;
});
const getEstadoColor = (cama: Cama) => {
if (cama.estado === 'Disponible') {
return 'bg-green-100 border-green-300 text-green-800 dark:bg-green-900 dark:border-green-700 dark:text-green-300';
}
if (cama.estado === 'Reservada') {
return 'bg-orange-100 border-orange-300 text-orange-800 dark:bg-orange-900 dark:border-orange-700 dark:text-orange-300';
}
if (cama.estado === 'Ocupada') {
// Tipos de aislamiento
const aislamientos = ['Aislamiento KPC', 'Aislamiento COVID', 'Aislamiento Clostridium', 'Aislamiento Neutropenico'];
if (aislamientos.includes(cama.tipo)) {
return 'bg-red-100 border-red-300 text-red-800 dark:bg-red-900 dark:border-red-700 dark:text-red-300';
}
return 'bg-blue-100 border-blue-300 text-blue-800 dark:bg-blue-900 dark:border-blue-700 dark:text-blue-300';
}
return 'bg-amber-100 border-amber-300 text-amber-800 dark:bg-amber-900 dark:border-amber-700 dark:text-amber-300';
};
const getEstadoIcono = (cama: Cama) => {
switch (cama.estado) {
case 'Disponible': return <CheckCircle2 className="h-5 w-5" />;
case 'Ocupada': return <User className="h-5 w-5" />;
case 'Reparacion': return <Wrench className="h-5 w-5" />;
case 'Reservada': return <Clock className="h-5 w-5" />;
}
};
const parseBedNumber = (numero: string) => {
const match = numero.match(/^(\d{3})-(\d+)$/);
if (!match) return { sala: 0, cama: 0 };
return {
sala: parseInt(match[1]),
cama: parseInt(match[2])
};
};
const sortedCamas = [...camasFiltradas].sort((a, b) => {
const pa = parseBedNumber(a.numero);
const pb = parseBedNumber(b.numero);
const getOrden = (sala: number) => {
const grupo = Math.floor(sala / 100);
const esPar = sala % 2 === 0;
if (grupo === 2) {
if (esPar) return { grupo: 2, suborden: 0, sala }; // 2XX pares, menor a mayor
return { grupo: 1, suborden: 1, sala }; // 2XX impares, mayor a menor
}
if (grupo === 3) {
if (esPar) return { grupo: 3, suborden: 0, sala }; // 3XX pares, menor a mayor
return { grupo: 4, suborden: 1, sala }; // 3XX impares, mayor a menor
}
if (grupo === 4) {
if (esPar) return { grupo: 6, suborden: 0, sala }; // 4XX pares, menor a mayor
return { grupo: 5, suborden: 1, sala }; // 4XX impares, mayor a menor
}
return { grupo: 10, suborden: 1, sala };
};
const oa = getOrden(pa.sala);
const ob = getOrden(pb.sala);
if (oa.grupo !== ob.grupo) return oa.grupo - ob.grupo;
if (oa.suborden === ob.suborden) {
if (oa.suborden === 0) return (oa.sala || pa.sala) - (ob.sala || pb.sala);
return (ob.sala || 0) - (oa.sala || 0);
}
return oa.suborden - ob.suborden;
});
const handleOcuparCama = () => {
if (camaSeleccionada && areaSeleccionada && pacienteSeleccionado && diagnostico && enfermedadActual && medico) {
onIniciarInternacion({
pacienteId: pacienteSeleccionado,
camaId: camaSeleccionada.id,
areaId: areaSeleccionada,
diagnosticoIngreso: diagnostico,
enfermedadActual,
antecedentesEnfermedadActual: antecedentesEnfermedadActual || undefined,
medicoIngresante: medico,
});
setDialogoAbierto(false);
setCamaSeleccionada(null);
setAreaSeleccionada('');
setPacienteSeleccionado('');
setDiagnostico('');
setEnfermedadActual('');
setAntecedentesEnfermedadActual('');
setMedico('');
}
};
const handleCambiarEstado = (cama: Cama, nuevoEstado: Cama['estado']) => {
onActualizarCama(cama.id, { estado: nuevoEstado });
};
const pacientesSinInternar = pacientes.filter(p => {
const internacionActiva = internaciones.find(i => i.pacienteId === p.id && i.activa);
return !internacionActiva;
});
return (
<div className="space-y-6 dark:text-white">
<div className="flex flex-col sm:flex-row sm:items-center sm:justify-between gap-4">
<div>
<h1 className="text-2xl font-bold text-gray-900 dark:text-white">Mapa de Camas</h1>
<p className="text-gray-500 dark:text-gray-400">Gestión de camas del servicio</p>
</div>
<div className="flex items-center gap-2">
<Badge variant="outline" className="bg-green-50 text-green-700 dark:bg-green-900 dark:text-green-300 dark:border-green-700">
{camas.filter(c => c.estado === 'Disponible').length} Disponibles
</Badge>
<Badge variant="outline" className="bg-red-50 text-red-700 dark:bg-red-900 dark:text-red-300 dark:border-red-700">
{camas.filter(c => c.estado === 'Ocupada').length} Ocupadas
</Badge>
<div className="ml-4 flex items-center gap-2 flex-wrap sm:flex-nowrap">
<Button variant="outline" onClick={() => {
setEditingAreaId(null);
setAreaNombre('');
setAreaDialogOpen(true);
}} className="text-xs px-2 py-1 sm:text-sm sm:px-4 sm:py-2">
Administrar Áreas
</Button>
<Button variant="outline" onClick={() => {
setEditingBed(null);
setBedNumero('');
setBedTipo('General');
setBedAreaId(areas?.[0]?.id);
setBedDialogOpen(true);
}} className="text-xs px-2 py-1 sm:text-sm sm:px-4 sm:py-2">
<Plus className="h-3 w-3 sm:h-4 sm:w-4 sm:mr-1" />
<span className="hidden sm:inline">Agregar Cama</span>
</Button>
</div>
</div>
</div>
{/* Filtros */}
<Card>
<CardContent className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-3 gap-4">
<div>
<Label className="text-xs text-gray-500 mb-1 block">Área</Label>
<Select value={filtroSala} onValueChange={setFiltroSala}>
<SelectTrigger>
<SelectValue placeholder="Todas las áreas" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todas">Todas las áreas</SelectItem>
{salas.map(sala => (
<SelectItem key={sala} value={sala}>{areaById[sala] ?? sala}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-gray-500 mb-1 block">Tipo</Label>
<Select value={filtroTipo} onValueChange={setFiltroTipo}>
<SelectTrigger>
<SelectValue placeholder="Todos los tipos" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todos los tipos</SelectItem>
{tipos.map(tipo => (
<SelectItem key={tipo} value={tipo}>{tipo}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label className="text-xs text-gray-500 mb-1 block">Estado</Label>
<Select value={filtroEstado} onValueChange={setFiltroEstado}>
<SelectTrigger>
<SelectValue placeholder="Todos los estados" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todos los estados</SelectItem>
<SelectItem value="Disponible">Disponible</SelectItem>
<SelectItem value="Ocupada">Ocupada</SelectItem>
<SelectItem value="Mantenimiento">Mantenimiento</SelectItem>
<SelectItem value="Reservada">Reservada</SelectItem>
</SelectContent>
</Select>
</div>
</div>
</CardContent>
</Card>
{/* Mapa de Camas */}
<div className="grid grid-cols-2 sm:grid-cols-3 md:grid-cols-4 lg:grid-cols-5 gap-2 sm:gap-4">
{sortedCamas.map((cama) => {
const paciente = cama.pacienteId ? getPacienteById(cama.pacienteId) : null;
const internacion = cama.internacionId ? getInternacionById(cama.internacionId) : null;
return (
<Dialog key={cama.id}>
<DialogTrigger asChild>
<Card
className={`cursor-pointer hover:shadow-md transition-shadow border-2 w-full ${getEstadoColor(cama)}`}
onClick={() => setCamaSeleccionada(cama)}
>
<CardContent className="p-2 sm:p-4">
<div className="flex items-center justify-between mb-1 sm:mb-2">
<Bed className="h-4 w-4 sm:h-6 sm:w-6" />
{getEstadoIcono(cama)}
</div>
<p className="font-bold text-sm sm:text-lg">{cama.numero}</p>
<p className="text-xs opacity-75 truncate">{cama.areaId ? areaById[cama.areaId] ?? 'Área desconocida' : 'Sin área'}</p>
<Badge variant="outline" className="mt-1 sm:mt-2 text-xs bg-white/50 dark:bg-gray-800 dark:text-gray-300">
{cama.tipo}
</Badge>
{cama.estado === 'Ocupada' && paciente && (
<div className="mt-1 sm:mt-2 pt-1 sm:pt-2 border-t border-current/20">
<p className="text-xs font-medium truncate">
{paciente.apellido}, {paciente.nombre}
</p>
</div>
)}
</CardContent>
</Card>
</DialogTrigger>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle className="flex items-center gap-2">
<Bed className="h-5 w-5" />
Cama {cama.numero} - {cama.areaId ? areaById[cama.areaId] ?? 'Área desconocida' : 'Sin área'}
</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div className="flex items-center gap-2">
<Badge className={getEstadoColor(cama)}>
{cama.estado}
</Badge>
<Badge variant="outline">{cama.tipo}</Badge>
</div>
{cama.estado === 'Ocupada' && paciente && internacion && (
<div className="bg-gray-50 p-4 rounded-lg space-y-2">
<p className="font-medium">Paciente:</p>
<p className="text-lg">{paciente.apellido}, {paciente.nombre}</p>
<p className="text-sm text-gray-500">DNI: {paciente.dni}</p>
<p className="text-sm text-gray-500">Ingreso: {internacion.fechaIngresoClinica ? formatDateDDMMYYYY(internacion.fechaIngresoClinica) : 'N/A'}</p>
<p className="text-sm text-gray-500">Diagnóstico: {internacion.diagnosticoIngreso}</p>
<p className="text-sm text-gray-500">Médico: {internacion.medicoIngresante}</p>
</div>
)}
<div className="space-y-2">
<p className="font-medium">Acciones:</p>
<div className="grid grid-cols-1 sm:grid-cols-2 gap-2">
{cama.estado === 'Disponible' && canEditCama(cama.id) && (
<Dialog open={dialogoAbierto} onOpenChange={setDialogoAbierto}>
<DialogTrigger asChild>
<Button variant="outline"
className="w-full"
onClick={() => {
setCamaSeleccionada(cama);
setDialogoAbierto(true);
}}
>
<Plus className="h-4 w-4 mr-1" />
Ocupar
</Button>
</DialogTrigger>
<DialogContent className="sm:max-w-lg">
<DialogHeader>
<DialogTitle>Nueva Internación - Cama {cama.numero}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Paciente *</Label>
<Select value={pacienteSeleccionado} onValueChange={setPacienteSeleccionado}>
<SelectTrigger>
<SelectValue placeholder="Seleccionar paciente" />
</SelectTrigger>
<SelectContent>
{pacientesSinInternar.map(p => (
<SelectItem key={p.id} value={p.id}>
{p.apellido}, {p.nombre} - DNI: {p.dni}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Área de Trabajo *</Label>
<Select value={areaSeleccionada} onValueChange={setAreaSeleccionada}>
<SelectTrigger>
<SelectValue placeholder="Seleccionar área" />
</SelectTrigger>
<SelectContent>
{areas.filter(a => a.nombre !== 'Fuera de area').map(a => (
<SelectItem key={a.id} value={a.id}>
{a.nombre}
</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div>
<Label>Diagnóstico de Ingreso *</Label>
<textarea
className="w-full p-2 border rounded-md text-sm"
rows={3}
value={diagnostico}
onChange={(e) => setDiagnostico(e.target.value)}
placeholder="Ingrese el diagnóstico..."
/>
</div>
<div>
<Label>Enfermedad Actual *</Label>
<textarea
className="w-full p-2 border rounded-md text-sm"
rows={4}
value={enfermedadActual}
onChange={(e) => setEnfermedadActual(e.target.value)}
placeholder="Describa la enfermedad actual..."
/>
</div>
<div>
<Label>Antecedentes de Enfermedad Actual</Label>
<textarea
className="w-full p-2 border rounded-md text-sm"
rows={3}
value={antecedentesEnfermedadActual}
onChange={(e) => setAntecedentesEnfermedadActual(e.target.value)}
placeholder="Antecedentes relevantes..."
/>
</div>
<div>
<Label>Médico Tratante *</Label>
<input
type="text"
className="w-full p-2 border rounded-md text-sm"
value={medico}
onChange={(e) => setMedico(e.target.value)}
placeholder="Nombre del médico..."
/>
</div>
<Button variant="outline"
className="w-full"
onClick={handleOcuparCama}
disabled={!pacienteSeleccionado || !diagnostico || !enfermedadActual || !medico}
>
<Plus className="h-4 w-4 mr-1" />
Iniciar Internación
</Button>
</div>
</DialogContent>
</Dialog>
)}
{cama.estado !== 'Disponible' && canEditCama(cama.id) && (
<Button
variant="outline"
className="w-full"
onClick={() => handleCambiarEstado(cama, 'Disponible')}
>
<CheckCircle2 className="h-4 w-4 mr-1" />
Disponible
</Button>
)}
{cama.estado !== 'Reparacion' && canEditCama(cama.id) && (
<Button
variant="outline"
className="w-full"
onClick={() => handleCambiarEstado(cama, 'Reparacion')}
>
<Wrench className="h-4 w-4 mr-1" />
Reparacion
</Button>
)}
{cama.estado !== 'Reservada' && canEditCama(cama.id) && (
<Button
variant="outline"
className="w-full"
onClick={() => handleCambiarEstado(cama, 'Reservada')}
>
<Clock className="h-4 w-4 mr-1" />
Reservar
</Button>
)}
{canEditCama(cama.id) && (
<Button variant="outline" onClick={() => {
setEditingBed(cama);
setBedNumero(cama.numero);
setBedTipo(cama.tipo);
setBedAreaId(cama.areaId);
setBedDialogOpen(true);
}}>
<Edit2 className="h-4 w-4 mr-1" />Editar
</Button>
)}
{canEditCama(cama.id) && (
<Button variant="destructive" onClick={() => {
if (confirm('Eliminar esta cama?')) {
onEliminarCama(cama.id);
}
}}>
<Trash className="h-4 w-4 mr-1" />Eliminar
</Button>
)}
</div>
</div>
</div>
</DialogContent>
</Dialog>
);
})}
</div>
{/* Area dialog */}
<Dialog open={areaDialogOpen} onOpenChange={setAreaDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{editingAreaId ? 'Editar Área' : 'Nueva Área'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Nombre</Label>
<Input value={areaNombre} onChange={(e) => setAreaNombre(e.target.value)} />
</div>
<div className="flex gap-2 justify-end">
{editingAreaId && (
<Button size="sm" variant="destructive" onClick={() => { if (confirm('Eliminar área?')) { onEliminarArea(editingAreaId); setAreaDialogOpen(false); } }}>
<Trash className="h-4 w-4" />
</Button>
)}
<Button size="sm" variant="outline" onClick={() => setAreaDialogOpen(false)}>
<X className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" onClick={() => {
if (!areaNombre.trim()) return alert('Nombre requerido');
if (editingAreaId) onActualizarArea(editingAreaId, { nombre: areaNombre });
else onAgregarArea({ nombre: areaNombre });
setAreaDialogOpen(false);
}}>
<Save className="h-4 w-4" />
</Button>
</div>
<div>
<p className="text-sm font-medium">Áreas existentes</p>
<div className="mt-2 space-y-2">
{areas.map(a => (
<div key={a.id} className="flex items-center justify-between">
<div>{a.nombre}</div>
<div className="flex gap-1">
<Button size="sm" variant="outline" onClick={() => { setEditingAreaId(a.id); setAreaNombre(a.nombre); }}>
<Edit2 className="h-4 w-4" />
</Button>
<Button size="sm" variant="outline" className="text-red-600" onClick={() => { if (confirm('Eliminar área?')) onEliminarArea(a.id); }}>
<Trash className="h-4 w-4" />
</Button>
</div>
</div>
))}
</div>
</div>
</div>
</DialogContent>
</Dialog>
{/* Bed dialog */}
<Dialog open={bedDialogOpen} onOpenChange={setBedDialogOpen}>
<DialogContent className="sm:max-w-md">
<DialogHeader>
<DialogTitle>{editingBed ? `Editar Cama ${editingBed.numero}` : 'Nueva Cama'}</DialogTitle>
</DialogHeader>
<div className="space-y-4">
<div>
<Label>Número</Label>
<Input value={bedNumero} onChange={(e) => setBedNumero(e.target.value)} />
</div>
<div>
<Label>Tipo</Label>
<Select value={bedTipo} onValueChange={(v) => setBedTipo(v as Cama['tipo'])}>
<SelectTrigger>
<SelectValue />
</SelectTrigger>
<SelectContent>
<SelectItem value="General">General</SelectItem>
<SelectItem value="Aislamiento KPC">Aislamiento KPC</SelectItem>
<SelectItem value="Aislamiento COVID">Aislamiento COVID</SelectItem>
<SelectItem value="Aislamiento Clostridium">Aislamiento Clostridium</SelectItem>
<SelectItem value="Aislamiento Neutropenico">Aislamiento Neutropenico</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Label>Área</Label>
<Select value={bedAreaId ?? '__none'} onValueChange={(v) => setBedAreaId(v === '__none' ? undefined : v)}>
<SelectTrigger>
<SelectValue placeholder="Seleccione área" />
</SelectTrigger>
<SelectContent>
<SelectItem value="__none">Sin área</SelectItem>
{areas.map(a => (
<SelectItem key={a.id} value={a.id}>{a.nombre}</SelectItem>
))}
</SelectContent>
</Select>
</div>
<div className="flex gap-2 justify-end">
{editingBed && (
<Button variant="destructive" onClick={() => { if (confirm('Eliminar cama?')) { onEliminarCama(editingBed.id); setBedDialogOpen(false); } }}>
Eliminar
</Button>
)}
<Button variant="outline" onClick={() => setBedDialogOpen(false)}><X className="h-4 w-4 mr-2" />Cancelar</Button>
<Button variant="outline" onClick={() => {
if (!bedNumero.trim()) return alert('Número requerido');
if (editingBed) {
onActualizarCama(editingBed.id, { numero: bedNumero, tipo: bedTipo, areaId: bedAreaId });
} else {
onAgregarCama({ numero: bedNumero, tipo: bedTipo, estado: 'Disponible', areaId: bedAreaId });
}
setBedDialogOpen(false);
}}>Guardar</Button>
</div>
</div>
</DialogContent>
</Dialog>
{camasFiltradas.length === 0 && (
<div className="text-center py-12 text-gray-400">
<Bed className="h-16 w-16 mx-auto mb-4 opacity-50" />
<p className="text-lg">No se encontraron camas con los filtros seleccionados</p>
</div>
)}
</div>
);
}