Files
administracionHospital/src/sections/PendientesSala.tsx
T

506 lines
24 KiB
TypeScript

import { useState, useEffect } from 'react';
import {
ListTodo,
Search,
ArrowLeft,
Clock,
Bed,
Trash2,
FileText,
Copy,
CheckSquare,
Square
} 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 { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
import { Table, TableBody, TableCell, TableHead, TableHeader, TableRow } from '@/components/ui/table';
import { toast } from 'sonner';
import type { Pendiente, Internacion, Paciente, Cama } from '@/types';
import { formatDateDDMMYYYY, getNombreProfesional, getLocalToday } from '@/lib/utils';
import { useHospitalStore } from '@/hooks/useHospitalStore';
interface PendientesSalaProps {
pendientes: Pendiente[];
internaciones: Internacion[];
pacientes: Paciente[];
camas?: Cama[];
onActualizarPendiente: (id: string, datos: Partial<Pendiente>) => Promise<void> | void;
onEliminarPendiente: (id: string) => Promise<void> | void;
onVerHC: (internacionId: string) => void;
onVolver: () => void;
getPacienteById: (id: string) => Paciente | undefined;
getCamaById: (id: string) => Cama | undefined;
}
export function PendientesSala({
pendientes,
internaciones,
onActualizarPendiente,
onEliminarPendiente,
onVerHC,
onVolver,
getPacienteById,
getCamaById,
}: PendientesSalaProps) {
const { currentUser, refreshState } = useHospitalStore();
const [busqueda, setBusqueda] = useState('');
const [filtroCategoria, setFiltroCategoria] = useState<string>('todos');
const [filtroEstado, setFiltroEstado] = useState<string>('pendiente');
const [filtroFechaProgramada, setFiltroFechaProgramada] = useState<string>('');
useEffect(() => {
refreshState();
}, [refreshState]);
// Active internaciones map for quick lookup
const internacionesActivas = internaciones.filter(i => i.activa);
const activeInternacionMap = new Map<string, Internacion>();
internacionesActivas.forEach(i => {
if (i.id) activeInternacionMap.set(i.id, i);
if (i.pacienteId) activeInternacionMap.set(i.pacienteId, i);
});
// Filter pending items belonging to active interned patients (or all if activeInternacionMap is empty / fallback)
const pendientesSala = (pendientes || []).filter(p => {
if (!p) return false;
// If internacionId or pacienteId matches an active internacion, include it.
// Also if no internaciones match or if we want to be robust, check if patient has any active internacion.
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
// Fallback: if no explicit internacion matched but there are active internations, let's also check if patient is in active internaciones
if (!internacion && p.pacienteId) {
const foundByPaciente = internacionesActivas.find(i => i.pacienteId === p.pacienteId);
return !!foundByPaciente;
}
return !!internacion || (!p.internacionId && !p.pacienteId);
});
const totalPendientesActivos = pendientesSala.filter(p => p.estado === 'pendiente').length;
// Apply search, category, status, and scheduled date filters
const pendientesFiltrados = pendientesSala.filter(p => {
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined) ||
internacionesActivas.find(i => i.pacienteId === p.pacienteId);
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
const textoMatch = !busqueda ||
(paciente && (paciente.nombre.toLowerCase().includes(busqueda.toLowerCase()) || paciente.apellido.toLowerCase().includes(busqueda.toLowerCase()) || paciente.dni.includes(busqueda))) ||
(cama && cama.numero.toLowerCase().includes(busqueda.toLowerCase())) ||
p.descripcion.toLowerCase().includes(busqueda.toLowerCase()) ||
(p.observaciones && p.observaciones.toLowerCase().includes(busqueda.toLowerCase()));
const categoriaMatch = filtroCategoria === 'todos' || p.categoria === filtroCategoria;
const estadoMatch = filtroEstado === 'todos' || p.estado === filtroEstado;
// Filter by scheduled date (fechaProgramada), not creation date
const fechaProgMatch = !filtroFechaProgramada || p.fechaProgramada === filtroFechaProgramada;
return textoMatch && categoriaMatch && estadoMatch && fechaProgMatch;
}).sort((a, b) => {
// Sort by scheduled date if available, then creation date
const fechaA = a.fechaProgramada || a.fechaCreacion || '';
const fechaB = b.fechaProgramada || b.fechaCreacion || '';
return fechaB.localeCompare(fechaA);
});
const handleToggleEstado = async (p: Pendiente) => {
const nuevoEstado = p.estado === 'realizado' ? 'pendiente' : 'realizado';
try {
await onActualizarPendiente(p.id, {
estado: nuevoEstado,
fechaRealizado: nuevoEstado === 'realizado' ? getLocalToday() : 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 onEliminarPendiente(id);
toast.success('Pendiente eliminado');
} catch {
toast.error('Error al eliminar');
}
};
const handleCopiarPendientes = () => {
const activos = pendientesSala.filter(p => p.estado === 'pendiente');
if (activos.length === 0) {
toast.error('No hay pendientes activos en la sala');
return;
}
const texto = activos.map((p, idx) => {
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
const infoPaciente = paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente';
const infoCama = cama ? `[Cama ${cama.numero}]` : '';
let line = `${idx + 1}. ${infoCama} ${infoPaciente} - [${(p.categoria || 'General').toUpperCase()}] ${p.descripcion}`;
if (p.fechaProgramada) line += ` (Prog: ${formatDateDDMMYYYY(p.fechaProgramada)}${p.horaProgramada ? ' ' + p.horaProgramada : ''})`;
if (p.prioridad === 'alta') line += ' (ALTA)';
if (p.observaciones) line += ` - Obs: ${p.observaciones}`;
return line;
}).join('\n');
navigator.clipboard.writeText(`PENDIENTES DE SALA:\n${texto}`);
toast.success('Pendientes copiados al portapapeles');
};
return (
<div className="space-y-6 dark:text-white">
{/* Header */}
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-4">
<div className="flex items-center gap-3">
<Button variant="ghost" size="icon" onClick={onVolver} className="shrink-0">
<ArrowLeft className="h-5 w-5" />
</Button>
<div>
<h1 className="text-xl sm:text-2xl font-bold text-gray-900 dark:text-white flex items-center gap-2">
<ListTodo className="h-6 w-6 text-blue-600 dark:text-blue-400 shrink-0" />
Pendientes de Sala
</h1>
<p className="text-sm text-gray-500 dark:text-gray-400">
Gestión y seguimiento de pendientes de todos los pacientes internados ({totalPendientesActivos} pendientes activos)
</p>
</div>
</div>
<div className="flex items-center gap-2 w-full sm:w-auto justify-end">
<Button variant="outline" onClick={handleCopiarPendientes} className="gap-2 w-full sm:w-auto">
<Copy className="h-4 w-4" />
Copiar Pendientes Activos
</Button>
</div>
</div>
{/* Filters Card */}
<Card>
<CardContent className="p-4">
<div className="grid grid-cols-1 sm:grid-cols-2 lg:grid-cols-4 gap-3">
<div className="relative">
<Search className="absolute left-3 top-1/2 -translate-y-1/2 h-4 w-4 text-gray-400" />
<Input
className="pl-10 text-xs"
placeholder="Buscar paciente, cama o descripción..."
value={busqueda}
onChange={(e) => setBusqueda(e.target.value)}
/>
</div>
<div>
<Select value={filtroCategoria} onValueChange={setFiltroCategoria}>
<SelectTrigger className="h-9 text-xs">
<SelectValue placeholder="Categoría" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todas las categorías</SelectItem>
<SelectItem value="General">General</SelectItem>
<SelectItem value="Estudio">Estudio</SelectItem>
<SelectItem value="Procedimiento">Procedimiento</SelectItem>
<SelectItem value="Laboratorio">Laboratorio</SelectItem>
<SelectItem value="Interconsulta">Interconsulta</SelectItem>
<SelectItem value="Tratamiento">Tratamiento</SelectItem>
<SelectItem value="Administrativo">Administrativo</SelectItem>
</SelectContent>
</Select>
</div>
<div>
<Select value={filtroEstado} onValueChange={setFiltroEstado}>
<SelectTrigger className="h-9 text-xs">
<SelectValue placeholder="Estado" />
</SelectTrigger>
<SelectContent>
<SelectItem value="todos">Todos los estados</SelectItem>
<SelectItem value="pendiente">Pendientes</SelectItem>
<SelectItem value="realizado">Realizados</SelectItem>
<SelectItem value="cancelado">Cancelados</SelectItem>
</SelectContent>
</Select>
</div>
<div className="flex items-center gap-2">
<div className="relative flex-1">
<Input
type="date"
className="h-9 text-xs"
value={filtroFechaProgramada}
onChange={(e) => setFiltroFechaProgramada(e.target.value)}
placeholder="Fecha programada"
/>
</div>
{filtroFechaProgramada && (
<Button variant="ghost" size="sm" onClick={() => setFiltroFechaProgramada('')} className="h-9 px-2 text-xs text-muted-foreground">
Limpiar
</Button>
)}
</div>
</div>
</CardContent>
</Card>
{/* Lista de Pendientes en Tarjetas (móvil) y Tabla (escritorio) */}
{pendientesFiltrados.length === 0 ? (
<div className="text-center py-12 text-gray-400 bg-card rounded-lg border p-8">
<ListTodo className="h-16 w-16 mx-auto mb-4 opacity-40 text-blue-500" />
<p className="text-lg font-medium text-gray-700 dark:text-gray-300">No se encontraron pendientes</p>
<p className="text-sm text-gray-500 mt-1">Pruebe cambiando los filtros de búsqueda o fecha programada.</p>
</div>
) : (
<>
{/* Vista móvil en Tarjetas (block sm:hidden) */}
<div className="space-y-3 sm:hidden">
{pendientesFiltrados.map((p) => {
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
const esRealizado = p.estado === 'realizado';
return (
<div
key={p.id}
className={`p-4 rounded-lg border bg-card shadow-sm space-y-3 transition-colors ${
esRealizado ? 'opacity-60 bg-muted/20 border-muted' : 'border-border'
}`}
>
<div className="flex items-center justify-between gap-2">
<div className="flex items-center gap-2 flex-wrap">
<div className="flex items-center gap-1 font-semibold text-xs bg-blue-50 dark:bg-blue-950/40 text-blue-700 dark:text-blue-300 px-2 py-1 rounded">
<Bed className="h-3.5 w-3.5" />
<span>{cama ? `Cama ${cama.numero}` : 'Sin cama'}</span>
</div>
<Badge variant="outline" className="text-xs font-medium">
{p.categoria || 'General'}
</Badge>
{p.prioridad === 'alta' ? (
<Badge className="bg-red-100 text-red-800 dark:bg-red-950/80 dark:text-red-300 border-red-200 text-[11px]">
Alta
</Badge>
) : p.prioridad === 'media' ? (
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300 border-amber-200 text-[11px]">
Media
</Badge>
) : (
<Badge variant="outline" className="text-[11px]">
Baja
</Badge>
)}
</div>
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0 shrink-0"
onClick={() => handleToggleEstado(p)}
title={esRealizado ? "Marcar como pendiente" : "Marcar como realizado"}
>
{esRealizado ? (
<CheckSquare className="h-5 w-5 text-emerald-600" />
) : (
<Square className="h-5 w-5 text-gray-400 hover:text-blue-600" />
)}
</Button>
</div>
<div>
<div className="font-semibold text-sm text-gray-900 dark:text-white">
{paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente desconocido'}
</div>
<div className="text-xs text-muted-foreground font-mono">
DNI: {paciente?.dni || 'N/A'}
</div>
</div>
<div className="bg-muted/30 p-2.5 rounded text-xs space-y-1">
<div className={`font-medium ${esRealizado ? 'line-through text-muted-foreground' : 'text-gray-800 dark:text-gray-200'}`}>
{p.descripcion}
</div>
{p.observaciones && (
<div className="text-[11px] text-muted-foreground">
<span className="font-semibold">Obs:</span> {p.observaciones}
</div>
)}
</div>
<div className="flex items-center justify-between pt-1 border-t border-border/50 text-xs">
<div>
{p.fechaProgramada ? (
<div className="flex items-center gap-1 text-blue-700 dark:text-blue-300 font-medium">
<Clock className="h-3 w-3 shrink-0" />
<span>{formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada}` : ''}</span>
</div>
) : (
<span className="text-muted-foreground italic text-[11px]">Sin programar</span>
)}
</div>
<div className="flex items-center gap-1">
{internacion && (
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-xs text-blue-600 hover:text-blue-700 hover:bg-blue-50 gap-1"
onClick={() => onVerHC(internacion.id)}
>
<FileText className="h-3.5 w-3.5" />
<span>HC</span>
</Button>
)}
<Button
size="sm"
variant="ghost"
className="h-7 px-2 text-xs text-red-600 hover:text-red-700 hover:bg-red-50 gap-1"
onClick={() => handleEliminar(p.id)}
>
<Trash2 className="h-3.5 w-3.5" />
<span>Eliminar</span>
</Button>
</div>
</div>
</div>
);
})}
</div>
{/* Vista de Tabla para Escritorio (hidden sm:block) */}
<div className="hidden sm:block border rounded-lg bg-card overflow-hidden shadow-sm">
<div className="overflow-x-auto">
<Table className="w-full min-w-[1000px] text-sm">
<TableHeader>
<TableRow>
<TableHead className="w-[50px] text-center">Estado</TableHead>
<TableHead className="w-[80px]">Cama</TableHead>
<TableHead>Paciente</TableHead>
<TableHead className="w-[120px]">Categoría</TableHead>
<TableHead className="w-[100px]">Prioridad</TableHead>
<TableHead>Descripción / Observaciones</TableHead>
<TableHead className="w-[140px]">Fecha Programada</TableHead>
<TableHead className="text-center w-[100px]">Acciones</TableHead>
</TableRow>
</TableHeader>
<TableBody>
{pendientesFiltrados.map((p) => {
const internacion = (p.internacionId ? activeInternacionMap.get(p.internacionId) : undefined) ||
(p.pacienteId ? activeInternacionMap.get(p.pacienteId) : undefined);
const paciente = p.pacienteId ? getPacienteById(p.pacienteId) : undefined;
const cama = internacion ? getCamaById(internacion.camaId) : undefined;
const esRealizado = p.estado === 'realizado';
return (
<TableRow key={p.id} className={`hover:bg-muted/50 ${esRealizado ? 'opacity-60 bg-muted/20' : ''}`}>
<TableCell className="text-center">
<Button
variant="ghost"
size="sm"
className="h-8 w-8 p-0"
onClick={() => handleToggleEstado(p)}
title={esRealizado ? "Marcar como pendiente" : "Marcar como realizado"}
>
{esRealizado ? (
<CheckSquare className="h-5 w-5 text-emerald-600" />
) : (
<Square className="h-5 w-5 text-gray-400 hover:text-blue-600" />
)}
</Button>
</TableCell>
<TableCell className="font-semibold text-xs whitespace-nowrap">
<div className="flex items-center gap-1.5">
<Bed className="h-3.5 w-3.5 text-blue-600" />
<span>{cama ? cama.numero : '-'}</span>
</div>
</TableCell>
<TableCell className="whitespace-nowrap">
<div className="font-medium text-xs">
{paciente ? `${paciente.apellido}, ${paciente.nombre}` : 'Paciente desconocido'}
</div>
<div className="text-[11px] text-muted-foreground font-mono">
DNI: {paciente?.dni || 'N/A'}
</div>
</TableCell>
<TableCell className="whitespace-nowrap">
<Badge variant="outline" className="text-xs font-medium">
{p.categoria || 'General'}
</Badge>
</TableCell>
<TableCell className="whitespace-nowrap">
{p.prioridad === 'alta' ? (
<Badge className="bg-red-100 text-red-800 dark:bg-red-950/80 dark:text-red-300 border-red-200 text-[11px]">
Alta
</Badge>
) : p.prioridad === 'media' ? (
<Badge className="bg-amber-100 text-amber-800 dark:bg-amber-950/80 dark:text-amber-300 border-amber-200 text-[11px]">
Media
</Badge>
) : (
<Badge variant="outline" className="text-[11px]">
Baja
</Badge>
)}
</TableCell>
<TableCell>
<div className={`text-xs ${esRealizado ? 'line-through text-muted-foreground' : 'font-medium'}`}>
{p.descripcion}
</div>
{p.observaciones && (
<div className="text-[11px] text-muted-foreground mt-0.5">
Obs: {p.observaciones}
</div>
)}
</TableCell>
<TableCell className="whitespace-nowrap text-xs">
{p.fechaProgramada ? (
<div className="flex items-center gap-1 text-blue-700 dark:text-blue-300 font-medium bg-blue-50 dark:bg-blue-950/40 px-2 py-1 rounded w-fit">
<Clock className="h-3 w-3" />
<span>{formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada}` : ''}</span>
</div>
) : (
<span className="text-muted-foreground italic text-[11px]">Sin programar</span>
)}
</TableCell>
<TableCell className="text-center">
<div className="flex items-center justify-center gap-1">
{internacion && (
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-blue-600 hover:text-blue-700 hover:bg-blue-50"
onClick={() => onVerHC(internacion.id)}
title="Ver Historia Clínica"
>
<FileText className="h-3.5 w-3.5" />
</Button>
)}
<Button
size="icon"
variant="ghost"
className="h-7 w-7 text-red-600 hover:text-red-700 hover:bg-red-50"
onClick={() => handleEliminar(p.id)}
title="Eliminar pendiente"
>
<Trash2 className="h-3.5 w-3.5" />
</Button>
</div>
</TableCell>
</TableRow>
);
})}
</TableBody>
</Table>
</div>
</div>
</>
)}
</div>
);
}