@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.
- package/.turbo/turbo-build.log +13 -0
- package/package.json +47 -0
- package/src/App.tsx +116 -0
- package/src/components/DatawayDashboard.tsx +152 -0
- package/src/components/DatawayFilterModal.tsx +132 -0
- package/src/components/DatawayMathModal.tsx +120 -0
- package/src/components/DatawayOrchestratorCanvas.tsx +345 -0
- package/src/components/DatawayPipelineManager.tsx +227 -0
- package/src/components/DatawaySchemaMapper.tsx +291 -0
- package/src/components/FormulaBar.tsx +242 -0
- package/src/hooks/useSocketSync.ts +104 -0
- package/src/index.css +18 -0
- package/src/index.ts +121 -0
- package/src/logic/rules.ts +39 -0
- package/src/logic/workflowGuard.ts +45 -0
- package/src/main.tsx +10 -0
- package/src/services/gemini.ts +110 -0
- package/src/store/datawayStore.ts +26 -0
- package/src/stores/authStore.ts +26 -0
- package/src/stores/orderStore.ts +263 -0
- package/src/stores/uiStore.ts +19 -0
- package/src/types.ts +39 -0
- package/src/useAgent.ts +89 -0
- package/src/utils/sounds.ts +52 -0
- package/src/views/DatabaseAdminView.tsx +707 -0
- package/src/views/DatawayStudioView.tsx +959 -0
- package/src/views/DiscoveryAdminView.tsx +59 -0
- package/src/views/KuspideDashboardView.tsx +144 -0
- package/src/views/LoginView.tsx +122 -0
- package/src/views/MadefrontChatView.tsx +174 -0
- package/src/views/MadefrontExcelView.tsx +95 -0
- package/src/views/MadefrontKanbanView.tsx +292 -0
- package/src/views/roles/CajaView.tsx +76 -0
- package/src/views/roles/DespachoView.tsx +100 -0
- package/src/views/roles/PlantaView.tsx +83 -0
- package/src/views/roles/VentasView.tsx +101 -0
- package/src/views/roles/WorkflowAdminView.tsx +62 -0
- 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
|
+
};
|