@decido/plugin-chameleon 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (38) hide show
  1. package/.turbo/turbo-build.log +13 -0
  2. package/package.json +47 -0
  3. package/src/App.tsx +116 -0
  4. package/src/components/DatawayDashboard.tsx +152 -0
  5. package/src/components/DatawayFilterModal.tsx +132 -0
  6. package/src/components/DatawayMathModal.tsx +120 -0
  7. package/src/components/DatawayOrchestratorCanvas.tsx +345 -0
  8. package/src/components/DatawayPipelineManager.tsx +227 -0
  9. package/src/components/DatawaySchemaMapper.tsx +291 -0
  10. package/src/components/FormulaBar.tsx +242 -0
  11. package/src/hooks/useSocketSync.ts +104 -0
  12. package/src/index.css +18 -0
  13. package/src/index.ts +121 -0
  14. package/src/logic/rules.ts +39 -0
  15. package/src/logic/workflowGuard.ts +45 -0
  16. package/src/main.tsx +10 -0
  17. package/src/services/gemini.ts +110 -0
  18. package/src/store/datawayStore.ts +26 -0
  19. package/src/stores/authStore.ts +26 -0
  20. package/src/stores/orderStore.ts +263 -0
  21. package/src/stores/uiStore.ts +19 -0
  22. package/src/types.ts +39 -0
  23. package/src/useAgent.ts +89 -0
  24. package/src/utils/sounds.ts +52 -0
  25. package/src/views/DatabaseAdminView.tsx +707 -0
  26. package/src/views/DatawayStudioView.tsx +959 -0
  27. package/src/views/DiscoveryAdminView.tsx +59 -0
  28. package/src/views/KuspideDashboardView.tsx +144 -0
  29. package/src/views/LoginView.tsx +122 -0
  30. package/src/views/MadefrontChatView.tsx +174 -0
  31. package/src/views/MadefrontExcelView.tsx +95 -0
  32. package/src/views/MadefrontKanbanView.tsx +292 -0
  33. package/src/views/roles/CajaView.tsx +76 -0
  34. package/src/views/roles/DespachoView.tsx +100 -0
  35. package/src/views/roles/PlantaView.tsx +83 -0
  36. package/src/views/roles/VentasView.tsx +101 -0
  37. package/src/views/roles/WorkflowAdminView.tsx +62 -0
  38. package/tsconfig.json +34 -0
