feat: Agregar modulo de Sistema (mongosh, logs, export/import)

This commit is contained in:
2026-08-12 03:17:28 +00:00
parent e038c25e9e
commit 205c8f1c7c
6 changed files with 450 additions and 3 deletions
+11
View File
@@ -14,6 +14,7 @@ import { EditIngreso } from '@/sections/EditIngreso';
import { Login } from '@/sections/Login';
import { GestionUsuarios } from '@/sections/GestionUsuarios';
import { PendientesSala } from '@/sections/PendientesSala';
import { SeccionSistema } from '@/sections/SeccionSistema';
import { Spinner } from '@/components/ui/spinner';
import { Button } from '@/components/ui/button';
import { ThemeProvider } from "@/components/theme-provider";
@@ -306,6 +307,16 @@ function AppContent() {
return (
<GestionUsuarios />
);
case 'sistema':
if (store.currentUser?.rol !== 'admin') {
return (
<div className="p-8 text-center text-red-500 font-semibold">
<p>No tiene permisos para acceder a esta sección.</p>
<Button className="mt-4" onClick={() => store.setVista('dashboard')}>Volver al Dashboard</Button>
</div>
);
}
return <SeccionSistema />;
default:
return null;
}
+3 -1
View File
@@ -11,7 +11,8 @@ import {
Sun,
Moon,
LogOut,
UserCog
UserCog,
Terminal
} from 'lucide-react';
import { Button } from '@/components/ui/button';
import { Sheet, SheetContent, SheetTrigger, SheetTitle, SheetDescription, SheetHeader } from '@/components/ui/sheet';
@@ -59,6 +60,7 @@ export function Layout({ children, vistaActual, onCambiarVista, currentUser, onL
if ((currentUser?.rol as string) === 'admin') {
filteredMenuItems.push({ vista: 'usuarios', label: 'Usuarios', icon: UserCog });
filteredMenuItems.push({ vista: 'sistema', label: 'Sistema', icon: Terminal });
}
const renderNavContent = (onItemClick?: () => void) => (
+251
View File
@@ -0,0 +1,251 @@
import { useState, useRef, useEffect } from 'react';
import { Card, CardContent, CardHeader, CardTitle, CardDescription } from '@/components/ui/card';
import { Button } from '@/components/ui/button';
import { Terminal, Download, Upload, FileText, Send, AlertCircle, RefreshCw } from 'lucide-react';
import { toast } from 'sonner';
export function SeccionSistema() {
const [mongoshCommand, setMongoshCommand] = useState('');
const [consoleHistory, setConsoleHistory] = useState<{ id: number; type: 'input' | 'output' | 'error'; content: string }[]>([]);
const [logs, setLogs] = useState<string>('');
const [isLogsModalOpen, setIsLogsModalOpen] = useState(false);
const [isExecuting, setIsExecuting] = useState(false);
const historyCounter = useRef(0);
const consoleEndRef = useRef<HTMLDivElement>(null);
const scrollToBottom = () => {
consoleEndRef.current?.scrollIntoView({ behavior: 'smooth' });
};
useEffect(() => {
scrollToBottom();
}, [consoleHistory]);
const addHistory = (type: 'input' | 'output' | 'error', content: string) => {
setConsoleHistory(prev => [...prev, { id: historyCounter.current++, type, content }]);
};
const handleExecuteMongosh = async () => {
if (!mongoshCommand.trim()) return;
addHistory('input', mongoshCommand);
const commandToRun = mongoshCommand;
setMongoshCommand('');
setIsExecuting(true);
try {
const response = await fetch('/api/admin/mongosh', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify({ command: commandToRun })
});
const data = await response.json();
if (!response.ok) {
addHistory('error', data.error || 'Error desconocido');
} else {
const outputString = typeof data.result === 'object' ? JSON.stringify(data.result, null, 2) : String(data.result);
addHistory('output', outputString);
if (data.isMemStore) {
toast.warning('MongoDB no está conectado. Utilizando modo memoria.');
}
}
} catch (error) {
addHistory('error', String(error));
} finally {
setIsExecuting(false);
}
};
const handleKeyDown = (e: React.KeyboardEvent<HTMLInputElement>) => {
if (e.key === 'Enter') {
handleExecuteMongosh();
}
};
const handleViewLogs = async () => {
try {
const response = await fetch('/api/admin/logs');
if (!response.ok) throw new Error('Error al obtener logs');
const data = await response.text();
setLogs(data || 'No hay logs disponibles.');
setIsLogsModalOpen(true);
} catch (e) {
toast.error('Error al cargar logs: ' + String(e));
}
};
const handleExportData = () => {
window.location.href = '/api/admin/db/export';
toast.success('Descarga iniciada');
};
const handleImportData = (e: React.ChangeEvent<HTMLInputElement>) => {
const file = e.target.files?.[0];
if (!file) return;
const reader = new FileReader();
reader.onload = async (event) => {
try {
const content = event.target?.result as string;
const dump = JSON.parse(content);
const response = await fetch('/api/admin/db/import', {
method: 'POST',
headers: { 'Content-Type': 'application/json' },
body: JSON.stringify(dump)
});
if (!response.ok) {
const data = await response.json();
throw new Error(data.error || 'Error al importar base de datos');
}
toast.success('Base de datos importada exitosamente. Recarga la página para ver los cambios.');
// Opcional: window.location.reload();
} catch (error) {
toast.error('Error: ' + String(error));
}
};
reader.readAsText(file);
e.target.value = ''; // Reset input
};
const handleClearConsole = () => {
setConsoleHistory([]);
};
return (
<div className="space-y-6">
<div className="flex items-center justify-between">
<div>
<h2 className="text-3xl font-bold tracking-tight text-gray-900 dark:text-white">Sistema y Depuración</h2>
<p className="text-gray-500 dark:text-gray-400">Herramientas avanzadas para administradores</p>
</div>
</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6">
<Card className="col-span-1 md:col-span-3">
<CardHeader>
<div className="flex items-center justify-between">
<div>
<CardTitle className="flex items-center gap-2">
<Terminal className="h-5 w-5" /> Consola mongosh
</CardTitle>
<CardDescription>Ejecuta comandos de consulta directamente en la base de datos.</CardDescription>
</div>
<Button variant="outline" size="sm" onClick={handleClearConsole}>
<RefreshCw className="h-4 w-4 mr-2" /> Limpiar
</Button>
</div>
</CardHeader>
<CardContent>
<div className="bg-gray-900 text-green-400 font-mono text-sm p-4 rounded-md h-[400px] overflow-y-auto mb-4 flex flex-col gap-2 relative">
{consoleHistory.length === 0 && (
<div className="text-gray-500 italic">Conectado. Esperando comandos... Ej: db.pacientes.find()</div>
)}
{consoleHistory.map((item) => (
<div key={item.id} className={`${item.type === 'error' ? 'text-red-400' : item.type === 'input' ? 'text-blue-300' : 'text-gray-300'}`}>
{item.type === 'input' && <span className="text-blue-500 mr-2">{'>'}</span>}
{item.type === 'output' && <span className="text-gray-500 mr-2">{'<-'}</span>}
{item.type === 'error' && <span className="text-red-500 mr-2">{'!'}</span>}
<pre className="inline-block whitespace-pre-wrap font-inherit m-0">{item.content}</pre>
</div>
))}
<div ref={consoleEndRef} />
</div>
<div className="flex gap-2">
<input
type="text"
className="flex-1 px-3 py-2 bg-white dark:bg-gray-800 border border-gray-300 dark:border-gray-700 rounded-md shadow-sm focus:outline-none focus:ring-2 focus:ring-blue-500 font-mono"
placeholder="Escribe un comando... (ej: db.usuarios.find())"
value={mongoshCommand}
onChange={(e) => setMongoshCommand(e.target.value)}
onKeyDown={handleKeyDown}
disabled={isExecuting}
/>
<Button onClick={handleExecuteMongosh} disabled={isExecuting}>
<Send className="h-4 w-4 mr-2" />
Ejecutar
</Button>
</div>
<div className="mt-2 text-xs text-gray-500 flex items-center gap-1">
<AlertCircle className="h-3 w-3" />
Soporta asincronía. Variables globales: `db`, `ObjectId`. Las promesas se resuelven automáticamente.
</div>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<FileText className="h-5 w-5" /> Logs del Servidor
</CardTitle>
<CardDescription>Visualiza los registros recientes del servidor.</CardDescription>
</CardHeader>
<CardContent>
<Button className="w-full" onClick={handleViewLogs}>
Ver Logs Recientes
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Download className="h-5 w-5" /> Exportar Datos
</CardTitle>
<CardDescription>Descarga una copia completa de la base de datos (JSON).</CardDescription>
</CardHeader>
<CardContent>
<Button className="w-full" variant="secondary" onClick={handleExportData}>
Exportar Todo
</Button>
</CardContent>
</Card>
<Card>
<CardHeader>
<CardTitle className="flex items-center gap-2">
<Upload className="h-5 w-5" /> Importar Datos
</CardTitle>
<CardDescription>Restaura la base de datos desde un archivo (JSON).</CardDescription>
</CardHeader>
<CardContent>
<div className="relative">
<input
type="file"
accept=".json"
className="absolute inset-0 w-full h-full opacity-0 cursor-pointer"
onChange={handleImportData}
/>
<Button className="w-full" variant="destructive">
Seleccionar e Importar
</Button>
</div>
<p className="text-xs text-red-500 mt-2 text-center">Advertencia: Esto sobreescribirá todos los datos actuales.</p>
</CardContent>
</Card>
</div>
{isLogsModalOpen && (
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
<div className="bg-white dark:bg-gray-900 rounded-lg p-6 w-full max-w-4xl max-h-[90vh] flex flex-col shadow-xl">
<h3 className="text-lg font-bold mb-4 flex justify-between items-center text-gray-900 dark:text-white">
<span>Logs del Servidor</span>
<Button variant="ghost" size="sm" onClick={() => setIsLogsModalOpen(false)}>Cerrar</Button>
</h3>
<div className="flex-1 overflow-auto bg-gray-950 text-green-400 font-mono text-xs p-4 rounded border border-gray-800">
<pre className="whitespace-pre-wrap">{logs}</pre>
</div>
<div className="mt-4 flex justify-end">
<Button onClick={() => setIsLogsModalOpen(false)}>Aceptar</Button>
</div>
</div>
</div>
)}
</div>
);
}
+1 -1
View File
@@ -237,7 +237,7 @@ export interface MovimientoIndicacion {
observaciones?: string;
}
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso' | 'usuarios' | 'pendientessala';
export type Vista = 'dashboard' | 'camas' | 'pacientes' | 'internaciones' | 'evoluciones' | 'laboratorios' | 'acidobase' | 'cultivos' | 'historiaclinica' | 'nuevoingreso' | 'editaringreso' | 'usuarios' | 'pendientessala' | 'sistema';
export type RolUsuario = 'admin' | 'medico' | 'enfermero';