@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,13 @@
1
+
2
+ Debugger listening on ws://127.0.0.1:62232/fe9a20a2-d2b8-4c56-a8f7-2d0433a57e69
3
+ For help, see: https://nodejs.org/en/docs/inspector
4
+ Debugger attached.
5
+
6
+ > @decido/plugin-chameleon@1.0.0 build /Users/julioramirez/dev/active/OnBoardingDecido/plugins/official/chameleon
7
+ > tsc
8
+
9
+ Debugger listening on ws://127.0.0.1:62248/fc0de670-2b38-4d5c-9475-ab31e7f92dd5
10
+ For help, see: https://nodejs.org/en/docs/inspector
11
+ Debugger attached.
12
+ Waiting for the debugger to disconnect...
13
+ Waiting for the debugger to disconnect...
package/package.json ADDED
@@ -0,0 +1,47 @@
1
+ {
2
+ "name": "@decido/plugin-chameleon",
3
+ "version": "1.0.0",
4
+ "description": "Chameleon UI Engine - Plugin for Decido OS",
5
+ "main": "src/index.ts",
6
+ "types": "src/index.ts",
7
+ "dependencies": {
8
+ "@codemirror/autocomplete": "^6.20.1",
9
+ "@codemirror/state": "^6.4.1",
10
+ "@codemirror/view": "^6.35.0",
11
+ "@dnd-kit/core": "^6.1.0",
12
+ "@dnd-kit/sortable": "^8.0.0",
13
+ "@dnd-kit/utilities": "^3.2.2",
14
+ "@google/genai": "latest",
15
+ "@tanstack/react-virtual": "^3.11.2",
16
+ "@uiw/react-codemirror": "^4.23.7",
17
+ "@xyflow/react": "^12.0.0",
18
+ "framer-motion": "^11.0.0",
19
+ "lucide-react": "^0.300.0",
20
+ "react-router-dom": "^6.22.3",
21
+ "recharts": "^3.8.1",
22
+ "socket.io-client": "^4.8.3",
23
+ "sonner": "^1.4.3",
24
+ "y-websocket": "^3.0.0",
25
+ "yjs": "^13.6.29",
26
+ "zustand": "^4.5.2",
27
+ "@decido/discovery-studio": "0.1.0"
28
+ },
29
+ "devDependencies": {
30
+ "@types/react": "^18.2.43",
31
+ "@types/react-dom": "^18.2.17",
32
+ "typescript": "^5.3.3"
33
+ },
34
+ "peerDependencies": {
35
+ "react": "^18.3.1",
36
+ "react-dom": "^18.3.1",
37
+ "@decido/kernel-bridge": "1.0.0",
38
+ "@decido/shell": "1.0.0",
39
+ "@decido/sdk": "1.0.0"
40
+ },
41
+ "license": "UNLICENSED",
42
+ "scripts": {
43
+ "build": "tsup src/index.ts --format esm --dts --minify --clean",
44
+ "dev": "tsup src/index.ts --format esm --watch",
45
+ "lint": "eslint src/"
46
+ }
47
+ }
package/src/App.tsx ADDED
@@ -0,0 +1,116 @@
1
+ import { useCallback, useEffect } from 'react';
2
+ import { motion, AnimatePresence } from 'framer-motion';
3
+ import { Toaster, toast } from 'sonner';
4
+
5
+ // Vistas Madefront
6
+ import { MadefrontChatView } from './views/MadefrontChatView';
7
+ import { MadefrontKanbanView } from './views/MadefrontKanbanView';
8
+ import { MadefrontExcelView } from './views/MadefrontExcelView';
9
+ import { LoginView } from './views/LoginView';
10
+ import { WorkflowAdminView } from './views/roles/WorkflowAdminView';
11
+ import { KuspideDashboardView } from './views/KuspideDashboardView';
12
+ import { DiscoveryAdminView } from './views/DiscoveryAdminView';
13
+ import { DatabaseAdminView } from './views/DatabaseAdminView';
14
+ import { DatawayStudioView } from './views/DatawayStudioView';
15
+
16
+ // Vistas Roles
17
+ import { CajaView } from './views/roles/CajaView';
18
+ import { PlantaView } from './views/roles/PlantaView';
19
+ import { VentasView } from './views/roles/VentasView';
20
+ import { DespachoView } from './views/roles/DespachoView';
21
+
22
+ // Utilidades y Stores
23
+ import { ViewType, AgentCommand } from './types';
24
+ import { playUISound } from './utils/sounds';
25
+ import { useAuthStore } from './stores/authStore';
26
+ import { useUIStore } from './stores/uiStore';
27
+ import { useSocketSync } from './hooks/useSocketSync';
28
+
29
+ export default function App() {
30
+ const { currentRole, setRole } = useAuthStore();
31
+ const { currentView, setCurrentView, setSearchTerm } = useUIStore();
32
+
33
+ // Initialize WebSocket Sync
34
+ useSocketSync();
35
+
36
+ // Escuchar eventos globales para cambiar sub-vistas desde el Main Chat
37
+ useEffect(() => {
38
+ const handleSetView = (e: any) => {
39
+ const view = e.detail?.view;
40
+ if (view) {
41
+ setRole('NONE' as any, 'Agente Asistente');
42
+ setCurrentView(view as ViewType);
43
+ }
44
+ };
45
+ window.addEventListener('chameleon:set-view', handleSetView);
46
+ return () => window.removeEventListener('chameleon:set-view', handleSetView);
47
+ }, [setRole, setCurrentView]);
48
+
49
+ const renderView = () => {
50
+ switch (currentRole) {
51
+ case 'CAJA':
52
+ return <CajaView />;
53
+ case 'PLANTA':
54
+ return <PlantaView />;
55
+ case 'GERENCIA':
56
+ return <VentasView />;
57
+ case 'DESPACHO':
58
+ return <DespachoView />;
59
+ case 'NONE':
60
+ switch (currentView) {
61
+ case 'madefront_chat':
62
+ return <MadefrontChatView />;
63
+ case 'madefront_kanban':
64
+ return <MadefrontKanbanView />;
65
+ case 'madefront_excel':
66
+ return <MadefrontExcelView />;
67
+ case 'workflow_admin':
68
+ return <WorkflowAdminView />;
69
+ case 'kuspide_dashboard':
70
+ return <KuspideDashboardView />;
71
+ case 'discovery_admin':
72
+ return <DiscoveryAdminView />;
73
+ case 'database_admin':
74
+ return <DatabaseAdminView />;
75
+ case 'dataway_studio':
76
+ return <DatawayStudioView />;
77
+ default:
78
+ return <LoginView />;
79
+ }
80
+ default:
81
+ return <LoginView />;
82
+ }
83
+ };
84
+
85
+ return (
86
+ <div className="w-full h-full flex flex-col overflow-y-auto overflow-x-hidden bg-[#05070a] text-gray-900 font-sans transition-colors duration-500 relative min-w-0">
87
+ <div className="w-full flex-1 transition-all duration-700 min-h-0 min-w-0 relative overflow-x-hidden">
88
+ <AnimatePresence mode="wait">
89
+ <motion.div
90
+ key={`${currentRole}-${currentView}`}
91
+ initial={{ opacity: 0, y: 10 }}
92
+ animate={{ opacity: 1, y: 0 }}
93
+ exit={{ opacity: 0, y: -10 }}
94
+ transition={{ type: "tween", duration: 0.2 }}
95
+ className="w-full h-full overflow-x-hidden"
96
+ >
97
+ {renderView()}
98
+ </motion.div>
99
+ </AnimatePresence>
100
+ </div>
101
+
102
+ {/* Botón flotante para regresar al menú principal (opcional) */}
103
+ {currentRole !== 'NONE' && (
104
+ <button
105
+ onClick={() => setRole('NONE' as any, 'usuario')}
106
+ className="fixed top-4 right-4 z-40 bg-white/10 hover:bg-white/20 backdrop-blur-md border border-white/20 text-white px-4 py-2 rounded-full shadow-lg font-bold text-sm transition-all"
107
+ >
108
+ Cambiar de Terminal
109
+ </button>
110
+ )}
111
+
112
+ {/* Chat global siempre presente */}
113
+ <Toaster richColors position="bottom-right" />
114
+ </div>
115
+ );
116
+ }
@@ -0,0 +1,152 @@
1
+ import React, { useMemo } from 'react';
2
+ import {
3
+ ResponsiveContainer, BarChart, Bar, XAxis, YAxis, CartesianGrid,
4
+ Tooltip, Legend, LineChart, Line, AreaChart, Area
5
+ } from 'recharts';
6
+ import { BarChart3, LineChart as LineIcon, PieChart, Activity } from 'lucide-react';
7
+
8
+ interface DatawayDashboardProps {
9
+ data: any[];
10
+ columns: string[];
11
+ }
12
+
13
+ export const DatawayDashboard: React.FC<DatawayDashboardProps> = ({ data, columns }) => {
14
+ const [chartType, setChartType] = React.useState<'bar' | 'line' | 'area'>('bar');
15
+
16
+ // Auto-detect dimensions and metrics
17
+ const { dimension, metrics } = useMemo(() => {
18
+ if (data.length === 0 || columns.length === 0) return { dimension: '', metrics: [] };
19
+
20
+ // Find first string/categorical column for X axis
21
+ let xCol = columns.find(col => {
22
+ const val = data.find(r => r[col] !== null && r[col] !== undefined)?.[col];
23
+ return val !== undefined && (typeof val === 'string' || isNaN(Number(val)));
24
+ });
25
+
26
+ // Fallback: Use first column if no categorical column was found
27
+ xCol = xCol || columns[0];
28
+
29
+ // Find all numerical columns for Y axis
30
+ const yCols = columns.filter(col => {
31
+ if (col === xCol) return false;
32
+ const val = data.find(r => r[col] !== null && r[col] !== undefined)?.[col];
33
+ return val !== undefined && !isNaN(Number(val)) && String(val).trim() !== '';
34
+ });
35
+
36
+ return { dimension: xCol, metrics: yCols };
37
+ }, [data, columns]);
38
+
39
+ if (data.length === 0) {
40
+ return (
41
+ <div className="flex-1 flex flex-col items-center justify-center opacity-40">
42
+ <Activity className="w-16 h-16 mb-4" />
43
+ <h2 className="text-xl font-bold">Sin Datos para Graficar</h2>
44
+ <p className="text-sm">La tabla procesada está vacía.</p>
45
+ </div>
46
+ );
47
+ }
48
+
49
+ if (metrics.length === 0) {
50
+ return (
51
+ <div className="flex-1 flex flex-col items-center justify-center opacity-60">
52
+ <BarChart3 className="w-16 h-16 mb-4" />
53
+ <h2 className="text-lg font-bold">Sin Columnas Numéricas</h2>
54
+ <p className="text-sm text-center max-w-sm mt-2">
55
+ El motor de gráficos necesita al menos una métrica numérica para generar la visualización. (Usa M-Script para castear o generar números).
56
+ </p>
57
+ </div>
58
+ );
59
+ }
60
+
61
+ // Colors generator
62
+ const colors = ['#6366f1', '#ec4899', '#14b8a6', '#f59e0b', '#8b5cf6', '#10b981'];
63
+
64
+ return (
65
+ <div className="flex-1 flex flex-col h-full bg-[var(--bg-primary)] p-6">
66
+ <div className="flex justify-between items-center mb-6">
67
+ <div>
68
+ <h2 className="text-lg font-bold text-[var(--text-primary)]">Analytics Auto-Dashboard</h2>
69
+ <p className="text-[11px] font-medium text-[var(--text-secondary)] uppercase tracking-wider">
70
+ Dimensión: <span className="text-[var(--brand-primary)]">{dimension}</span> |
71
+ Métricas: {metrics.join(', ')}
72
+ </p>
73
+ </div>
74
+
75
+ <div className="flex bg-[var(--bg-elevated)] p-1 rounded-lg border border-[var(--border-color)]">
76
+ <button
77
+ onClick={() => setChartType('bar')}
78
+ className={`p-2 rounded-md transition-all ${chartType === 'bar' ? 'bg-white shadow-sm text-[var(--brand-primary)]' : 'text-[var(--text-secondary)] hover:bg-[var(--bg-surface)]'}`}
79
+ title="Gráfico de Barras"
80
+ >
81
+ <BarChart3 className="w-4 h-4" />
82
+ </button>
83
+ <button
84
+ onClick={() => setChartType('line')}
85
+ className={`p-2 rounded-md transition-all ${chartType === 'line' ? 'bg-white shadow-sm text-[var(--brand-primary)]' : 'text-[var(--text-secondary)] hover:bg-[var(--bg-surface)]'}`}
86
+ title="Gráfico de Líneas"
87
+ >
88
+ <LineIcon className="w-4 h-4" />
89
+ </button>
90
+ <button
91
+ onClick={() => setChartType('area')}
92
+ className={`p-2 rounded-md transition-all ${chartType === 'area' ? 'bg-white shadow-sm text-[var(--brand-primary)]' : 'text-[var(--text-secondary)] hover:bg-[var(--bg-surface)]'}`}
93
+ title="Gráfico de Área"
94
+ >
95
+ <PieChart className="w-4 h-4" />
96
+ </button>
97
+ </div>
98
+ </div>
99
+
100
+ <div className="flex-1 min-h-0 bg-white border border-[var(--border-color)] rounded-xl p-4 shadow-inner">
101
+ <ResponsiveContainer width="100%" height="100%">
102
+ {chartType === 'bar' ? (
103
+ <BarChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
104
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
105
+ <XAxis
106
+ dataKey={dimension}
107
+ tick={{fontSize: 12, fill: '#64748b'}}
108
+ axisLine={{stroke: '#cbd5e1'}}
109
+ tickLine={false}
110
+ />
111
+ <YAxis
112
+ tick={{fontSize: 12, fill: '#64748b'}}
113
+ axisLine={false}
114
+ tickLine={false}
115
+ />
116
+ <Tooltip
117
+ contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }}
118
+ cursor={{fill: '#f1f5f9'}}
119
+ />
120
+ <Legend wrapperStyle={{ paddingTop: '20px' }} />
121
+ {metrics.map((metric, idx) => (
122
+ <Bar key={metric} dataKey={metric} fill={colors[idx % colors.length]} radius={[4, 4, 0, 0]} />
123
+ ))}
124
+ </BarChart>
125
+ ) : chartType === 'line' ? (
126
+ <LineChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
127
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
128
+ <XAxis dataKey={dimension} tick={{fontSize: 12, fill: '#64748b'}} axisLine={{stroke: '#cbd5e1'}} tickLine={false} />
129
+ <YAxis tick={{fontSize: 12, fill: '#64748b'}} axisLine={false} tickLine={false} />
130
+ <Tooltip contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
131
+ <Legend wrapperStyle={{ paddingTop: '20px' }} />
132
+ {metrics.map((metric, idx) => (
133
+ <Line key={metric} type="monotone" dataKey={metric} stroke={colors[idx % colors.length]} strokeWidth={3} dot={{r: 4}} activeDot={{r: 6}} />
134
+ ))}
135
+ </LineChart>
136
+ ) : (
137
+ <AreaChart data={data} margin={{ top: 20, right: 30, left: 20, bottom: 20 }}>
138
+ <CartesianGrid strokeDasharray="3 3" vertical={false} stroke="#e2e8f0" />
139
+ <XAxis dataKey={dimension} tick={{fontSize: 12, fill: '#64748b'}} axisLine={{stroke: '#cbd5e1'}} tickLine={false} />
140
+ <YAxis tick={{fontSize: 12, fill: '#64748b'}} axisLine={false} tickLine={false} />
141
+ <Tooltip contentStyle={{ borderRadius: '8px', border: 'none', boxShadow: '0 4px 6px -1px rgb(0 0 0 / 0.1)' }} />
142
+ <Legend wrapperStyle={{ paddingTop: '20px' }} />
143
+ {metrics.map((metric, idx) => (
144
+ <Area key={metric} type="monotone" dataKey={metric} fill={colors[idx % colors.length]} stroke={colors[idx % colors.length]} fillOpacity={0.3} strokeWidth={2} />
145
+ ))}
146
+ </AreaChart>
147
+ )}
148
+ </ResponsiveContainer>
149
+ </div>
150
+ </div>
151
+ );
152
+ };
@@ -0,0 +1,132 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Filter, Calendar, Type, Hash } from 'lucide-react';
3
+
4
+ interface FilterModalProps {
5
+ isOpen: boolean;
6
+ column: string;
7
+ colType: 'text' | 'number' | 'date';
8
+ onClose: () => void;
9
+ onApply: (col: string, operator: string, value: string) => void;
10
+ }
11
+
12
+ export const DatawayFilterModal: React.FC<FilterModalProps> = ({ isOpen, column, colType, onClose, onApply }) => {
13
+ const [operator, setOperator] = useState('=');
14
+ const [value, setValue] = useState('');
15
+
16
+ useEffect(() => {
17
+ if (isOpen) {
18
+ setOperator('=');
19
+ setValue('');
20
+ }
21
+ }, [isOpen]);
22
+
23
+ if (!isOpen) return null;
24
+
25
+ const operators = {
26
+ text: [
27
+ { val: '=', label: 'Es igual a' },
28
+ { val: 'contains', label: 'Contiene' },
29
+ { val: 'starts_with', label: 'Empieza con' }
30
+ ],
31
+ number: [
32
+ { val: '=', label: 'Es igual a' },
33
+ { val: '>', label: 'Es mayor que' },
34
+ { val: '<', label: 'Es menor que' },
35
+ { val: '>=', label: 'Mayor o igual' },
36
+ { val: '<=', label: 'Menor o igual' }
37
+ ],
38
+ date: [
39
+ { val: '=', label: 'Es exactamente' },
40
+ { val: '<', label: 'Antes de' },
41
+ { val: '>', label: 'Después de' }
42
+ ]
43
+ };
44
+
45
+ const currentOperators = operators[colType];
46
+
47
+ const handleSubmit = (e: React.FormEvent) => {
48
+ e.preventDefault();
49
+ if (!value.trim()) return;
50
+ onApply(column, operator, value.trim());
51
+ };
52
+
53
+ return (
54
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
55
+ <div className="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden animate-in zoom-in-95 duration-200">
56
+ <div className="flex items-center justify-between p-4 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
57
+ <h3 className="font-bold flex items-center gap-2 text-[var(--text-primary)]">
58
+ <Filter className="w-4 h-4 text-[var(--brand-primary)]" />
59
+ Filtro Inteligente
60
+ </h3>
61
+ <button onClick={onClose} className="p-1 hover:bg-[var(--bg-surface)] rounded-md transition-colors text-[var(--text-secondary)]">
62
+ <X className="w-4 h-4" />
63
+ </button>
64
+ </div>
65
+
66
+ <form onSubmit={handleSubmit} className="p-5 space-y-4">
67
+ <div className="flex flex-col gap-1">
68
+ <label className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider flex items-center gap-1.5">
69
+ {colType === 'text' && <Type className="w-3.5 h-3.5" />}
70
+ {colType === 'number' && <Hash className="w-3.5 h-3.5" />}
71
+ {colType === 'date' && <Calendar className="w-3.5 h-3.5" />}
72
+ Columna Objetivo
73
+ </label>
74
+ <div className="px-3 py-2 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-sm font-semibold text-[var(--text-primary)]">
75
+ {column}
76
+ <span className="ml-2 text-[10px] bg-[var(--brand-primary)]/10 text-[var(--brand-primary)] px-2 py-0.5 rounded-full uppercase">
77
+ {colType}
78
+ </span>
79
+ </div>
80
+ </div>
81
+
82
+ <div className="flex flex-col gap-1">
83
+ <label className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">
84
+ Condición
85
+ </label>
86
+ <select
87
+ value={operator}
88
+ onChange={(e) => setOperator(e.target.value)}
89
+ className="w-full px-3 py-2 bg-[var(--bg-surface)] border border-[var(--border-color)] rounded-lg text-sm outline-none focus:ring-2 focus:ring-[var(--brand-primary)]/50 transition-shadow appearance-none"
90
+ >
91
+ {currentOperators.map(op => (
92
+ <option key={op.val} value={op.val}>{op.label}</option>
93
+ ))}
94
+ </select>
95
+ </div>
96
+
97
+ <div className="flex flex-col gap-1">
98
+ <label className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider">
99
+ Valor
100
+ </label>
101
+ <input
102
+ type={colType === 'date' ? 'date' : colType === 'number' ? 'number' : 'text'}
103
+ step="any"
104
+ autoFocus
105
+ value={value}
106
+ onChange={(e) => setValue(e.target.value)}
107
+ placeholder="Escribe el valor..."
108
+ className="w-full px-3 py-2 bg-transparent border border-[var(--border-color)] rounded-lg text-sm outline-none focus:ring-2 focus:ring-[var(--brand-primary)]/50 transition-shadow"
109
+ />
110
+ </div>
111
+
112
+ <div className="pt-2 flex gap-3">
113
+ <button
114
+ type="button"
115
+ onClick={onClose}
116
+ className="flex-1 px-4 py-2 border border-[var(--border-color)] rounded-xl text-sm font-medium hover:bg-[var(--bg-surface)] transition-colors"
117
+ >
118
+ Cancelar
119
+ </button>
120
+ <button
121
+ type="submit"
122
+ disabled={!value.trim()}
123
+ className="flex-1 px-4 py-2 bg-[var(--brand-primary)] hover:opacity-90 text-white rounded-xl text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed"
124
+ >
125
+ Aplicar Filtro
126
+ </button>
127
+ </div>
128
+ </form>
129
+ </div>
130
+ </div>
131
+ );
132
+ };
@@ -0,0 +1,120 @@
1
+ import React, { useState, useEffect } from 'react';
2
+ import { X, Calculator, Hash, Edit3 } from 'lucide-react';
3
+
4
+ interface MathModalProps {
5
+ isOpen: boolean;
6
+ columns: string[];
7
+ onClose: () => void;
8
+ onApply: (newCol: string, leftCol: string, operator: string, rightOperand: string) => void;
9
+ }
10
+
11
+ export const DatawayMathModal: React.FC<MathModalProps> = ({ isOpen, columns, onClose, onApply }) => {
12
+ const [newColName, setNewColName] = useState('');
13
+ const [leftCol, setLeftCol] = useState(columns[0] || '');
14
+ const [operator, setOperator] = useState('+');
15
+ const [rightOperand, setRightOperand] = useState('');
16
+
17
+ useEffect(() => {
18
+ if (isOpen) {
19
+ setNewColName('');
20
+ setLeftCol(columns[0] || '');
21
+ setOperator('+');
22
+ setRightOperand('');
23
+ }
24
+ }, [isOpen, columns]);
25
+
26
+ if (!isOpen) return null;
27
+
28
+ const handleSubmit = (e: React.FormEvent) => {
29
+ e.preventDefault();
30
+ if (!newColName.trim() || !rightOperand.trim()) return;
31
+ onApply(newColName.trim(), leftCol, operator, rightOperand.trim());
32
+ };
33
+
34
+ return (
35
+ <div className="fixed inset-0 z-[100] flex items-center justify-center bg-black/50 backdrop-blur-sm p-4 animate-in fade-in duration-200">
36
+ <div className="bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-2xl shadow-2xl w-full max-w-sm overflow-hidden animate-in zoom-in-95 duration-200">
37
+ <div className="flex items-center justify-between p-4 border-b border-[var(--border-color)] bg-[var(--bg-elevated)]">
38
+ <h3 className="font-bold flex items-center gap-2 text-[var(--text-primary)]">
39
+ <Calculator className="w-4 h-4 text-purple-600" />
40
+ Columna Calculada
41
+ </h3>
42
+ <button onClick={onClose} className="p-1 hover:bg-[var(--bg-surface)] rounded-md transition-colors text-[var(--text-secondary)]">
43
+ <X className="w-4 h-4" />
44
+ </button>
45
+ </div>
46
+
47
+ <form onSubmit={handleSubmit} className="p-5 space-y-4">
48
+ <div className="flex flex-col gap-1">
49
+ <label className="text-xs font-bold text-[var(--text-secondary)] uppercase tracking-wider flex items-center gap-1.5">
50
+ <Edit3 className="w-3.5 h-3.5" /> Nombre Nueva Columna
51
+ </label>
52
+ <input
53
+ type="text"
54
+ autoFocus
55
+ value={newColName}
56
+ onChange={(e) => setNewColName(e.target.value)}
57
+ placeholder="Ej. Ingresos Netos"
58
+ className="w-full px-3 py-2 bg-transparent border border-[var(--border-color)] rounded-lg text-sm outline-none focus:ring-2 focus:ring-purple-600/50 transition-shadow"
59
+ />
60
+ </div>
61
+
62
+ <div className="flex bg-[var(--bg-surface)] p-3 rounded-xl border border-[var(--border-color)] gap-3 items-center">
63
+ <div className="flex-1 flex flex-col gap-1">
64
+ <label className="text-[10px] font-bold text-[var(--text-secondary)] uppercase">Columna (A)</label>
65
+ <select
66
+ value={leftCol}
67
+ onChange={(e) => setLeftCol(e.target.value)}
68
+ className="w-full px-2 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-md text-sm outline-none"
69
+ >
70
+ {columns.map(col => <option key={col} value={col}>{col}</option>)}
71
+ </select>
72
+ </div>
73
+
74
+ <div className="w-12 flex flex-col gap-1">
75
+ <label className="text-[10px] font-bold text-[var(--text-secondary)] uppercase text-center">Op</label>
76
+ <select
77
+ value={operator}
78
+ onChange={(e) => setOperator(e.target.value)}
79
+ className="w-full px-1 py-1.5 bg-purple-50 text-purple-700 border border-purple-200 rounded-md text-sm outline-none text-center font-bold font-mono"
80
+ >
81
+ <option value="+">+</option>
82
+ <option value="-">-</option>
83
+ <option value="*">*</option>
84
+ <option value="/">/</option>
85
+ </select>
86
+ </div>
87
+
88
+ <div className="flex-1 flex flex-col gap-1">
89
+ <label className="text-[10px] font-bold text-[var(--text-secondary)] uppercase" title="Puede ser otra columna o un número fijo">Columna o # (B)</label>
90
+ <input
91
+ type="text"
92
+ value={rightOperand}
93
+ onChange={(e) => setRightOperand(e.target.value)}
94
+ placeholder="Col o #..."
95
+ className="w-full px-2 py-1.5 bg-[var(--bg-primary)] border border-[var(--border-color)] rounded-md text-sm outline-none"
96
+ />
97
+ </div>
98
+ </div>
99
+
100
+ <div className="pt-2 flex gap-3">
101
+ <button
102
+ type="button"
103
+ onClick={onClose}
104
+ className="flex-1 px-4 py-2 border border-[var(--border-color)] rounded-xl text-sm font-medium hover:bg-[var(--bg-surface)] transition-colors"
105
+ >
106
+ Cancelar
107
+ </button>
108
+ <button
109
+ type="submit"
110
+ disabled={!newColName.trim() || !rightOperand.trim()}
111
+ className="flex-1 px-4 py-2 bg-purple-600 hover:bg-purple-700 text-white rounded-xl text-sm font-medium transition-all disabled:opacity-50 disabled:cursor-not-allowed flex items-center justify-center gap-2"
112
+ >
113
+ <Calculator className="w-4 h-4" /> Calcular
114
+ </button>
115
+ </div>
116
+ </form>
117
+ </div>
118
+ </div>
119
+ );
120
+ };