erp-core-frontend-v2/src/pages/settings/SystemSettingsPage.tsx
rckrdmrd 3a461cb184 [TASK-2026-01-20-005] feat: Resolve P2 gaps - Actions, Settings, Kanban
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>
2026-01-20 04:32:20 -06:00

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;