Agregar tab de pendientes, categoria procedimiento y fecha/hora programada
This commit is contained in:
@@ -16,6 +16,7 @@ import { initDb,
|
|||||||
getAllAtb, createAtb, updateAtb, deleteAtb,
|
getAllAtb, createAtb, updateAtb, deleteAtb,
|
||||||
getAllIndicaciones, createIndicacion, updateIndicacion, deleteIndicacion,
|
getAllIndicaciones, createIndicacion, updateIndicacion, deleteIndicacion,
|
||||||
getAllMovimientosIndicaciones, createMovimientoIndicacion,
|
getAllMovimientosIndicaciones, createMovimientoIndicacion,
|
||||||
|
getAllPendientes, createPendiente, updatePendiente, deletePendiente,
|
||||||
getValue, setValue
|
getValue, setValue
|
||||||
} from './db-mongodb.js';
|
} from './db-mongodb.js';
|
||||||
|
|
||||||
@@ -63,6 +64,7 @@ app.get('/api/state', async (req, res) => {
|
|||||||
atb: await getAllAtb(),
|
atb: await getAllAtb(),
|
||||||
indicaciones: await getAllIndicaciones(),
|
indicaciones: await getAllIndicaciones(),
|
||||||
movimientosIndicaciones: await getAllMovimientosIndicaciones(),
|
movimientosIndicaciones: await getAllMovimientosIndicaciones(),
|
||||||
|
pendientes: await getAllPendientes(),
|
||||||
usuarios: usuariosList.map(({ passwordHash, ...u }) => u),
|
usuarios: usuariosList.map(({ passwordHash, ...u }) => u),
|
||||||
vistaActual: 'dashboard',
|
vistaActual: 'dashboard',
|
||||||
currentInternacionId: null
|
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 ==========
|
// ========== AUTH ==========
|
||||||
app.post('/api/auth/login', async (req, res) => {
|
app.post('/api/auth/login', async (req, res) => {
|
||||||
try {
|
try {
|
||||||
|
|||||||
@@ -64,6 +64,7 @@ const memStore = {
|
|||||||
atb: [],
|
atb: [],
|
||||||
indicaciones: [],
|
indicaciones: [],
|
||||||
movimientos_indicaciones: [],
|
movimientos_indicaciones: [],
|
||||||
|
pendientes: [],
|
||||||
kv: {}
|
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 ==========
|
// ========== KV STORE ==========
|
||||||
export async function getValue(key) {
|
export async function getValue(key) {
|
||||||
if (db) {
|
if (db) {
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ import type {
|
|||||||
ATB,
|
ATB,
|
||||||
Indicacion,
|
Indicacion,
|
||||||
MovimientoIndicacion,
|
MovimientoIndicacion,
|
||||||
|
Pendiente,
|
||||||
Vista,
|
Vista,
|
||||||
Usuario
|
Usuario
|
||||||
} from '@/types';
|
} from '@/types';
|
||||||
@@ -48,6 +49,7 @@ interface HospitalState {
|
|||||||
atb: ATB[];
|
atb: ATB[];
|
||||||
indicaciones: Indicacion[];
|
indicaciones: Indicacion[];
|
||||||
movimientosIndicaciones: MovimientoIndicacion[];
|
movimientosIndicaciones: MovimientoIndicacion[];
|
||||||
|
pendientes: Pendiente[];
|
||||||
vistaActual: Vista;
|
vistaActual: Vista;
|
||||||
currentInternacionId?: string | null;
|
currentInternacionId?: string | null;
|
||||||
usuarios: Usuario[];
|
usuarios: Usuario[];
|
||||||
@@ -73,6 +75,7 @@ const defaultState = (): HospitalState => ({
|
|||||||
vistaActual: 'dashboard',
|
vistaActual: 'dashboard',
|
||||||
indicaciones: [],
|
indicaciones: [],
|
||||||
movimientosIndicaciones: [],
|
movimientosIndicaciones: [],
|
||||||
|
pendientes: [],
|
||||||
camas: [],
|
camas: [],
|
||||||
usuarios: [],
|
usuarios: [],
|
||||||
currentUser: null,
|
currentUser: null,
|
||||||
@@ -104,6 +107,7 @@ export function useHospitalStore() {
|
|||||||
atb: body.atb || [],
|
atb: body.atb || [],
|
||||||
glucemias: body.glucemias || [],
|
glucemias: body.glucemias || [],
|
||||||
movimientosIndicaciones: body.movimientosIndicaciones || [],
|
movimientosIndicaciones: body.movimientosIndicaciones || [],
|
||||||
|
pendientes: body.pendientes || [],
|
||||||
currentInternacionId: storedInternacionId || body.currentInternacionId || null,
|
currentInternacionId: storedInternacionId || body.currentInternacionId || null,
|
||||||
currentUser,
|
currentUser,
|
||||||
isAuthenticated: !!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());
|
.sort((a, b) => new Date(b.fechaToma).getTime() - new Date(a.fechaToma).getTime());
|
||||||
}, [state.cultivos]);
|
}, [state.cultivos]);
|
||||||
|
|
||||||
|
const agregarPendiente = useCallback(async (datos: Omit<Pendiente, 'id'>) => {
|
||||||
|
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<Pendiente>) => {
|
||||||
|
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 getEstadisticas = useCallback(() => {
|
||||||
const camasEnArea = state.camas.filter(c => !isCamaFueraDeGrupo(c));
|
const camasEnArea = state.camas.filter(c => !isCamaFueraDeGrupo(c));
|
||||||
const camasFueraDeArea = 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,
|
getGlucemiasByInternacion,
|
||||||
getAcidosBaseByInternacion,
|
getAcidosBaseByInternacion,
|
||||||
getCultivosByInternacion,
|
getCultivosByInternacion,
|
||||||
|
agregarPendiente,
|
||||||
|
actualizarPendiente,
|
||||||
|
eliminarPendiente,
|
||||||
|
getPendientesByPaciente,
|
||||||
setCurrentInternacion,
|
setCurrentInternacion,
|
||||||
getEstadisticas,
|
getEstadisticas,
|
||||||
login,
|
login,
|
||||||
|
|||||||
@@ -37,13 +37,20 @@ import {
|
|||||||
TrendingUp,
|
TrendingUp,
|
||||||
Activity,
|
Activity,
|
||||||
Pill,
|
Pill,
|
||||||
Users
|
Users,
|
||||||
|
ListTodo,
|
||||||
|
CheckSquare,
|
||||||
|
Square,
|
||||||
|
Copy,
|
||||||
|
Clock,
|
||||||
|
Search
|
||||||
} from 'lucide-react';
|
} from 'lucide-react';
|
||||||
import { Card, CardContent } from '@/components/ui/card';
|
import { Card, CardContent } from '@/components/ui/card';
|
||||||
import { Badge } from '@/components/ui/badge';
|
import { Badge } from '@/components/ui/badge';
|
||||||
import { Button } from '@/components/ui/button';
|
import { Button } from '@/components/ui/button';
|
||||||
import { Input } from '@/components/ui/input';
|
import { Input } from '@/components/ui/input';
|
||||||
import { Label } from '@/components/ui/label';
|
import { Label } from '@/components/ui/label';
|
||||||
|
import { Textarea } from '@/components/ui/textarea';
|
||||||
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
import { Dialog, DialogContent, DialogHeader, DialogTitle } from '@/components/ui/dialog';
|
||||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@/components/ui/select';
|
||||||
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
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 { ScrollArea, ScrollBar } from "@/components/ui/scroll-area"
|
||||||
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuTrigger } from '@/components/ui/dropdown-menu';
|
||||||
import { LineChart, Line, XAxis, YAxis, CartesianGrid, Tooltip, ResponsiveContainer } from 'recharts';
|
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 { formatDateDDMMYYYY } from '@/lib/utils';
|
||||||
import { SeccionIndicaciones } from './SeccionIndicaciones';
|
import { SeccionIndicaciones } from './SeccionIndicaciones';
|
||||||
|
|
||||||
@@ -241,7 +248,9 @@ export function HistoriaClinica({
|
|||||||
canEdit,
|
canEdit,
|
||||||
}: HistoriaClinicaProps) {
|
}: HistoriaClinicaProps) {
|
||||||
const [tabActivo, setTabActivo] = useState('evoluciones');
|
const [tabActivo, setTabActivo] = useState('evoluciones');
|
||||||
|
const { pendientes } = useHospitalStore();
|
||||||
const indicacionesActivas = (indicadores || []).filter(i => i.estado === 'Activa');
|
const indicacionesActivas = (indicadores || []).filter(i => i.estado === 'Activa');
|
||||||
|
const pendientesActivos = (pendientes || []).filter(p => p.pacienteId === paciente.id && p.estado === 'pendiente');
|
||||||
|
|
||||||
|
|
||||||
const calcularEdad = (fechaNacimiento: string) => {
|
const calcularEdad = (fechaNacimiento: string) => {
|
||||||
@@ -554,6 +563,11 @@ export function HistoriaClinica({
|
|||||||
<span>Interconsultas</span>
|
<span>Interconsultas</span>
|
||||||
({interconsultas.length})
|
({interconsultas.length})
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
|
<TabsTrigger value="pendientes">
|
||||||
|
<ListTodo className="h-3 w-3 sm:h-4 sm:w-4" />
|
||||||
|
<span>Pendientes</span>
|
||||||
|
({pendientesActivos.length})
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
<ScrollBar orientation="horizontal" />
|
<ScrollBar orientation="horizontal" />
|
||||||
</ScrollArea>
|
</ScrollArea>
|
||||||
@@ -630,6 +644,14 @@ export function HistoriaClinica({
|
|||||||
canEdit={canEdit}
|
canEdit={canEdit}
|
||||||
/>
|
/>
|
||||||
</TabsContent>
|
</TabsContent>
|
||||||
|
|
||||||
|
<TabsContent value="pendientes" className="mt-4">
|
||||||
|
<SeccionPendientes
|
||||||
|
pacienteId={paciente.id}
|
||||||
|
internacionId={internacion.id}
|
||||||
|
canEdit={canEdit}
|
||||||
|
/>
|
||||||
|
</TabsContent>
|
||||||
</Tabs>
|
</Tabs>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -2985,3 +3007,422 @@ function SeccionATB({ atb, internacionId, pacienteId, add, update, del, canEdit
|
|||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
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<Pendiente | null>(null);
|
||||||
|
|
||||||
|
// Form fields
|
||||||
|
const [descripcion, setDescripcion] = useState('');
|
||||||
|
const [categoria, setCategoria] = useState<Pendiente['categoria']>('General');
|
||||||
|
const [prioridad, setPrioridad] = useState<Pendiente['prioridad']>('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<string>('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<Pendiente> = {
|
||||||
|
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 <Badge className="bg-red-100 text-red-800 dark:bg-red-900/60 dark:text-red-200 border-red-200">Alta</Badge>;
|
||||||
|
case 'media':
|
||||||
|
return <Badge className="bg-amber-100 text-amber-800 dark:bg-amber-900/60 dark:text-amber-200 border-amber-200">Media</Badge>;
|
||||||
|
case 'baja':
|
||||||
|
return <Badge className="bg-blue-100 text-blue-800 dark:bg-blue-900/60 dark:text-blue-200 border-blue-200">Baja</Badge>;
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const getCategoryBadge = (cat: Pendiente['categoria']) => {
|
||||||
|
return <Badge variant="outline" className="text-xs">{cat}</Badge>;
|
||||||
|
};
|
||||||
|
|
||||||
|
return (
|
||||||
|
<div className="space-y-4">
|
||||||
|
{/* Header controls */}
|
||||||
|
<div className="flex flex-col sm:flex-row items-start sm:items-center justify-between gap-3 bg-card p-4 rounded-lg border shadow-sm">
|
||||||
|
<div className="flex flex-wrap items-center gap-2 w-full sm:w-auto">
|
||||||
|
<div className="relative flex-1 sm:w-60">
|
||||||
|
<Search className="absolute left-2.5 top-2.5 h-4 w-4 text-muted-foreground" />
|
||||||
|
<Input
|
||||||
|
placeholder="Buscar pendientes..."
|
||||||
|
value={busqueda}
|
||||||
|
onChange={e => setBusqueda(e.target.value)}
|
||||||
|
className="pl-8 h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<Select value={filtroEstado} onValueChange={(val: 'todos' | 'pendiente' | 'realizado' | 'cancelado') => setFiltroEstado(val)}>
|
||||||
|
<SelectTrigger className="w-[130px] h-9 text-xs">
|
||||||
|
<SelectValue placeholder="Estado" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="pendiente">Pendientes</SelectItem>
|
||||||
|
<SelectItem value="realizado">Realizados</SelectItem>
|
||||||
|
<SelectItem value="cancelado">Cancelados</SelectItem>
|
||||||
|
<SelectItem value="todos">Todos</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
|
||||||
|
<Select value={filtroCategoria} onValueChange={setFiltroCategoria}>
|
||||||
|
<SelectTrigger className="w-[150px] h-9 text-xs">
|
||||||
|
<SelectValue placeholder="Categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="todas">Todas las cat.</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 className="flex items-center gap-2 w-full sm:w-auto justify-end">
|
||||||
|
<Button variant="outline" size="sm" onClick={handleCopiarPendientes} title="Copiar pendientes activos">
|
||||||
|
<Copy className="h-4 w-4 mr-1.5" />
|
||||||
|
Copiar
|
||||||
|
</Button>
|
||||||
|
{canEdit && (
|
||||||
|
<Button size="sm" onClick={() => { resetForm(); setModalAbierto(true); }}>
|
||||||
|
<Plus className="h-4 w-4 mr-1.5" />
|
||||||
|
Nuevo Pendiente
|
||||||
|
</Button>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{/* List of Pendientes */}
|
||||||
|
{pendientesFiltrados.length === 0 ? (
|
||||||
|
<Card className="p-8 text-center text-muted-foreground border-dashed">
|
||||||
|
<ListTodo className="h-10 w-10 mx-auto mb-2 opacity-40" />
|
||||||
|
<p className="font-medium">No se encontraron registros de pendientes</p>
|
||||||
|
<p className="text-xs mt-1">
|
||||||
|
{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.'}
|
||||||
|
</p>
|
||||||
|
</Card>
|
||||||
|
) : (
|
||||||
|
<div className="space-y-2.5">
|
||||||
|
{pendientesFiltrados.map(p => (
|
||||||
|
<Card
|
||||||
|
key={p.id}
|
||||||
|
className={`transition-colors ${
|
||||||
|
p.estado === 'realizado' ? 'bg-muted/30 border-muted opacity-75' : 'hover:border-primary/50'
|
||||||
|
}`}
|
||||||
|
>
|
||||||
|
<CardContent className="p-4 flex items-start gap-3">
|
||||||
|
{canEdit && (
|
||||||
|
<button
|
||||||
|
onClick={() => handleToggleEstado(p)}
|
||||||
|
className="mt-0.5 text-muted-foreground hover:text-primary transition-colors focus:outline-none"
|
||||||
|
title={p.estado === 'pendiente' ? 'Marcar como realizado' : 'Marcar como pendiente'}
|
||||||
|
>
|
||||||
|
{p.estado === 'realizado' ? (
|
||||||
|
<CheckSquare className="h-5 w-5 text-emerald-600 dark:text-emerald-400" />
|
||||||
|
) : (
|
||||||
|
<Square className="h-5 w-5" />
|
||||||
|
)}
|
||||||
|
</button>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex-1 min-w-0 space-y-1">
|
||||||
|
<div className="flex flex-wrap items-center gap-2">
|
||||||
|
<span className={`font-medium text-sm ${p.estado === 'realizado' ? 'line-through text-muted-foreground' : ''}`}>
|
||||||
|
{p.descripcion}
|
||||||
|
</span>
|
||||||
|
{getPriorityBadge(p.prioridad)}
|
||||||
|
{getCategoryBadge(p.categoria)}
|
||||||
|
{p.estado === 'realizado' && (
|
||||||
|
<Badge className="bg-emerald-100 text-emerald-800 dark:bg-emerald-900/60 dark:text-emerald-200">
|
||||||
|
Realizado
|
||||||
|
</Badge>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{p.fechaProgramada && (
|
||||||
|
<div className="flex items-center gap-1.5 text-xs font-semibold text-blue-700 dark:text-blue-300 bg-blue-50 dark:bg-blue-950/40 px-2 py-1 rounded w-fit">
|
||||||
|
<Clock className="h-3.5 w-3.5" />
|
||||||
|
Programado: {formatDateDDMMYYYY(p.fechaProgramada)} {p.horaProgramada ? `a las ${p.horaProgramada} hs` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{p.observaciones && (
|
||||||
|
<p className="text-xs text-muted-foreground bg-muted/40 p-2 rounded border mt-1">
|
||||||
|
{p.observaciones}
|
||||||
|
</p>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div className="flex flex-wrap items-center gap-3 text-[11px] text-muted-foreground pt-1">
|
||||||
|
<span className="flex items-center gap-1">
|
||||||
|
<Clock className="h-3 w-3" />
|
||||||
|
Creado: {formatDateDDMMYYYY(p.fechaCreacion)} {p.horaCreacion || ''}
|
||||||
|
</span>
|
||||||
|
{p.profesional && <span>Por: {p.profesional}</span>}
|
||||||
|
{p.fechaRealizado && (
|
||||||
|
<span className="text-emerald-600 dark:text-emerald-400 font-medium">
|
||||||
|
Realizado el {formatDateDDMMYYYY(p.fechaRealizado)} {p.usuarioRealizado ? `por ${p.usuarioRealizado}` : ''}
|
||||||
|
</span>
|
||||||
|
)}
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{canEdit && (
|
||||||
|
<div className="flex items-center gap-1">
|
||||||
|
<Button size="icon" variant="ghost" className="h-7 w-7" onClick={() => openEditar(p)} title="Editar">
|
||||||
|
<Pencil 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 dark:hover:bg-red-950/40"
|
||||||
|
onClick={() => handleEliminar(p.id)}
|
||||||
|
title="Eliminar pendiente"
|
||||||
|
>
|
||||||
|
<Trash2 className="h-3.5 w-3.5" />
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
</CardContent>
|
||||||
|
</Card>
|
||||||
|
))}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
{/* Modal Crear/Editar */}
|
||||||
|
<Dialog open={modalAbierto} onOpenChange={setModalAbierto}>
|
||||||
|
<DialogContent className="sm:max-w-lg">
|
||||||
|
<DialogHeader>
|
||||||
|
<DialogTitle className="flex items-center gap-2">
|
||||||
|
<ListTodo className="h-5 w-5 text-blue-600" />
|
||||||
|
{pendienteEditar ? 'Editar Pendiente' : 'Nuevo Pendiente de Paciente'}
|
||||||
|
</DialogTitle>
|
||||||
|
</DialogHeader>
|
||||||
|
|
||||||
|
<div className="space-y-4 pt-2">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">Descripción del Pendiente *</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Ej: Solicitar ecografía abdominal, Chequear laboratorio de control, Pendiente interconsulta con Cardiología..."
|
||||||
|
value={descripcion}
|
||||||
|
onChange={e => setDescripcion(e.target.value)}
|
||||||
|
rows={3}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="grid grid-cols-2 gap-3">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">Categoría</Label>
|
||||||
|
<Select value={categoria} onValueChange={(val: Pendiente['categoria']) => setCategoria(val)}>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="Categoría" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<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>
|
||||||
|
<Label className="text-xs font-medium">Prioridad</Label>
|
||||||
|
<Select value={prioridad} onValueChange={(val: Pendiente['prioridad']) => setPrioridad(val)}>
|
||||||
|
<SelectTrigger className="mt-1">
|
||||||
|
<SelectValue placeholder="Prioridad" />
|
||||||
|
</SelectTrigger>
|
||||||
|
<SelectContent>
|
||||||
|
<SelectItem value="alta">Alta</SelectItem>
|
||||||
|
<SelectItem value="media">Media</SelectItem>
|
||||||
|
<SelectItem value="baja">Baja</SelectItem>
|
||||||
|
</SelectContent>
|
||||||
|
</Select>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
{(categoria === 'Estudio' || categoria === 'Procedimiento') && (
|
||||||
|
<div className="grid grid-cols-2 gap-3 p-3 bg-muted/30 rounded-lg border">
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">Fecha Programada</Label>
|
||||||
|
<Input
|
||||||
|
type="date"
|
||||||
|
value={fechaProgramada}
|
||||||
|
onChange={e => setFechaProgramada(e.target.value)}
|
||||||
|
className="mt-1 h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">Hora Programada</Label>
|
||||||
|
<Input
|
||||||
|
type="time"
|
||||||
|
value={horaProgramada}
|
||||||
|
onChange={e => setHoraProgramada(e.target.value)}
|
||||||
|
className="mt-1 h-9 text-xs"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
|
<div>
|
||||||
|
<Label className="text-xs font-medium">Observaciones / Detalles (opcional)</Label>
|
||||||
|
<Textarea
|
||||||
|
placeholder="Detalles adicionales, indicaciones específicas..."
|
||||||
|
value={observaciones}
|
||||||
|
onChange={e => setObservaciones(e.target.value)}
|
||||||
|
rows={2}
|
||||||
|
className="mt-1"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<div className="flex justify-end gap-2 mt-4 pt-2 border-t">
|
||||||
|
<Button variant="outline" onClick={() => setModalAbierto(false)}>
|
||||||
|
Cancelar
|
||||||
|
</Button>
|
||||||
|
<Button onClick={handleGuardar}>
|
||||||
|
<Save className="h-4 w-4 mr-1.5" />
|
||||||
|
Guardar
|
||||||
|
</Button>
|
||||||
|
</div>
|
||||||
|
</DialogContent>
|
||||||
|
</Dialog>
|
||||||
|
</div>
|
||||||
|
);
|
||||||
|
}
|
||||||
@@ -254,3 +254,21 @@ export interface Usuario {
|
|||||||
areaId?: string;
|
areaId?: string;
|
||||||
fechaCreacion: string;
|
fechaCreacion: string;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export interface Pendiente {
|
||||||
|
id: string;
|
||||||
|
pacienteId: string;
|
||||||
|
internacionId?: string;
|
||||||
|
descripcion: string;
|
||||||
|
prioridad: 'alta' | 'media' | 'baja';
|
||||||
|
estado: 'pendiente' | 'realizado' | 'cancelado';
|
||||||
|
fechaCreacion: string;
|
||||||
|
horaCreacion?: string;
|
||||||
|
fechaRealizado?: string;
|
||||||
|
categoria?: 'General' | 'Estudio' | 'Procedimiento' | 'Laboratorio' | 'Interconsulta' | 'Tratamiento' | 'Administrativo' | string;
|
||||||
|
fechaProgramada?: string;
|
||||||
|
horaProgramada?: string;
|
||||||
|
usuarioId?: string;
|
||||||
|
usuarioNombre?: string;
|
||||||
|
observaciones?: string;
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user