@@ -0,0 +1,707 @@
1
+ import React, { useState, useEffect, useMemo } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { toast } from 'sonner';
4
+ import {
5
+ Database, TableProperties, RefreshCw, Plus, Edit2, Trash2, X,
6
+ Check, Search, Filter, ServerCrash, DatabaseZap, LayoutGrid,
7
+ ChevronRight, AlertCircle, Loader2, ArrowUpDown, Server
8
+ } from 'lucide-react';
9
+
10
+ const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
11
+
12
+ type SortConfig = { key: string; direction: 'asc' | 'desc' } | null;
13
+
14
+ export const DatabaseAdminView = () => {
15
+ // Dynamic Schema & Tables State
16
+ const [connectors] = useState<{id: string, name: string}[]>([
17
+ { id: 'postgres', name: 'PostgreSQL Core' },
18
+ { id: 'google_sheets', name: 'Google Sheets MCP' }
19
+ ]);
20
+ const [activeConnector, setActiveConnector] = useState('postgres');
21
+ const [tables, setTables] = useState<string[]>([]);
22
+ const [activeTable, setActiveTable] = useState<string | null>(null);
23
+
24
+ // UI states
25
+ const [isTablesLoading, setIsTablesLoading] = useState(true);
26
+ const [isDataLoading, setIsDataLoading] = useState(false);
27
+ const [data, setData] = useState<any[]>([]);
28
+ const [searchTerm, setSearchTerm] = useState('');
29
+ const [sortConfig, setSortConfig] = useState<SortConfig>(null);
30
+ const [connectionError, setConnectionError] = useState<string | null>(null);
31
+
32
+ // Schema View States
33
+ const [viewMode, setViewMode] = useState<'data' | 'schema'>('data');
34
+ const [schemaData, setSchemaData] = useState<any[]>([]);
35
+ const [isSchemaLoading, setIsSchemaLoading] = useState(false);
36
+
37
+ // Form & Modal States
38
+ const [isFormOpen, setIsFormOpen] = useState(false);
39
+ const [editRecord, setEditRecord] = useState<any>(null);
40
+ const [isCreating, setIsCreating] = useState(false);
41
+ const [isSaving, setIsSaving] = useState(false);
42
+
43
+ // 1. Fetch available tables dynamically from the backend
44
+ const fetchSchema = async () => {
45
+ setIsTablesLoading(true);
46
+ setConnectionError(null);
47
+ try {
48
+ const res = await fetch(`${baseUrl}/api/dataway/resources?connectorId=${activeConnector}`, {
49
+ headers: { 'x-api-key': 'dev_ak_chameleon_studio_99x' }
50
+ });
51
+ if (!res.ok) throw new Error('API Endpoint /api/dataway/resources no disponible');
52
+
53
+ const result = await res.json();
54
+ const tableList = Array.isArray(result.resources) ? result.resources : [];
55
+ setTables(tableList);
56
+
57
+ if (tableList.length > 0) setActiveTable(tableList[0]);
58
+ else setActiveTable(null);
59
+ } catch (error: any) {
60
+ console.error('[DB Admin] Failed to load schema:', error);
61
+ setConnectionError(error.message);
62
+ } finally {
63
+ setIsTablesLoading(false);
64
+ }
65
+ };
66
+
67
+ useEffect(() => {
68
+ fetchSchema();
69
+ }, [activeConnector]);
70
+
71
+ // 2. Fetch specific table data using Universal Dataway
72
+ const fetchTableData = async (tableName: string, silent = false) => {
73
+ if (!silent) setIsDataLoading(true);
74
+ setSearchTerm(''); // Reset search on table change
75
+ setSortConfig(null);
76
+ try {
77
+ // Using generic Dataway routing instead of direct DB endpoints
78
+ const res = await fetch(`${baseUrl}/api/dataway/query`, {
79
+ method: 'POST',
80
+ headers: {
81
+ 'x-api-key': 'dev_ak_chameleon_studio_99x',
82
+ 'Content-Type': 'application/json'
83
+ },
84
+ body: JSON.stringify({
85
+ connectorId: activeConnector,
86
+ resource: tableName,
87
+ type: activeConnector === 'postgres' ? 'table' : 'range'
88
+ })
89
+ });
90
+ if (!res.ok) throw new Error(`El backend Dataway rechazó la lectura de la tabla ${tableName}`);
91
+
92
+ const result = await res.json();
93
+ setData(Array.isArray(result.data) ? result.data : []);
94
+ } catch (error: any) {
95
+ toast.error(`Acceso Denegado a ${tableName}`, { description: error.message });
96
+ setData([]); // NO MOCKS.
97
+ } finally {
98
+ setIsDataLoading(false);
99
+ }
100
+ };
101
+
102
+ const fetchTableSchema = async (tableName: string) => {
103
+ setIsSchemaLoading(true);
104
+ try {
105
+ const res = await fetch(`${baseUrl}/api/dataway/schema?connectorId=${activeConnector}&resource=${tableName}`, {
106
+ headers: { 'x-api-key': 'dev_ak_chameleon_studio_99x' }
107
+ });
108
+ if (!res.ok) throw new Error(`El Dataway backend rechazó el esquema de ${tableName}`);
109
+
110
+ const result = await res.json();
111
+ setSchemaData(result.schema || []);
112
+ } catch (error: any) {
113
+ toast.error(`Error de Esquema Dataway`, { description: error.message });
114
+ setSchemaData([]);
115
+ } finally {
116
+ setIsSchemaLoading(false);
117
+ }
118
+ };
119
+
120
+ useEffect(() => {
121
+ if (activeTable) {
122
+ fetchTableData(activeTable);
123
+ fetchTableSchema(activeTable);
124
+ setViewMode('data'); // Reset to data view on table change
125
+ }
126
+ }, [activeTable]);
127
+
128
+ // 3. Handle Sort
129
+ const handleSort = (key: string) => {
130
+ let direction: 'asc' | 'desc' = 'asc';
131
+ if (sortConfig && sortConfig.key === key && sortConfig.direction === 'asc') {
132
+ direction = 'desc';
133
+ }
134
+ setSortConfig({ key, direction });
135
+ };
136
+
137
+ // Derived processed data
138
+ const processedData = useMemo(() => {
139
+ let sortableItems = [...data];
140
+
141
+ // Search Filter
142
+ if (searchTerm) {
143
+ const lowercasedFilter = searchTerm.toLowerCase();
144
+ sortableItems = sortableItems.filter(item => {
145
+ return Object.keys(item).some(key =>
146
+ String(item[key]).toLowerCase().includes(lowercasedFilter)
147
+ );
148
+ });
149
+ }
150
+
151
+ // Sorting
152
+ if (sortConfig !== null) {
153
+ sortableItems.sort((a, b) => {
154
+ const aVal = a[sortConfig.key];
155
+ const bVal = b[sortConfig.key];
156
+
157
+ if (aVal < bVal) return sortConfig.direction === 'asc' ? -1 : 1;
158
+ if (aVal > bVal) return sortConfig.direction === 'asc' ? 1 : -1;
159
+ return 0;
160
+ });
161
+ }
162
+
163
+ return sortableItems;
164
+ }, [data, searchTerm, sortConfig]);
165
+
166
+ // Ocultar ID, created_at u otros metadatos complejos en columnas automáticas si lo deseamos,
167
+ // pero por defecto, en un DDBB Admin pro, queremos ver todo (o truncado)
168
+ const columns = data.length > 0 ? Object.keys(data[0]) : [];
169
+
170
+ // 4. CRUD Actions
171
+ const handleSave = async (record: any) => {
172
+ if (!activeTable) return;
173
+ setIsSaving(true);
174
+ const isNew = isCreating;
175
+ const method = isNew ? 'POST' : 'PUT';
176
+ const url = isNew
177
+ ? `${baseUrl}/api/db/${activeTable}`
178
+ : `${baseUrl}/api/db/${activeTable}/${record.id}`;
179
+
180
+ try {
181
+ const payload = { ...record };
182
+ if (isNew) delete payload.id; // Evitar enviar ID en POST si es autoincremental/UUID
183
+
184
+ const res = await fetch(url, {
185
+ method,
186
+ headers: {
187
+ 'Content-Type': 'application/json',
188
+ 'x-api-key': 'dev_ak_chameleon_studio_99x'
189
+ },
190
+ body: JSON.stringify(payload)
191
+ });
192
+
193
+ if (!res.ok) {
194
+ const err = await res.json().catch(() => ({}));
195
+ throw new Error(err.message || err.error || 'Error desconocido al guardar en base de datos');
196
+ }
197
+
198
+ toast.success(`Registro ${isNew ? 'creado' : 'actualizado'} en la tabla ${activeTable}`);
199
+ setIsFormOpen(false);
200
+
201
+ // Reload from server source of truth
202
+ fetchTableData(activeTable, true);
203
+ } catch (error: any) {
204
+ toast.error('Error de Integridad', { description: error.message });
205
+ } finally {
206
+ setIsSaving(false);
207
+ }
208
+ };
209
+
210
+ const handleDelete = async (id: string | number) => {
211
+ if (!activeTable) return;
212
+ if (!confirm('¿Atención: Esta acción es destructiva y permanente. ¿Confirmar eliminación?')) return;
213
+
214
+ try {
215
+ const res = await fetch(`${baseUrl}/api/db/${activeTable}/${id}`, {
216
+ method: 'DELETE',
217
+ headers: { 'x-api-key': 'dev_ak_chameleon_studio_99x' }
218
+ });
219
+
220
+ if (!res.ok) throw new Error('No se pudo eliminar el registro (violación de Foreign Key o ruta no implementada)');
221
+
222
+ toast.success('Registro eliminado exitosamente');
223
+ // Optimistic update
224
+ setData(prev => prev.filter(r => r.id !== id));
225
+ } catch (error: any) {
226
+ toast.error('Fallo al Eliminar', { description: error.message });
227
+ }
228
+ };
229
+
230
+ const openEditModal = (record: any) => {
231
+ setEditRecord({ ...record });
232
+ setIsCreating(false);
233
+ setIsFormOpen(true);
234
+ };
235
+
236
+ const openCreateModal = () => {
237
+ const structuralRecord: any = {};
238
+ if (columns.length > 0) {
239
+ columns.forEach(col => {
240
+ if (col !== 'id' && col !== 'created_at' && col !== 'updated_at') {
241
+ structuralRecord[col] = '';
242
+ }
243
+ });
244
+ }
245
+ setEditRecord(structuralRecord);
246
+ setIsCreating(true);
247
+ setIsFormOpen(true);
248
+ };
249
+
250
+ const inferInputType = (val: any) => {
251
+ if (typeof val === 'boolean') return 'checkbox';
252
+ if (typeof val === 'number') return 'number';
253
+ return 'text';
254
+ };
255
+
256
+ return (
257
+ <div className="w-full h-full flex flex-row overflow-hidden" style={{
258
+ '--bg-primary': 'var(--ds-surface-primary, #ffffff)',
259
+ '--bg-elevated': 'var(--ds-surface-secondary, #f8fafc)',
260
+ '--bg-surface': 'var(--ds-surface-tertiary, #f1f5f9)',
261
+ '--border-color': 'var(--ds-border-default, #e2e8f0)',
262
+ '--text-primary': 'var(--ds-text-primary, #0f172a)',
263
+ '--text-secondary': 'var(--ds-text-secondary, #64748b)',
264
+ '--brand-primary': 'var(--ds-accent-blue, #3b82f6)',
265
+ '--brand-primary-hover': 'var(--ds-accent-blue-hover, #2563eb)',
266
+ } as React.CSSProperties}>
267
+
268
+ {/* Sidebar de Estructura */}
269
+ <div className="w-72 border-r border-[var(--border-color)] bg-[var(--bg-elevated)] flex flex-col h-full z-10 shadow-[2px_0_10px_rgba(0,0,0,0.02)]">
270
+ <div className="p-6 border-b border-[var(--border-color)] flex flex-col gap-4 bg-[var(--bg-primary)]">
271
+ <div className="flex items-center gap-3">
272
+ <div className="p-2 bg-[var(--brand-primary)]/10 rounded-xl">
273
+ <DatabaseZap className="w-6 h-6 text-[var(--brand-primary)]" />
274
+ </div>
275
+ <div>
276
+ <h2 className="font-extrabold text-[var(--text-primary)] tracking-tight text-lg leading-none">Data Studio</h2>
277
+ <p className="text-[11px] font-medium text-[var(--text-secondary)] uppercase tracking-wider mt-1">Dataway Core</p>
278
+ </div>
279
+ </div>
280
+
281
+ <select
282
+ value={activeConnector}
283
+ onChange={(e) => setActiveConnector(e.target.value)}
284
+ className="w-full bg-[var(--bg-surface)] border border-[var(--border-color)] text-[var(--text-primary)] text-sm font-bold rounded-lg px-3 py-2 outline-none cursor-pointer hover:border-[var(--brand-primary)]/50 focus:border-[var(--brand-primary)] focus:ring-2 focus:ring-[var(--brand-primary)]/20 transition-all shadow-sm"
285
+ >
286
+ {connectors.map(c => (
287
+ <option key={c.id} value={c.id}>{c.name}</option>
288
+ ))}
289
+ </select>
290
+ </div>
291
+
292
+ <div className="p-4 flex-1 overflow-y-auto w-full space-y-1.5 custom-scrollbar">
293
+ <h3 className="text-xs font-bold text-[var(--text-secondary)] tracking-wider uppercase mb-3 px-2 flex items-center justify-between">
294
+ Esquema Púbico
295
+ <button onClick={fetchSchema} className="hover:text-[var(--text-primary)] transition-colors p-1 rounded-md hover:bg-[var(--bg-surface)]">
296
+ <RefreshCw className="w-3 h-3" />
297
+ </button>
298
+ </h3>
299
+
300
+ {isTablesLoading ? (
301
+ <div className="space-y-2 px-2">
302
+ {[1,2,3,4].map(skeleton => (
303
+ <div key={skeleton} className="h-10 bg-[var(--bg-surface)] rounded-lg animate-pulse w-full"></div>
304
+ ))}
305
+ </div>
306
+ ) : connectionError ? (
307
+ <div className="p-4 mx-2 bg-red-50 text-red-600 rounded-xl border border-red-100 flex flex-col items-center text-center gap-2">
308
+ <ServerCrash className="w-6 h-6" />
309
+ <p className="text-sm font-semibold">Backend Offline</p>
310
+ <p className="text-xs opacity-80">{connectionError}</p>
311
+ </div>
312
+ ) : tables.length === 0 ? (
313
+ <div className="p-4 text-center text-[var(--text-secondary)] text-sm">
314
+ No se descubrieron tablas vía /api/db/tables.
315
+ </div>
316
+ ) : (
317
+ tables.map(table => (
318
+ <button
319
+ key={table}
320
+ onClick={() => setActiveTable(table)}
321
+ className={`w-full flex items-center justify-between px-3 py-2.5 rounded-xl text-sm font-semibold transition-all duration-200 group ${
322
+ activeTable === table
323
+ ? 'bg-gradient-to-r from-[var(--brand-primary)] to-[var(--brand-primary-hover)] text-white shadow-md shadow-blue-500/20'
324
+ : 'text-[var(--text-secondary)] hover:bg-[var(--bg-surface)] hover:text-[var(--text-primary)]'
325
+ }`}
326
+ >
327
+ <div className="flex items-center gap-3">
328
+ <TableProperties className={`w-4 h-4 ${activeTable === table ? 'text-blue-100' : 'text-slate-400 group-hover:text-slate-600'}`} />
329
+ <span className="capitalize">{table}</span>
330
+ </div>
331
+ {activeTable === table && <ChevronRight className="w-4 h-4 opacity-70" />}
332
+ </button>
333
+ ))
334
+ )}
335
+ </div>
336
+ </div>
337
+
338
+ {/* Panel Principal */}
339
+ <div className="flex-1 bg-[var(--bg-surface)] flex flex-col h-full min-w-0 relative">
340
+ {/* Header Superior */}
341
+ <header className="px-8 py-6 bg-[var(--bg-primary)] border-b border-[var(--border-color)] flex items-center justify-between sticky top-0 z-20 shadow-sm">
342
+ <div className="flex flex-col">
343
+ <div className="flex items-center gap-2 text-[var(--text-secondary)] text-xs font-bold uppercase tracking-wider mb-1">
344
+ <Server className="w-3.5 h-3.5" />
345
+ <span>{connectors.find(c => c.id === activeConnector)?.name} / {activeTable || '...'}</span>
346
+ </div>
347
+ <h1 className="text-3xl font-extrabold text-[var(--text-primary)] capitalize tracking-tight flex items-center gap-3">
348
+ {activeTable || 'Selecciona un Recurso'}
349
+ {isDataLoading && <Loader2 className="w-5 h-5 animate-spin text-[var(--brand-primary)]" />}
350
+ </h1>
351
+ </div>
352
+
353
+ {activeTable && (
354
+ <div className="flex gap-4 items-center">
355
+ {/* Toggle Mode Segmented Control */}
356
+ <div className="flex bg-[var(--bg-elevated)] border border-[var(--border-color)] rounded-lg p-1 shadow-sm h-[42px] items-center mr-2">
357
+ <button onClick={() => setViewMode('data')}
358
+ className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all duration-200 ${
359
+ viewMode === 'data'
360
+ ? 'bg-[var(--bg-primary)] text-[var(--text-primary)] shadow-sm border border-[var(--border-color)]'
361
+ : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] border border-transparent'
362
+ }`}>
363
+ Registros
364
+ </button>
365
+ <button onClick={() => setViewMode('schema')}
366
+ className={`px-4 py-1.5 text-xs font-bold rounded-md transition-all duration-200 ${
367
+ viewMode === 'schema'
368
+ ? 'bg-[var(--bg-primary)] text-[var(--brand-primary)] shadow-sm border border-[var(--brand-primary)]/30'
369
+ : 'text-[var(--text-secondary)] hover:text-[var(--text-primary)] border border-transparent'
370
+ }`}>
371
+ Estructura
372
+ </button>
373
+ </div>
374
+
375
+ <div className="relative group">
376
+ <Search className="w-4 h-4 text-[var(--text-secondary)] absolute left-3.5 top-1/2 -translate-y-1/2 group-focus-within:text-[var(--brand-primary)] transition-colors" />
377
+ <input
378
+ type="text"
379
+ placeholder={`Buscar en ${activeTable}...`}
380
+ value={searchTerm}
381
+ onChange={(e) => setSearchTerm(e.target.value)}
382
+ className="pl-10 pr-4 py-2.5 rounded-xl border border-[var(--border-color)] bg-[var(--bg-elevated)] text-[var(--text-primary)] text-sm font-medium outline-none focus:ring-2 focus:ring-[var(--brand-primary)]/20 focus:border-[var(--brand-primary)] transition-all w-72 shadow-inner"
383
+ />
384
+ </div>
385
+ <div className="h-8 w-px bg-[var(--border-color)] mx-2"></div>
386
+ <button
387
+ onClick={() => fetchTableData(activeTable)}
388
+ disabled={isDataLoading}
389
+ className="p-2.5 text-[var(--text-secondary)] hover:bg-[var(--bg-surface)] hover:text-[var(--text-primary)] rounded-xl transition-all disabled:opacity-50 border border-transparent hover:border-[var(--border-color)]"
390
+ title="Recargar Data"
391
+ >
392
+ <RefreshCw className={`w-[18px] h-[18px] ${isDataLoading ? 'animate-spin' : ''}`} />
393
+ </button>
394
+ <button
395
+ onClick={openCreateModal}
396
+ disabled={tables.length === 0}
397
+ className="flex items-center gap-2 bg-[var(--brand-primary)] hover:bg-[var(--brand-primary-hover)] text-white px-5 py-2.5 rounded-xl font-bold text-sm shadow-lg shadow-[var(--brand-primary)]/25 transition-all active:scale-95 disabled:opacity-50 disabled:grayscale"
398
+ >
399
+ <Plus className="w-[18px] h-[18px]" />
400
+ Insertar Fila
401
+ </button>
402
+ </div>
403
+ )}
404
+ </header>
405
+
406
+ {/* Área de Grilla de Datos */}
407
+ <div className="flex-1 overflow-hidden p-6 relative flex flex-col">
408
+ {!activeTable ? (
409
+ <div className="m-auto flex flex-col items-center justify-center opacity-40">
410
+ <LayoutGrid className="w-16 h-16 mb-4" />
411
+ <h2 className="text-xl font-bold">Sin Datos Activos</h2>
412
+ <p className="text-sm">Selecciona una tabla en el panel lateral.</p>
413
+ </div>
414
+ ) : (
415
+ <div className="flex-1 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-2xl shadow-sm overflow-hidden flex flex-col relative">
416
+ {/* Pro Data Grid */}
417
+ <div className="overflow-auto flex-1 custom-scrollbar">
418
+ {viewMode === 'schema' ? (
419
+ <table className="w-full text-left text-sm whitespace-nowrap">
420
+ <thead className="bg-[var(--bg-elevated)] sticky top-0 z-10 shadow-[0_1px_2px_rgba(0,0,0,0.05)]">
421
+ <tr>
422
+ <th className="px-6 py-4 text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">Columna</th>
423
+ <th className="px-6 py-4 text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">Tipo de Dato</th>
424
+ <th className="px-6 py-4 text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">Nulabilidad</th>
425
+ <th className="px-6 py-4 text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">Valor por Defecto</th>
426
+ </tr>
427
+ </thead>
428
+ <tbody className="divide-y divide-[var(--border-color)] text-[var(--text-primary)] font-medium">
429
+ {isSchemaLoading ? (
430
+ <tr>
431
+ <td colSpan={4} className="h-48 text-center text-[var(--text-secondary)]">
432
+ <div className="flex flex-col items-center gap-3">
433
+ <Loader2 className="w-8 h-8 animate-spin text-[var(--brand-primary)]" />
434
+ <span>Cargando esquema estructural...</span>
435
+ </div>
436
+ </td>
437
+ </tr>
438
+ ) : (
439
+ schemaData.map((col: any) => (
440
+ <motion.tr
441
+ initial={{ opacity: 0 }}
442
+ animate={{ opacity: 1 }}
443
+ key={col.name}
444
+ className="hover:bg-[var(--bg-surface)]/70 transition-colors"
445
+ >
446
+ <td className="px-6 py-3">
447
+ <span className="font-bold text-[var(--text-primary)]">{col.name}</span>
448
+ </td>
449
+ <td className="px-6 py-3">
450
+ <span className="font-mono text-[11px] text-blue-600 bg-blue-50 px-2 py-1 rounded-md">
451
+ {col.type}
452
+ </span>
453
+ </td>
454
+ <td className="px-6 py-3">
455
+ {col.is_nullable === 'YES'
456
+ ? <span className="opacity-60 text-[11px] font-bold uppercase">Nullable</span>
457
+ : <span className="text-red-600 bg-red-50 px-2 py-0.5 rounded text-[10px] font-bold uppercase">Not Null</span>
458
+ }
459
+ </td>
460
+ <td className="px-6 py-3">
461
+ {col.default_value ? (
462
+ <span className="font-mono text-[11px] text-purple-600 bg-purple-50 px-2 py-1 rounded-md">
463
+ {col.default_value}
464
+ </span>
465
+ ) : <span className="text-gray-400 italic">None</span>}
466
+ </td>
467
+ </motion.tr>
468
+ ))
469
+ )}
470
+ </tbody>
471
+ </table>
472
+ ) : (
473
+ <table className="w-full text-left text-sm whitespace-nowrap">
474
+ <thead className="bg-[var(--bg-elevated)] sticky top-0 z-10 shadow-[0_1px_2px_rgba(0,0,0,0.05)]">
475
+ <tr>
476
+ {columns.map(col => (
477
+ <th
478
+ key={col}
479
+ onClick={() => handleSort(col)}
480
+ className="px-6 py-4 cursor-pointer hover:bg-[var(--bg-surface)] transition-colors group select-none"
481
+ >
482
+ <div className="flex items-center gap-2 text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">
483
+ {col}
484
+ <ArrowUpDown className={`w-3.5 h-3.5 ${sortConfig?.key === col ? 'text-[var(--brand-primary)]' : 'opacity-0 group-hover:opacity-50 transition-opacity'}`} />
485
+ </div>
486
+ </th>
487
+ ))}
488
+ <th className="px-6 py-4 w-24 sticky right-0 bg-[var(--bg-elevated)] backdrop-blur-md shadow-[-4px_0_10px_rgba(0,0,0,0.02)]">
489
+ <span className="sr-only">Actions</span>
490
+ </th>
491
+ </tr>
492
+ </thead>
493
+ <tbody className="divide-y divide-[var(--border-color)] text-[var(--text-primary)] font-medium">
494
+ {processedData.length === 0 ? (
495
+ <tr>
496
+ <td colSpan={columns.length + 1} className="h-48 text-center text-[var(--text-secondary)]">
497
+ {isDataLoading ? (
498
+ <div className="flex flex-col items-center gap-3">
499
+ <Loader2 className="w-8 h-8 animate-spin text-[var(--brand-primary)]" />
500
+ <span>Consultando {activeTable}...</span>
501
+ </div>
502
+ ) : (
503
+ <div className="flex flex-col items-center gap-3 opacity-60">
504
+ <Database className="w-10 h-10" />
505
+ <span>La consulta retornó 0 filas.</span>
506
+ </div>
507
+ )}
508
+ </td>
509
+ </tr>
510
+ ) : (
511
+ processedData.map((row, i) => (
512
+ <motion.tr
513
+ initial={{ opacity: 0 }}
514
+ animate={{ opacity: 1 }}
515
+ transition={{ duration: 0.2, delay: Math.min(i * 0.02, 0.5) }}
516
+ key={row.id || i}
517
+ className="hover:bg-[var(--bg-surface)]/70 transition-colors group"
518
+ >
519
+ {columns.map(col => {
520
+ const val = row[col];
521
+ const isObject = typeof val === 'object' && val !== null;
522
+ const isBoolean = typeof val === 'boolean';
523
+
524
+ return (
525
+ <td key={col} className="px-6 py-3 max-w-[250px] truncate text-ellipsis">
526
+ {isBoolean ? (
527
+ <span className={`px-2 py-1 rounded-md text-[10px] font-bold uppercase tracking-wider ${val ? 'bg-green-100 text-green-700' : 'bg-red-100 text-red-700'}`}>
528
+ {val ? 'TRUE' : 'FALSE'}
529
+ </span>
530
+ ) : isObject ? (
531
+ <span className="font-mono text-[11px] text-emerald-600 bg-emerald-50 px-2 py-1 rounded-md">
532
+ {JSON.stringify(val)}
533
+ </span>
534
+ ) : (
535
+ <span title={String(val)}>{val !== null && val !== undefined ? String(val) : <span className="text-gray-400 italic">null</span>}</span>
536
+ )}
537
+ </td>
538
+ );
539
+ })}
540
+ <td className="px-6 py-3 text-right sticky right-0 bg-[var(--bg-primary)] group-hover:bg-[#f8fafc] transition-colors shadow-[-4px_0_10px_rgba(0,0,0,0.02)] border-l border-transparent group-hover:border-[var(--border-color)]">
541
+ <div className="flex items-center justify-end gap-1 opacity-10 md:opacity-0 group-hover:opacity-100 transition-opacity">
542
+ <button
543
+ onClick={() => openEditModal(row)}
544
+ className="p-1.5 hover:bg-blue-100 text-blue-600 rounded-md transition-colors tooltip-trigger"
545
+ title="Editar Registro"
546
+ >
547
+ <Edit2 className="w-4 h-4" />
548
+ </button>
549
+ <button
550
+ onClick={() => handleDelete(row.id)}
551
+ className="p-1.5 hover:bg-red-100 text-red-600 rounded-md transition-colors tooltip-trigger"
552
+ title="Eliminar Permanente"
553
+ >
554
+ <Trash2 className="w-4 h-4" />
555
+ </button>
556
+ </div>
557
+ </td>
558
+ </motion.tr>
559
+ ))
560
+ )}
561
+ </tbody>
562
+ </table>
563
+ )}
564
+ </div>
565
+
566
+ {/* Footer del Grid */}
567
+ <div className="px-6 py-3 border-t border-[var(--border-color)] bg-[var(--bg-elevated)] text-xs font-semibold text-[var(--text-secondary)] flex justify-between items-center z-10">
568
+ <span>Total Records: {processedData.length}</span>
569
+ <span className="uppercase tracking-wider">Macia Platform v2.0 - Core Gateway</span>
570
+ </div>
571
+ </div>
572
+ )}
573
+ </div>
574
+ </div>
575
+
576
+ {/* Modal Glassmórfico de Edición/Creación Pro */}
577
+ <AnimatePresence>
578
+ {isFormOpen && (
579
+ <motion.div
580
+ initial={{ opacity: 0 }}
581
+ animate={{ opacity: 1 }}
582
+ exit={{ opacity: 0 }}
583
+ className="fixed inset-0 z-50 flex items-center justify-center p-4 bg-[var(--text-primary)]/40 backdrop-blur-md"
584
+ >
585
+ <motion.div
586
+ initial={{ scale: 0.95, opacity: 0, y: 20 }}
587
+ animate={{ scale: 1, opacity: 1, y: 0 }}
588
+ exit={{ scale: 0.95, opacity: 0, y: -20 }}
589
+ transition={{ type: "spring", bounce: 0, duration: 0.3 }}
590
+ className="bg-[var(--bg-primary)] border border-white/20 rounded-3xl shadow-[0_20px_60px_-10px_rgba(0,0,0,0.3)] w-full max-w-2xl overflow-hidden flex flex-col relative"
591
+ >
592
+ {/* Gradiente sutil en el header */}
593
+ <div className="absolute top-0 left-0 w-full h-32 bg-gradient-to-br from-blue-500/10 to-purple-500/5 pointer-events-none"></div>
594
+
595
+ <div className="px-8 py-5 border-b border-[var(--border-color)] flex justify-between items-center relative z-10 bg-[var(--bg-primary)]/80 backdrop-blur-lg">
596
+ <div>
597
+ <h3 className="font-extrabold text-xl text-[var(--text-primary)] tracking-tight">
598
+ {isCreating ? 'Insertar Documento' : 'Modificar Documento'}
599
+ </h3>
600
+ <p className="text-xs text-[var(--text-secondary)] font-medium mt-0.5">
601
+ Tabla destino: <span className="uppercase tracking-wider bg-[var(--bg-surface)] px-1.5 py-0.5 rounded text-[var(--brand-primary)]">{activeTable}</span>
602
+ </p>
603
+ </div>
604
+ <button onClick={() => setIsFormOpen(false)} className="p-2 bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--border-color)] rounded-xl transition-all">
605
+ <X className="w-5 h-5" />
606
+ </button>
607
+ </div>
608
+
609
+ <div className="p-8 space-y-5 max-h-[65vh] overflow-y-auto custom-scrollbar relative z-10">
610
+ {editRecord && Object.keys(editRecord).map((key) => {
611
+ // Protect immutable fields based on Decido OS Postgres patterns
612
+ const isImmutable = (!isCreating && key === 'id') || key === 'created_at' || key === 'updated_at';
613
+ const val = editRecord[key];
614
+ const inferredType = inferInputType(val);
615
+
616
+ return (
617
+ <div key={key} className="flex flex-col gap-1.5">
618
+ <label className="text-[11px] font-bold text-[var(--text-secondary)] uppercase tracking-wider pl-1 flex justify-between">
619
+ {key}
620
+ {isImmutable && <span className="opacity-50 font-mono lowercase">auto-generated</span>}
621
+ </label>
622
+
623
+ {isImmutable ? (
624
+ <input
625
+ disabled
626
+ value={val || 'Depende del Motor DB'}
627
+ className="w-full px-4 py-3 rounded-xl border border-[var(--border-color)] bg-[var(--bg-surface)] text-[var(--text-secondary)] font-mono text-xs cursor-not-allowed opacity-70"
628
+ />
629
+ ) : typeof val === 'boolean' ? (
630
+ <select
631
+ value={String(val)}
632
+ onChange={e => setEditRecord({ ...editRecord, [key]: e.target.value === 'true' })}
633
+ className="w-full px-4 py-3 rounded-xl border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] focus:ring-2 focus:ring-[var(--brand-primary)]/20 focus:border-[var(--brand-primary)] text-sm font-medium outline-none transition-all cursor-pointer shadow-sm"
634
+ >
635
+ <option value="true">Verdadero (TRUE)</option>
636
+ <option value="false">Falso (FALSE)</option>
637
+ </select>
638
+ ) : typeof val === 'object' && val !== null ? (
639
+ <textarea
640
+ rows={4}
641
+ value={JSON.stringify(val, null, 2)}
642
+ onChange={e => {
643
+ try {
644
+ const parsed = JSON.parse(e.target.value);
645
+ setEditRecord({ ...editRecord, [key]: parsed });
646
+ } catch (err) {
647
+ // Provide a mechanism to handle invalid JSON edits later if needed,
648
+ // but for now, just allow arbitrary text entry representing the JSON blob.
649
+ setEditRecord({ ...editRecord, [key]: e.target.value });
650
+ }
651
+ }}
652
+ className="w-full px-4 py-3 rounded-xl border border-[var(--border-color)] bg-[var(--bg-primary)] font-mono text-xs focus:ring-2 focus:ring-[var(--brand-primary)]/20 focus:border-[var(--brand-primary)] outline-none transition-all shadow-sm"
653
+ />
654
+ ) : (
655
+ <input
656
+ type={inferredType}
657
+ value={val !== null ? val : ''}
658
+ onChange={e => setEditRecord({ ...editRecord, [key]: inferredType === 'number' ? Number(e.target.value) : e.target.value })}
659
+ className="w-full px-4 py-3 rounded-xl border border-[var(--border-color)] bg-[var(--bg-primary)] text-[var(--text-primary)] focus:ring-2 focus:ring-[var(--brand-primary)]/20 focus:border-[var(--brand-primary)] text-sm font-medium outline-none transition-all shadow-sm"
660
+ placeholder={`Ingresar ${key}...`}
661
+ />
662
+ )}
663
+ </div>
664
+ )
665
+ })}
666
+ </div>
667
+
668
+ <div className="px-8 py-5 border-t border-[var(--border-color)] bg-[var(--bg-surface)] flex justify-end gap-3 rounded-b-3xl relative z-10">
669
+ <button
670
+ onClick={() => setIsFormOpen(false)}
671
+ className="px-5 py-2.5 rounded-xl text-sm font-bold text-[var(--text-secondary)] hover:text-[var(--text-primary)] hover:bg-[var(--border-color)] transition-all"
672
+ >
673
+ Descartar
674
+ </button>
675
+ <button
676
+ onClick={() => handleSave(editRecord)}
677
+ disabled={isSaving}
678
+ className="flex items-center gap-2 px-6 py-2.5 rounded-xl text-sm font-extrabold text-white bg-gradient-to-r from-[var(--brand-primary)] to-[var(--brand-primary-hover)] shadow-lg shadow-[var(--brand-primary)]/30 transition-all active:scale-95 disabled:opacity-70"
679
+ >
680
+ {isSaving ? <Loader2 className="w-[18px] h-[18px] animate-spin" /> : <Check className="w-[18px] h-[18px]" />}
681
+ {isSaving ? 'Sincronizando...' : 'Confirmar Cambios'}
682
+ </button>
683
+ </div>
684
+ </motion.div>
685
+ </motion.div>
686
+ )}
687
+ </AnimatePresence>
688
+
689
+ <style>{`
690
+ .custom-scrollbar::-webkit-scrollbar {
691
+ width: 6px;
692
+ height: 6px;
693
+ }
694
+ .custom-scrollbar::-webkit-scrollbar-track {
695
+ background: transparent;
696
+ }
697
+ .custom-scrollbar::-webkit-scrollbar-thumb {
698
+ background: var(--border-color);
699
+ border-radius: 10px;
700
+ }
701
+ .custom-scrollbar::-webkit-scrollbar-thumb:hover {
702
+ background: var(--text-secondary);
703
+ }
704
+ `}</style>
705
+ </div>
706
+ );
707
+ };