EPIC-P2-001: Frontend Actions Implementation - Replace 31 console.log placeholders with navigate() calls - 16 pages updated with proper routing EPIC-P2-002: Settings Subpages Creation - Add CompanySettingsPage.tsx - Add ProfileSettingsPage.tsx - Add SecuritySettingsPage.tsx - Add SystemSettingsPage.tsx - Update router with new routes EPIC-P2-003: Bug Fix ValuationReportsPage - Fix recursive getToday() function EPIC-P2-006: CRM Pipeline Kanban - Add PipelineKanbanPage.tsx - Add KanbanColumn.tsx component - Add KanbanCard.tsx component - Add CRM routes to router Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
924 lines
29 KiB
TypeScript
924 lines
29 KiB
TypeScript
import { useState } from 'react';
|
|
import {
|
|
Database,
|
|
Download,
|
|
Upload,
|
|
FileText,
|
|
Key,
|
|
Plus,
|
|
Copy,
|
|
Eye,
|
|
EyeOff,
|
|
Trash2,
|
|
Check,
|
|
X,
|
|
AlertTriangle,
|
|
Clock,
|
|
HardDrive,
|
|
Cpu,
|
|
Activity,
|
|
Package,
|
|
ToggleLeft,
|
|
ToggleRight,
|
|
Calendar,
|
|
Filter,
|
|
} from 'lucide-react';
|
|
import { Button } from '@components/atoms/Button';
|
|
import { Input } from '@components/atoms/Input';
|
|
import { Card, CardHeader, CardTitle, CardContent, CardFooter } from '@components/molecules/Card';
|
|
import { DataTable, type Column } from '@components/organisms/DataTable';
|
|
import { Breadcrumbs } from '@components/organisms/Breadcrumbs';
|
|
import { ConfirmModal } from '@components/organisms/Modal';
|
|
import { formatDate } from '@utils/formatters';
|
|
|
|
interface BackupConfig {
|
|
autoBackup: boolean;
|
|
frequency: 'daily' | 'weekly' | 'monthly';
|
|
retentionDays: number;
|
|
lastBackup: string | null;
|
|
nextBackup: string | null;
|
|
storageLocation: string;
|
|
}
|
|
|
|
interface SystemLog {
|
|
id: string;
|
|
timestamp: string;
|
|
level: 'info' | 'warning' | 'error';
|
|
source: string;
|
|
message: string;
|
|
}
|
|
|
|
interface ApiKey {
|
|
id: string;
|
|
name: string;
|
|
key: string;
|
|
createdAt: string;
|
|
lastUsed: string | null;
|
|
permissions: string[];
|
|
isActive: boolean;
|
|
}
|
|
|
|
interface Module {
|
|
id: string;
|
|
name: string;
|
|
description: string;
|
|
version: string;
|
|
isActive: boolean;
|
|
isCore: boolean;
|
|
}
|
|
|
|
const initialBackupConfig: BackupConfig = {
|
|
autoBackup: true,
|
|
frequency: 'daily',
|
|
retentionDays: 30,
|
|
lastBackup: new Date(Date.now() - 86400000).toISOString(),
|
|
nextBackup: new Date(Date.now() + 43200000).toISOString(),
|
|
storageLocation: 'cloud',
|
|
};
|
|
|
|
const mockSystemLogs: SystemLog[] = [
|
|
{
|
|
id: '1',
|
|
timestamp: new Date().toISOString(),
|
|
level: 'info',
|
|
source: 'auth',
|
|
message: 'Usuario juan.perez@miempresa.com inicio sesion exitosamente',
|
|
},
|
|
{
|
|
id: '2',
|
|
timestamp: new Date(Date.now() - 300000).toISOString(),
|
|
level: 'warning',
|
|
source: 'database',
|
|
message: 'Conexion a base de datos lenta (>500ms)',
|
|
},
|
|
{
|
|
id: '3',
|
|
timestamp: new Date(Date.now() - 600000).toISOString(),
|
|
level: 'info',
|
|
source: 'backup',
|
|
message: 'Respaldo automatico completado exitosamente',
|
|
},
|
|
{
|
|
id: '4',
|
|
timestamp: new Date(Date.now() - 900000).toISOString(),
|
|
level: 'error',
|
|
source: 'email',
|
|
message: 'Error al enviar notificacion: SMTP timeout',
|
|
},
|
|
{
|
|
id: '5',
|
|
timestamp: new Date(Date.now() - 1200000).toISOString(),
|
|
level: 'info',
|
|
source: 'system',
|
|
message: 'Modulo de inventario actualizado a v2.1.0',
|
|
},
|
|
{
|
|
id: '6',
|
|
timestamp: new Date(Date.now() - 1500000).toISOString(),
|
|
level: 'warning',
|
|
source: 'storage',
|
|
message: 'Uso de almacenamiento al 75%',
|
|
},
|
|
{
|
|
id: '7',
|
|
timestamp: new Date(Date.now() - 1800000).toISOString(),
|
|
level: 'info',
|
|
source: 'auth',
|
|
message: 'Token de API generado para integracion externa',
|
|
},
|
|
{
|
|
id: '8',
|
|
timestamp: new Date(Date.now() - 2100000).toISOString(),
|
|
level: 'error',
|
|
source: 'api',
|
|
message: 'Rate limit excedido para IP 192.168.1.100',
|
|
},
|
|
{
|
|
id: '9',
|
|
timestamp: new Date(Date.now() - 2400000).toISOString(),
|
|
level: 'info',
|
|
source: 'database',
|
|
message: 'Migracion de base de datos completada',
|
|
},
|
|
{
|
|
id: '10',
|
|
timestamp: new Date(Date.now() - 2700000).toISOString(),
|
|
level: 'info',
|
|
source: 'system',
|
|
message: 'Sistema iniciado correctamente',
|
|
},
|
|
];
|
|
|
|
const mockApiKeys: ApiKey[] = [
|
|
{
|
|
id: '1',
|
|
name: 'Integracion Shopify',
|
|
key: 'sk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxx',
|
|
createdAt: new Date(Date.now() - 2592000000).toISOString(),
|
|
lastUsed: new Date(Date.now() - 3600000).toISOString(),
|
|
permissions: ['read:products', 'write:orders'],
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'API Movil',
|
|
key: 'sk_live_yyyyyyyyyyyyyyyyyyyyyyyyyyy',
|
|
createdAt: new Date(Date.now() - 5184000000).toISOString(),
|
|
lastUsed: new Date().toISOString(),
|
|
permissions: ['read:all', 'write:all'],
|
|
isActive: true,
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'Webhook Pagos',
|
|
key: 'sk_live_zzzzzzzzzzzzzzzzzzzzzzzzzzz',
|
|
createdAt: new Date(Date.now() - 7776000000).toISOString(),
|
|
lastUsed: null,
|
|
permissions: ['write:payments'],
|
|
isActive: false,
|
|
},
|
|
];
|
|
|
|
const mockModules: Module[] = [
|
|
{
|
|
id: '1',
|
|
name: 'Core',
|
|
description: 'Funcionalidades basicas del sistema',
|
|
version: '1.0.0',
|
|
isActive: true,
|
|
isCore: true,
|
|
},
|
|
{
|
|
id: '2',
|
|
name: 'Inventario',
|
|
description: 'Gestion de productos y stock',
|
|
version: '2.1.0',
|
|
isActive: true,
|
|
isCore: false,
|
|
},
|
|
{
|
|
id: '3',
|
|
name: 'Ventas',
|
|
description: 'Punto de venta y facturacion',
|
|
version: '1.5.0',
|
|
isActive: true,
|
|
isCore: false,
|
|
},
|
|
{
|
|
id: '4',
|
|
name: 'Compras',
|
|
description: 'Ordenes de compra y proveedores',
|
|
version: '1.3.0',
|
|
isActive: true,
|
|
isCore: false,
|
|
},
|
|
{
|
|
id: '5',
|
|
name: 'Contabilidad',
|
|
description: 'Plan de cuentas y asientos contables',
|
|
version: '1.2.0',
|
|
isActive: false,
|
|
isCore: false,
|
|
},
|
|
{
|
|
id: '6',
|
|
name: 'CRM',
|
|
description: 'Gestion de clientes y oportunidades',
|
|
version: '1.0.0',
|
|
isActive: false,
|
|
isCore: false,
|
|
},
|
|
{
|
|
id: '7',
|
|
name: 'Proyectos',
|
|
description: 'Gestion de proyectos y tareas',
|
|
version: '1.0.0',
|
|
isActive: false,
|
|
isCore: false,
|
|
},
|
|
{
|
|
id: '8',
|
|
name: 'RRHH',
|
|
description: 'Recursos humanos y nomina',
|
|
version: '0.9.0',
|
|
isActive: false,
|
|
isCore: false,
|
|
},
|
|
];
|
|
|
|
export function SystemSettingsPage() {
|
|
const [backupConfig, setBackupConfig] = useState<BackupConfig>(initialBackupConfig);
|
|
const [systemLogs] = useState<SystemLog[]>(mockSystemLogs);
|
|
const [apiKeys, setApiKeys] = useState<ApiKey[]>(mockApiKeys);
|
|
const [modules, setModules] = useState<Module[]>(mockModules);
|
|
const [isBackingUp, setIsBackingUp] = useState(false);
|
|
const [isRestoring, setIsRestoring] = useState(false);
|
|
const [showNewApiKeyModal, setShowNewApiKeyModal] = useState(false);
|
|
const [newApiKeyName, setNewApiKeyName] = useState('');
|
|
const [generatedApiKey, setGeneratedApiKey] = useState<string | null>(null);
|
|
const [visibleKeys, setVisibleKeys] = useState<Set<string>>(new Set());
|
|
const [apiKeyToDelete, setApiKeyToDelete] = useState<ApiKey | null>(null);
|
|
const [logLevelFilter, setLogLevelFilter] = useState<string>('');
|
|
const [logSourceFilter, setLogSourceFilter] = useState<string>('');
|
|
|
|
const handleBackupNow = async () => {
|
|
setIsBackingUp(true);
|
|
await new Promise((resolve) => setTimeout(resolve, 3000));
|
|
setBackupConfig((prev) => ({
|
|
...prev,
|
|
lastBackup: new Date().toISOString(),
|
|
}));
|
|
setIsBackingUp(false);
|
|
alert('Respaldo completado exitosamente');
|
|
};
|
|
|
|
const handleRestore = async () => {
|
|
setIsRestoring(true);
|
|
await new Promise((resolve) => setTimeout(resolve, 2000));
|
|
setIsRestoring(false);
|
|
alert('Por favor selecciona un archivo de respaldo para restaurar');
|
|
};
|
|
|
|
const handleBackupConfigChange = (field: keyof BackupConfig, value: unknown) => {
|
|
setBackupConfig((prev) => ({ ...prev, [field]: value }));
|
|
};
|
|
|
|
const handleCreateApiKey = async () => {
|
|
if (!newApiKeyName.trim()) {
|
|
alert('Ingresa un nombre para la API key');
|
|
return;
|
|
}
|
|
const newKey = `sk_live_${Math.random().toString(36).substring(2, 32)}`;
|
|
const newApiKey: ApiKey = {
|
|
id: String(apiKeys.length + 1),
|
|
name: newApiKeyName,
|
|
key: newKey,
|
|
createdAt: new Date().toISOString(),
|
|
lastUsed: null,
|
|
permissions: ['read:all'],
|
|
isActive: true,
|
|
};
|
|
setApiKeys((prev) => [...prev, newApiKey]);
|
|
setGeneratedApiKey(newKey);
|
|
};
|
|
|
|
const handleCloseNewApiKeyModal = () => {
|
|
setShowNewApiKeyModal(false);
|
|
setNewApiKeyName('');
|
|
setGeneratedApiKey(null);
|
|
};
|
|
|
|
const handleCopyApiKey = (key: string) => {
|
|
navigator.clipboard.writeText(key);
|
|
alert('API key copiada al portapapeles');
|
|
};
|
|
|
|
const toggleKeyVisibility = (keyId: string) => {
|
|
setVisibleKeys((prev) => {
|
|
const newSet = new Set(prev);
|
|
if (newSet.has(keyId)) {
|
|
newSet.delete(keyId);
|
|
} else {
|
|
newSet.add(keyId);
|
|
}
|
|
return newSet;
|
|
});
|
|
};
|
|
|
|
const handleDeleteApiKey = async () => {
|
|
if (apiKeyToDelete) {
|
|
setApiKeys((prev) => prev.filter((k) => k.id !== apiKeyToDelete.id));
|
|
setApiKeyToDelete(null);
|
|
alert(`API key "${apiKeyToDelete.name}" eliminada`);
|
|
}
|
|
};
|
|
|
|
const handleToggleModule = (moduleId: string) => {
|
|
setModules((prev) =>
|
|
prev.map((m) =>
|
|
m.id === moduleId && !m.isCore ? { ...m, isActive: !m.isActive } : m
|
|
)
|
|
);
|
|
};
|
|
|
|
const getLevelIcon = (level: SystemLog['level']) => {
|
|
switch (level) {
|
|
case 'info':
|
|
return <Activity className="h-4 w-4 text-blue-600" />;
|
|
case 'warning':
|
|
return <AlertTriangle className="h-4 w-4 text-yellow-600" />;
|
|
case 'error':
|
|
return <X className="h-4 w-4 text-red-600" />;
|
|
}
|
|
};
|
|
|
|
const getLevelColor = (level: SystemLog['level']) => {
|
|
switch (level) {
|
|
case 'info':
|
|
return 'bg-blue-100 text-blue-700';
|
|
case 'warning':
|
|
return 'bg-yellow-100 text-yellow-700';
|
|
case 'error':
|
|
return 'bg-red-100 text-red-700';
|
|
}
|
|
};
|
|
|
|
const filteredLogs = systemLogs.filter((log) => {
|
|
if (logLevelFilter && log.level !== logLevelFilter) return false;
|
|
if (logSourceFilter && log.source !== logSourceFilter) return false;
|
|
return true;
|
|
});
|
|
|
|
const uniqueSources = [...new Set(systemLogs.map((l) => l.source))];
|
|
|
|
const logColumns: Column<SystemLog>[] = [
|
|
{
|
|
key: 'timestamp',
|
|
header: 'Fecha',
|
|
render: (log) => (
|
|
<span className="text-sm text-gray-600 whitespace-nowrap">
|
|
{formatDate(log.timestamp, 'full')}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'level',
|
|
header: 'Nivel',
|
|
render: (log) => (
|
|
<span
|
|
className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${getLevelColor(
|
|
log.level
|
|
)}`}
|
|
>
|
|
{getLevelIcon(log.level)}
|
|
{log.level.toUpperCase()}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'source',
|
|
header: 'Fuente',
|
|
render: (log) => (
|
|
<span className="inline-flex items-center rounded-full bg-gray-100 px-2 py-1 text-xs font-medium text-gray-700">
|
|
{log.source}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'message',
|
|
header: 'Mensaje',
|
|
render: (log) => <span className="text-sm text-gray-900">{log.message}</span>,
|
|
},
|
|
];
|
|
|
|
const apiKeyColumns: Column<ApiKey>[] = [
|
|
{
|
|
key: 'name',
|
|
header: 'Nombre',
|
|
render: (apiKey) => (
|
|
<div>
|
|
<div className="font-medium text-gray-900">{apiKey.name}</div>
|
|
<div className="text-xs text-gray-500">
|
|
Creada: {formatDate(apiKey.createdAt, 'short')}
|
|
</div>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'key',
|
|
header: 'API Key',
|
|
render: (apiKey) => (
|
|
<div className="flex items-center gap-2">
|
|
<code className="rounded bg-gray-100 px-2 py-1 font-mono text-xs">
|
|
{visibleKeys.has(apiKey.id)
|
|
? apiKey.key
|
|
: `${apiKey.key.slice(0, 10)}${'*'.repeat(20)}`}
|
|
</code>
|
|
<button
|
|
onClick={() => toggleKeyVisibility(apiKey.id)}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
{visibleKeys.has(apiKey.id) ? (
|
|
<EyeOff className="h-4 w-4" />
|
|
) : (
|
|
<Eye className="h-4 w-4" />
|
|
)}
|
|
</button>
|
|
<button
|
|
onClick={() => handleCopyApiKey(apiKey.key)}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</button>
|
|
</div>
|
|
),
|
|
},
|
|
{
|
|
key: 'lastUsed',
|
|
header: 'Ultimo uso',
|
|
render: (apiKey) => (
|
|
<span className="text-sm text-gray-600">
|
|
{apiKey.lastUsed ? formatDate(apiKey.lastUsed, 'short') : 'Nunca'}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'isActive',
|
|
header: 'Estado',
|
|
render: (apiKey) => (
|
|
<span
|
|
className={`inline-flex items-center gap-1 rounded-full px-2 py-1 text-xs font-medium ${
|
|
apiKey.isActive
|
|
? 'bg-green-100 text-green-700'
|
|
: 'bg-gray-100 text-gray-600'
|
|
}`}
|
|
>
|
|
{apiKey.isActive ? (
|
|
<>
|
|
<Check className="h-3 w-3" />
|
|
Activa
|
|
</>
|
|
) : (
|
|
<>
|
|
<X className="h-3 w-3" />
|
|
Inactiva
|
|
</>
|
|
)}
|
|
</span>
|
|
),
|
|
},
|
|
{
|
|
key: 'actions',
|
|
header: '',
|
|
render: (apiKey) => (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => setApiKeyToDelete(apiKey)}
|
|
className="text-red-600 hover:text-red-700 hover:bg-red-50"
|
|
>
|
|
<Trash2 className="h-4 w-4" />
|
|
</Button>
|
|
),
|
|
},
|
|
];
|
|
|
|
return (
|
|
<div className="space-y-6 p-6">
|
|
<Breadcrumbs
|
|
items={[
|
|
{ label: 'Configuracion', href: '/settings' },
|
|
{ label: 'Sistema' },
|
|
]}
|
|
/>
|
|
|
|
<div>
|
|
<h1 className="text-2xl font-bold text-gray-900">Configuracion del Sistema</h1>
|
|
<p className="text-sm text-gray-500">
|
|
Administra respaldos, logs, API keys y modulos
|
|
</p>
|
|
</div>
|
|
|
|
{/* System Stats */}
|
|
<div className="grid grid-cols-1 gap-4 sm:grid-cols-4">
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-blue-100">
|
|
<Database className="h-5 w-5 text-blue-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">Base de datos</div>
|
|
<div className="text-lg font-bold text-blue-600">2.3 GB</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-green-100">
|
|
<HardDrive className="h-5 w-5 text-green-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">Almacenamiento</div>
|
|
<div className="text-lg font-bold text-green-600">75%</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-purple-100">
|
|
<Cpu className="h-5 w-5 text-purple-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">CPU</div>
|
|
<div className="text-lg font-bold text-purple-600">23%</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
<Card>
|
|
<CardContent className="p-4">
|
|
<div className="flex items-center gap-3">
|
|
<div className="flex h-10 w-10 items-center justify-center rounded-lg bg-orange-100">
|
|
<Activity className="h-5 w-5 text-orange-600" />
|
|
</div>
|
|
<div>
|
|
<div className="text-sm text-gray-500">Uptime</div>
|
|
<div className="text-lg font-bold text-orange-600">99.9%</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
</div>
|
|
|
|
{/* Backup Configuration */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Database className="h-5 w-5 text-blue-600" />
|
|
Respaldos
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-6 lg:grid-cols-2">
|
|
<div className="space-y-4">
|
|
<div className="flex items-center justify-between">
|
|
<div>
|
|
<div className="font-medium text-gray-900">Respaldo automatico</div>
|
|
<div className="text-sm text-gray-500">
|
|
Realiza respaldos automaticos de la base de datos
|
|
</div>
|
|
</div>
|
|
<button
|
|
type="button"
|
|
role="switch"
|
|
aria-checked={backupConfig.autoBackup}
|
|
onClick={() =>
|
|
handleBackupConfigChange('autoBackup', !backupConfig.autoBackup)
|
|
}
|
|
className={`relative inline-flex h-6 w-11 flex-shrink-0 cursor-pointer rounded-full border-2 border-transparent transition-colors duration-200 ease-in-out focus:outline-none focus:ring-2 focus:ring-primary-500 focus:ring-offset-2 ${
|
|
backupConfig.autoBackup ? 'bg-primary-600' : 'bg-gray-200'
|
|
}`}
|
|
>
|
|
<span
|
|
className={`pointer-events-none inline-block h-5 w-5 transform rounded-full bg-white shadow ring-0 transition duration-200 ease-in-out ${
|
|
backupConfig.autoBackup ? 'translate-x-5' : 'translate-x-0'
|
|
}`}
|
|
/>
|
|
</button>
|
|
</div>
|
|
|
|
{backupConfig.autoBackup && (
|
|
<>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Frecuencia
|
|
</label>
|
|
<select
|
|
value={backupConfig.frequency}
|
|
onChange={(e) =>
|
|
handleBackupConfigChange('frequency', e.target.value)
|
|
}
|
|
className="block w-full rounded-md border border-gray-300 px-3 py-2 text-sm shadow-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
>
|
|
<option value="daily">Diario</option>
|
|
<option value="weekly">Semanal</option>
|
|
<option value="monthly">Mensual</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Retencion (dias)
|
|
</label>
|
|
<Input
|
|
type="number"
|
|
value={backupConfig.retentionDays}
|
|
onChange={(e) =>
|
|
handleBackupConfigChange('retentionDays', parseInt(e.target.value))
|
|
}
|
|
min={7}
|
|
max={365}
|
|
/>
|
|
</div>
|
|
</>
|
|
)}
|
|
</div>
|
|
|
|
<div className="space-y-4 lg:border-l lg:pl-6">
|
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-gray-50">
|
|
<Clock className="h-8 w-8 text-gray-400" />
|
|
<div>
|
|
<div className="text-sm text-gray-500">Ultimo respaldo</div>
|
|
<div className="font-medium text-gray-900">
|
|
{backupConfig.lastBackup
|
|
? formatDate(backupConfig.lastBackup, 'full')
|
|
: 'Nunca'}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
|
|
{backupConfig.autoBackup && backupConfig.nextBackup && (
|
|
<div className="flex items-center gap-4 p-4 rounded-lg bg-blue-50">
|
|
<Calendar className="h-8 w-8 text-blue-400" />
|
|
<div>
|
|
<div className="text-sm text-blue-600">Proximo respaldo</div>
|
|
<div className="font-medium text-blue-900">
|
|
{formatDate(backupConfig.nextBackup, 'full')}
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
|
|
<div className="flex gap-2">
|
|
<Button onClick={handleBackupNow} isLoading={isBackingUp}>
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Respaldar ahora
|
|
</Button>
|
|
<Button variant="outline" onClick={handleRestore} isLoading={isRestoring}>
|
|
<Upload className="mr-2 h-4 w-4" />
|
|
Restaurar
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* System Logs */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<FileText className="h-5 w-5 text-green-600" />
|
|
Logs del Sistema
|
|
</CardTitle>
|
|
<Button variant="outline" size="sm">
|
|
<Download className="mr-2 h-4 w-4" />
|
|
Exportar logs
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="space-y-4">
|
|
{/* Filters */}
|
|
<div className="flex flex-wrap items-center gap-4 p-4 bg-gray-50 rounded-lg">
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
<Filter className="inline-block h-3 w-3 mr-1" />
|
|
Nivel
|
|
</label>
|
|
<select
|
|
value={logLevelFilter}
|
|
onChange={(e) => setLogLevelFilter(e.target.value)}
|
|
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
>
|
|
<option value="">Todos</option>
|
|
<option value="info">Info</option>
|
|
<option value="warning">Warning</option>
|
|
<option value="error">Error</option>
|
|
</select>
|
|
</div>
|
|
|
|
<div>
|
|
<label className="block text-xs font-medium text-gray-700 mb-1">
|
|
Fuente
|
|
</label>
|
|
<select
|
|
value={logSourceFilter}
|
|
onChange={(e) => setLogSourceFilter(e.target.value)}
|
|
className="rounded-md border border-gray-300 px-3 py-1.5 text-sm focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"
|
|
>
|
|
<option value="">Todas</option>
|
|
{uniqueSources.map((source) => (
|
|
<option key={source} value={source}>
|
|
{source}
|
|
</option>
|
|
))}
|
|
</select>
|
|
</div>
|
|
|
|
{(logLevelFilter || logSourceFilter) && (
|
|
<Button
|
|
variant="ghost"
|
|
size="sm"
|
|
onClick={() => {
|
|
setLogLevelFilter('');
|
|
setLogSourceFilter('');
|
|
}}
|
|
>
|
|
Limpiar filtros
|
|
</Button>
|
|
)}
|
|
</div>
|
|
|
|
<DataTable data={filteredLogs} columns={logColumns} isLoading={false} />
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* API Keys */}
|
|
<Card>
|
|
<CardHeader>
|
|
<div className="flex items-center justify-between">
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Key className="h-5 w-5 text-purple-600" />
|
|
API Keys
|
|
</CardTitle>
|
|
<Button onClick={() => setShowNewApiKeyModal(true)}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Nueva API Key
|
|
</Button>
|
|
</div>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<p className="text-sm text-gray-500 mb-4">
|
|
Gestiona las API keys para integraciones externas
|
|
</p>
|
|
<DataTable data={apiKeys} columns={apiKeyColumns} isLoading={false} />
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* Active Modules */}
|
|
<Card>
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Package className="h-5 w-5 text-orange-600" />
|
|
Modulos Activos
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent>
|
|
<div className="grid gap-4 sm:grid-cols-2 lg:grid-cols-4">
|
|
{modules.map((module) => (
|
|
<div
|
|
key={module.id}
|
|
className={`rounded-lg border p-4 transition-colors ${
|
|
module.isActive ? 'border-green-200 bg-green-50' : 'border-gray-200 bg-gray-50'
|
|
}`}
|
|
>
|
|
<div className="flex items-start justify-between">
|
|
<div>
|
|
<div className="font-medium text-gray-900 flex items-center gap-2">
|
|
{module.name}
|
|
{module.isCore && (
|
|
<span className="inline-flex items-center rounded-full bg-blue-100 px-2 py-0.5 text-xs font-medium text-blue-700">
|
|
Core
|
|
</span>
|
|
)}
|
|
</div>
|
|
<div className="text-xs text-gray-500 mt-1">v{module.version}</div>
|
|
<div className="text-sm text-gray-600 mt-2">{module.description}</div>
|
|
</div>
|
|
{!module.isCore && (
|
|
<button
|
|
onClick={() => handleToggleModule(module.id)}
|
|
className="text-gray-400 hover:text-gray-600"
|
|
>
|
|
{module.isActive ? (
|
|
<ToggleRight className="h-6 w-6 text-green-600" />
|
|
) : (
|
|
<ToggleLeft className="h-6 w-6 text-gray-400" />
|
|
)}
|
|
</button>
|
|
)}
|
|
</div>
|
|
</div>
|
|
))}
|
|
</div>
|
|
</CardContent>
|
|
</Card>
|
|
|
|
{/* New API Key Modal */}
|
|
{showNewApiKeyModal && (
|
|
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black bg-opacity-50">
|
|
<Card className="w-full max-w-md">
|
|
<CardHeader>
|
|
<CardTitle className="flex items-center gap-2">
|
|
<Key className="h-5 w-5" />
|
|
{generatedApiKey ? 'API Key Generada' : 'Nueva API Key'}
|
|
</CardTitle>
|
|
</CardHeader>
|
|
<CardContent className="space-y-4">
|
|
{!generatedApiKey ? (
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Nombre de la API Key
|
|
</label>
|
|
<Input
|
|
value={newApiKeyName}
|
|
onChange={(e) => setNewApiKeyName(e.target.value)}
|
|
placeholder="Ej: Integracion Shopify"
|
|
/>
|
|
<p className="mt-1 text-xs text-gray-500">
|
|
Un nombre descriptivo para identificar esta API key
|
|
</p>
|
|
</div>
|
|
) : (
|
|
<div>
|
|
<div className="flex items-start gap-3 p-4 bg-yellow-50 rounded-lg mb-4">
|
|
<AlertTriangle className="h-5 w-5 text-yellow-600 flex-shrink-0 mt-0.5" />
|
|
<div className="text-sm text-yellow-800">
|
|
<p className="font-medium">Guarda esta API key de forma segura</p>
|
|
<p className="mt-1">
|
|
Esta es la unica vez que veras esta API key completa. No podras verla de nuevo.
|
|
</p>
|
|
</div>
|
|
</div>
|
|
<div>
|
|
<label className="block text-sm font-medium text-gray-700 mb-1">
|
|
Tu nueva API Key
|
|
</label>
|
|
<div className="flex gap-2">
|
|
<code className="flex-1 rounded-md bg-gray-100 px-4 py-2 font-mono text-sm break-all">
|
|
{generatedApiKey}
|
|
</code>
|
|
<Button
|
|
variant="outline"
|
|
size="sm"
|
|
onClick={() => handleCopyApiKey(generatedApiKey)}
|
|
>
|
|
<Copy className="h-4 w-4" />
|
|
</Button>
|
|
</div>
|
|
</div>
|
|
</div>
|
|
)}
|
|
</CardContent>
|
|
<CardFooter className="flex justify-end gap-2">
|
|
<Button variant="outline" onClick={handleCloseNewApiKeyModal}>
|
|
{generatedApiKey ? 'Cerrar' : 'Cancelar'}
|
|
</Button>
|
|
{!generatedApiKey && (
|
|
<Button onClick={handleCreateApiKey}>
|
|
<Plus className="mr-2 h-4 w-4" />
|
|
Generar API Key
|
|
</Button>
|
|
)}
|
|
</CardFooter>
|
|
</Card>
|
|
</div>
|
|
)}
|
|
|
|
{/* Delete API Key Modal */}
|
|
<ConfirmModal
|
|
isOpen={!!apiKeyToDelete}
|
|
onClose={() => setApiKeyToDelete(null)}
|
|
onConfirm={handleDeleteApiKey}
|
|
title="Eliminar API Key"
|
|
message={`¿Eliminar la API key "${apiKeyToDelete?.name}"? Las integraciones que usen esta key dejaran de funcionar.`}
|
|
variant="danger"
|
|
confirmText="Eliminar"
|
|
/>
|
|
</div>
|
|
);
|
|
}
|
|
|
|
export default SystemSettingsPage;
|