@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,291 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
ReactFlow,
|
|
4
|
+
Background,
|
|
5
|
+
Controls,
|
|
6
|
+
MiniMap,
|
|
7
|
+
useNodesState,
|
|
8
|
+
useEdgesState,
|
|
9
|
+
Handle,
|
|
10
|
+
Position,
|
|
11
|
+
MarkerType,
|
|
12
|
+
Edge
|
|
13
|
+
} from '@xyflow/react';
|
|
14
|
+
import '@xyflow/react/dist/style.css';
|
|
15
|
+
|
|
16
|
+
interface SchemaColumn {
|
|
17
|
+
name: string;
|
|
18
|
+
type: string;
|
|
19
|
+
formula?: string;
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
interface SchemaData {
|
|
23
|
+
columns: SchemaColumn[];
|
|
24
|
+
inferences: string[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
interface TableNodeProps {
|
|
28
|
+
data: {
|
|
29
|
+
title: string;
|
|
30
|
+
columns: SchemaColumn[];
|
|
31
|
+
side: 'source' | 'target';
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// Custom Node for displaying a Database Table / Sheet Schema
|
|
36
|
+
const TableNode = ({ data }: TableNodeProps) => {
|
|
37
|
+
return (
|
|
38
|
+
<div className="bg-slate-900 border border-slate-700 rounded-lg shadow-xl w-72 text-sm overflow-hidden font-mono">
|
|
39
|
+
{/* Header */}
|
|
40
|
+
<div className={`px-4 py-2 font-bold text-white uppercase tracking-wider flex items-center justify-between ${data.side === 'source' ? 'bg-indigo-600' : 'bg-emerald-600'}`}>
|
|
41
|
+
<span>{data.title}</span>
|
|
42
|
+
<span className="text-xs opacity-75">{data.columns.length} cols</span>
|
|
43
|
+
</div>
|
|
44
|
+
|
|
45
|
+
{/* Columns list */}
|
|
46
|
+
<div className="flex flex-col">
|
|
47
|
+
{data.columns.map((col, idx) => (
|
|
48
|
+
<div key={col.name} className="relative px-4 py-2 border-b border-slate-800 flex items-center justify-between hover:bg-slate-800 transition-colors group">
|
|
49
|
+
{/* Source Handles go on the Right, Target Handles on the Left */}
|
|
50
|
+
{data.side === 'source' && (
|
|
51
|
+
<Handle
|
|
52
|
+
type="source"
|
|
53
|
+
position={Position.Right}
|
|
54
|
+
id={`src-${col.name}`}
|
|
55
|
+
className="w-3 h-3 bg-indigo-400 !border-2 !border-slate-900 right-[-6px]"
|
|
56
|
+
/>
|
|
57
|
+
)}
|
|
58
|
+
{data.side === 'target' && (
|
|
59
|
+
<Handle
|
|
60
|
+
type="target"
|
|
61
|
+
position={Position.Left}
|
|
62
|
+
id={`tgt-${col.name}`}
|
|
63
|
+
className="w-3 h-3 bg-emerald-400 !border-2 !border-slate-900 left-[-6px]"
|
|
64
|
+
/>
|
|
65
|
+
)}
|
|
66
|
+
|
|
67
|
+
<div className="flex items-center gap-2 overflow-hidden">
|
|
68
|
+
<span className={`w-2 h-2 rounded-full flex-shrink-0 ${data.side === 'source' ? 'bg-indigo-500' : 'bg-emerald-500'}`} />
|
|
69
|
+
<span className="text-slate-200 truncate font-semibold" title={col.name}>{col.name}</span>
|
|
70
|
+
</div>
|
|
71
|
+
<div className="flex flex-col items-end">
|
|
72
|
+
<span className="text-xs text-slate-500">{col.type}</span>
|
|
73
|
+
{col.formula && (
|
|
74
|
+
<span className="text-[10px] text-cyan-400 opacity-0 group-hover:opacity-100 transition-opacity absolute right-6 bg-slate-900 px-1 rounded z-10" title={col.formula}>
|
|
75
|
+
{col.formula}
|
|
76
|
+
</span>
|
|
77
|
+
)}
|
|
78
|
+
</div>
|
|
79
|
+
</div>
|
|
80
|
+
))}
|
|
81
|
+
</div>
|
|
82
|
+
</div>
|
|
83
|
+
);
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
const nodeTypes = {
|
|
87
|
+
tableNode: TableNode,
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
interface DatawaySchemaMapperProps {
|
|
91
|
+
sourceConnector: string;
|
|
92
|
+
sourceResource: string;
|
|
93
|
+
targetConnector: string;
|
|
94
|
+
targetResource: string;
|
|
95
|
+
onClose: () => void;
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
export const DatawaySchemaMapper: React.FC<DatawaySchemaMapperProps> = ({
|
|
99
|
+
sourceConnector,
|
|
100
|
+
sourceResource,
|
|
101
|
+
targetConnector,
|
|
102
|
+
targetResource,
|
|
103
|
+
onClose
|
|
104
|
+
}) => {
|
|
105
|
+
const [nodes, setNodes, onNodesChange] = useNodesState([]);
|
|
106
|
+
const [edges, setEdges, onEdgesChange] = useEdgesState([]);
|
|
107
|
+
const [isLoading, setIsLoading] = useState(true);
|
|
108
|
+
const [stats, setStats] = useState({ mapped: 0, missingSource: 0, missingTarget: 0 });
|
|
109
|
+
|
|
110
|
+
const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
|
|
111
|
+
|
|
112
|
+
useEffect(() => {
|
|
113
|
+
const fetchSchemaMap = async () => {
|
|
114
|
+
try {
|
|
115
|
+
const res = await fetch(`${baseUrl}/api/dataway/schema/mapper?sourceConnector=${sourceConnector}&sourceResource=${sourceResource}&targetConnector=${targetConnector}&targetResource=${targetResource}`);
|
|
116
|
+
if (!res.ok) throw new Error("Failed to fetch schema mapping");
|
|
117
|
+
const data: { sourceSchema: SchemaData, targetSchema: SchemaData } = await res.json();
|
|
118
|
+
|
|
119
|
+
// 1. Build Nodes
|
|
120
|
+
const initialNodes: any[] = [
|
|
121
|
+
{
|
|
122
|
+
id: 'source-table',
|
|
123
|
+
type: 'tableNode',
|
|
124
|
+
position: { x: 100, y: 100 },
|
|
125
|
+
data: {
|
|
126
|
+
title: sourceResource,
|
|
127
|
+
columns: data.sourceSchema.columns,
|
|
128
|
+
side: 'source'
|
|
129
|
+
}
|
|
130
|
+
},
|
|
131
|
+
{
|
|
132
|
+
id: 'target-table',
|
|
133
|
+
type: 'tableNode',
|
|
134
|
+
position: { x: 700, y: 100 },
|
|
135
|
+
data: {
|
|
136
|
+
title: targetResource,
|
|
137
|
+
columns: data.targetSchema.columns,
|
|
138
|
+
side: 'target'
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
];
|
|
142
|
+
|
|
143
|
+
// Sub-nodes for Google Sheet references
|
|
144
|
+
if (data.sourceSchema.inferences && data.sourceSchema.inferences.length > 0) {
|
|
145
|
+
data.sourceSchema.inferences.forEach((inf, i) => {
|
|
146
|
+
initialNodes.push({
|
|
147
|
+
id: `inf-${inf}`,
|
|
148
|
+
type: 'default',
|
|
149
|
+
position: { x: 100 + (i * 150), y: -100 },
|
|
150
|
+
data: { label: `📎 Ref: ${inf}` },
|
|
151
|
+
style: { background: '#1e293b', color: '#94a3b8', border: '1px solid #334155', borderRadius: '8px', padding: '10px' }
|
|
152
|
+
});
|
|
153
|
+
});
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
setNodes(initialNodes);
|
|
157
|
+
|
|
158
|
+
// 2. Build Edges (Auto-Map by column name similarity)
|
|
159
|
+
const initialEdges: Edge[] = [];
|
|
160
|
+
let mappedCount = 0;
|
|
161
|
+
|
|
162
|
+
const targetColNames = data.targetSchema.columns.map(c => c.name.toLowerCase());
|
|
163
|
+
|
|
164
|
+
data.sourceSchema.columns.forEach(srcCol => {
|
|
165
|
+
const srcName = srcCol.name.toLowerCase();
|
|
166
|
+
// Basic exact match or underscore/space fuzzy
|
|
167
|
+
const matchIdx = targetColNames.findIndex(tgtName => {
|
|
168
|
+
const s = srcName.replace(/[^a-z0-9]/g, '');
|
|
169
|
+
const t = tgtName.replace(/[^a-z0-9]/g, '');
|
|
170
|
+
return s === t;
|
|
171
|
+
});
|
|
172
|
+
|
|
173
|
+
if (matchIdx !== -1) {
|
|
174
|
+
const targetCol = data.targetSchema.columns[matchIdx];
|
|
175
|
+
initialEdges.push({
|
|
176
|
+
id: `e-${srcCol.name}-${targetCol.name}`,
|
|
177
|
+
source: 'source-table',
|
|
178
|
+
target: 'target-table',
|
|
179
|
+
sourceHandle: `src-${srcCol.name}`,
|
|
180
|
+
targetHandle: `tgt-${targetCol.name}`,
|
|
181
|
+
animated: true,
|
|
182
|
+
style: { stroke: '#10b981', strokeWidth: 2 },
|
|
183
|
+
markerEnd: { type: MarkerType.ArrowClosed, color: '#10b981' }
|
|
184
|
+
});
|
|
185
|
+
mappedCount++;
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
// Also trace formula inferences natively to the sub-nodes visually
|
|
189
|
+
if (srcCol.formula) {
|
|
190
|
+
data.sourceSchema.inferences.forEach(inf => {
|
|
191
|
+
if (srcCol.formula?.includes(inf)) {
|
|
192
|
+
initialEdges.push({
|
|
193
|
+
id: `infe-${srcCol.name}-${inf}`,
|
|
194
|
+
source: `inf-${inf}`,
|
|
195
|
+
target: 'source-table',
|
|
196
|
+
targetHandle: `src-${srcCol.name}`, // visually linking back
|
|
197
|
+
animated: true,
|
|
198
|
+
style: { stroke: '#cbd5e1', strokeWidth: 1, strokeDasharray: '5,5' }
|
|
199
|
+
});
|
|
200
|
+
}
|
|
201
|
+
});
|
|
202
|
+
}
|
|
203
|
+
});
|
|
204
|
+
|
|
205
|
+
setEdges(initialEdges);
|
|
206
|
+
|
|
207
|
+
setStats({
|
|
208
|
+
mapped: mappedCount,
|
|
209
|
+
missingSource: data.sourceSchema.columns.length - mappedCount,
|
|
210
|
+
missingTarget: data.targetSchema.columns.length - mappedCount
|
|
211
|
+
});
|
|
212
|
+
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error("Schema Map error:", e);
|
|
215
|
+
} finally {
|
|
216
|
+
setIsLoading(false);
|
|
217
|
+
}
|
|
218
|
+
};
|
|
219
|
+
|
|
220
|
+
fetchSchemaMap();
|
|
221
|
+
}, [sourceConnector, sourceResource, targetConnector, targetResource]);
|
|
222
|
+
|
|
223
|
+
return (
|
|
224
|
+
<div className="fixed inset-0 z-50 flex items-center justify-center bg-black/80 backdrop-blur-sm p-4 text-white">
|
|
225
|
+
<div className="bg-[#0b0e14] w-[95vw] h-[90vh] rounded-2xl border border-slate-700 shadow-2xl flex flex-col overflow-hidden relative">
|
|
226
|
+
|
|
227
|
+
{/* Header Toolbar */}
|
|
228
|
+
<div className="flex items-center justify-between px-6 py-4 border-b border-slate-800 bg-slate-900/50 backdrop-blur-md">
|
|
229
|
+
<div>
|
|
230
|
+
<h2 className="text-xl font-bold bg-gradient-to-r from-blue-400 to-indigo-400 bg-clip-text text-transparent flex items-center gap-2">
|
|
231
|
+
<span className="material-symbols-outlined text-indigo-400">schema</span>
|
|
232
|
+
Dataway ERD Mapper
|
|
233
|
+
</h2>
|
|
234
|
+
<p className="text-slate-400 text-sm mt-1">
|
|
235
|
+
Analyzed Source <b>({sourceResource})</b> against Target <b>({targetResource})</b>
|
|
236
|
+
</p>
|
|
237
|
+
</div>
|
|
238
|
+
|
|
239
|
+
<div className="flex gap-6 items-center">
|
|
240
|
+
<div className="flex gap-4 text-sm font-mono bg-black/40 px-4 py-2 rounded-lg border border-slate-800">
|
|
241
|
+
<div className="flex flex-col items-center px-2">
|
|
242
|
+
<span className="text-emerald-400 text-lg font-bold">{stats.mapped}</span>
|
|
243
|
+
<span className="text-slate-500 text-[10px] uppercase">Mapped</span>
|
|
244
|
+
</div>
|
|
245
|
+
<div className="w-px bg-slate-800"></div>
|
|
246
|
+
<div className="flex flex-col items-center px-2">
|
|
247
|
+
<span className="text-rose-400 text-lg font-bold">{stats.missingTarget}</span>
|
|
248
|
+
<span className="text-slate-500 text-[10px] uppercase">Orphans</span>
|
|
249
|
+
</div>
|
|
250
|
+
</div>
|
|
251
|
+
<button
|
|
252
|
+
onClick={onClose}
|
|
253
|
+
className="w-10 h-10 rounded-xl bg-slate-800 hover:bg-rose-500/20 hover:text-rose-400 flex items-center justify-center transition-all"
|
|
254
|
+
>
|
|
255
|
+
<span className="material-symbols-outlined">close</span>
|
|
256
|
+
</button>
|
|
257
|
+
</div>
|
|
258
|
+
</div>
|
|
259
|
+
|
|
260
|
+
{/* React Flow Canvas */}
|
|
261
|
+
<div className="flex-1 w-full h-full bg-[#0a0a0a]">
|
|
262
|
+
{isLoading ? (
|
|
263
|
+
<div className="w-full h-full flex items-center justify-center">
|
|
264
|
+
<div className="animate-spin rounded-full h-12 w-12 border-b-2 border-indigo-500"></div>
|
|
265
|
+
</div>
|
|
266
|
+
) : (
|
|
267
|
+
<ReactFlow
|
|
268
|
+
nodes={nodes}
|
|
269
|
+
edges={edges}
|
|
270
|
+
onNodesChange={onNodesChange}
|
|
271
|
+
onEdgesChange={onEdgesChange}
|
|
272
|
+
nodeTypes={nodeTypes}
|
|
273
|
+
fitView
|
|
274
|
+
attributionPosition="bottom-left"
|
|
275
|
+
className="bg-[#050505]"
|
|
276
|
+
>
|
|
277
|
+
<Background color="#334155" gap={20} size={1} />
|
|
278
|
+
<Controls className="!bg-slate-900 !border-slate-700 !fill-slate-400" />
|
|
279
|
+
<MiniMap
|
|
280
|
+
className="!bg-slate-900 !border-slate-700 rounded-lg"
|
|
281
|
+
maskColor="rgba(0,0,0,0.5)"
|
|
282
|
+
nodeColor={(n) => n.id.includes('source') ? '#4f46e5' : '#10b981'}
|
|
283
|
+
/>
|
|
284
|
+
</ReactFlow>
|
|
285
|
+
)}
|
|
286
|
+
</div>
|
|
287
|
+
|
|
288
|
+
</div>
|
|
289
|
+
</div>
|
|
290
|
+
);
|
|
291
|
+
};
|
|
@@ -0,0 +1,242 @@
|
|
|
1
|
+
import React, { useState, useEffect, useCallback } from 'react';
|
|
2
|
+
import { Play, FunctionSquare, XCircle, History, Sparkles, Loader2 } from 'lucide-react';
|
|
3
|
+
import CodeMirror from '@uiw/react-codemirror';
|
|
4
|
+
import { keymap } from '@codemirror/view';
|
|
5
|
+
import { autocompletion, CompletionContext } from '@codemirror/autocomplete';
|
|
6
|
+
import { useDatawayStore } from '../store/datawayStore';
|
|
7
|
+
import { toast } from 'sonner';
|
|
8
|
+
|
|
9
|
+
interface FormulaBarProps {
|
|
10
|
+
onExecute: (script: string) => void;
|
|
11
|
+
onClear: () => void;
|
|
12
|
+
activeResource: string | null;
|
|
13
|
+
activeConnector: string;
|
|
14
|
+
isLoading: boolean;
|
|
15
|
+
schemaColumns?: string[];
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
export const FormulaBar: React.FC<FormulaBarProps> = ({ onExecute, onClear, activeResource, activeConnector, isLoading, schemaColumns = [] }) => {
|
|
19
|
+
const [script, setScript] = useState('');
|
|
20
|
+
const [isCopilotActive, setIsCopilotActive] = useState(false);
|
|
21
|
+
const [copilotPrompt, setCopilotPrompt] = useState('');
|
|
22
|
+
const [isGenerating, setIsGenerating] = useState(false);
|
|
23
|
+
|
|
24
|
+
const { commandHistory, addCommand } = useDatawayStore();
|
|
25
|
+
const [historyIndex, setHistoryIndex] = useState(-1);
|
|
26
|
+
|
|
27
|
+
const handleExecute = () => {
|
|
28
|
+
if (!script.trim()) {
|
|
29
|
+
onClear();
|
|
30
|
+
} else {
|
|
31
|
+
addCommand(script);
|
|
32
|
+
setHistoryIndex(-1);
|
|
33
|
+
onExecute(script);
|
|
34
|
+
}
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const handleClear = () => {
|
|
38
|
+
setScript('');
|
|
39
|
+
setHistoryIndex(-1);
|
|
40
|
+
onClear();
|
|
41
|
+
};
|
|
42
|
+
|
|
43
|
+
const handleKeyDown = useCallback((e: React.KeyboardEvent) => {
|
|
44
|
+
if (e.key === 'ArrowUp' && (e.ctrlKey || e.metaKey)) {
|
|
45
|
+
e.preventDefault();
|
|
46
|
+
if (commandHistory.length > 0) {
|
|
47
|
+
const newIndex = historyIndex === -1 ? commandHistory.length - 1 : Math.max(0, historyIndex - 1);
|
|
48
|
+
setHistoryIndex(newIndex);
|
|
49
|
+
setScript(commandHistory[newIndex]);
|
|
50
|
+
}
|
|
51
|
+
} else if (e.key === 'ArrowDown' && (e.ctrlKey || e.metaKey)) {
|
|
52
|
+
e.preventDefault();
|
|
53
|
+
if (historyIndex !== -1) {
|
|
54
|
+
const newIndex = historyIndex + 1;
|
|
55
|
+
if (newIndex >= commandHistory.length) {
|
|
56
|
+
setHistoryIndex(-1);
|
|
57
|
+
setScript('');
|
|
58
|
+
} else {
|
|
59
|
+
setHistoryIndex(newIndex);
|
|
60
|
+
setScript(commandHistory[newIndex]);
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
}
|
|
64
|
+
}, [commandHistory, historyIndex]);
|
|
65
|
+
|
|
66
|
+
const handleGenerateCopilot = async () => {
|
|
67
|
+
if (!copilotPrompt.trim() || !activeResource) return;
|
|
68
|
+
setIsGenerating(true);
|
|
69
|
+
try {
|
|
70
|
+
const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
|
|
71
|
+
const res = await fetch(`${baseUrl}/api/dataway/copilot`, {
|
|
72
|
+
method: 'POST',
|
|
73
|
+
headers: { 'Content-Type': 'application/json' },
|
|
74
|
+
body: JSON.stringify({
|
|
75
|
+
prompt: copilotPrompt,
|
|
76
|
+
connectorId: activeConnector,
|
|
77
|
+
resource: activeResource
|
|
78
|
+
})
|
|
79
|
+
});
|
|
80
|
+
if (!res.ok) throw new Error('Falló la generación IA');
|
|
81
|
+
const data = await res.json();
|
|
82
|
+
|
|
83
|
+
setScript(data.script);
|
|
84
|
+
setIsCopilotActive(false);
|
|
85
|
+
setCopilotPrompt('');
|
|
86
|
+
toast.success('M-Script Generado por Inteligencia Artificial');
|
|
87
|
+
} catch (error: any) {
|
|
88
|
+
toast.error('Error del Copilot', { description: error.message });
|
|
89
|
+
} finally {
|
|
90
|
+
setIsGenerating(false);
|
|
91
|
+
}
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
const customExtensions = [
|
|
95
|
+
keymap.of([
|
|
96
|
+
{
|
|
97
|
+
key: 'Enter',
|
|
98
|
+
run: () => {
|
|
99
|
+
handleExecute();
|
|
100
|
+
return true;
|
|
101
|
+
},
|
|
102
|
+
shift: () => false // support shift-enter mapped natively to new lines
|
|
103
|
+
}
|
|
104
|
+
]),
|
|
105
|
+
autocompletion({
|
|
106
|
+
override: [
|
|
107
|
+
(context: CompletionContext) => {
|
|
108
|
+
const word = context.matchBefore(/["\w.]+/);
|
|
109
|
+
if (!word || (word.from === word.to && !context.explicit)) return null;
|
|
110
|
+
|
|
111
|
+
const mScriptFunctions = [
|
|
112
|
+
{ label: 'Table.SelectRows', type: 'function', info: 'Table.SelectRows(table, column, value)' },
|
|
113
|
+
{ label: 'Table.AddColumn', type: 'function', info: 'Table.AddColumn(table, newColumn, value)' },
|
|
114
|
+
{ label: 'Table.RenameColumns', type: 'function', info: 'Table.RenameColumns(table, {{"old", "new"}})' },
|
|
115
|
+
{ label: 'Table.SelectColumns', type: 'function', info: 'Table.SelectColumns(table, {"col1", "col2"})' },
|
|
116
|
+
{ label: 'Table.RemoveColumns', type: 'function', info: 'Table.RemoveColumns(table, {"col1", "col2"})' },
|
|
117
|
+
{ label: 'Table.FirstN', type: 'function', info: 'Table.FirstN(table, count)' },
|
|
118
|
+
{ label: 'Table.LastN', type: 'function', info: 'Table.LastN(table, count)' },
|
|
119
|
+
{ label: 'Table.Distinct', type: 'function', info: 'Table.Distinct(table)' },
|
|
120
|
+
{ label: 'Table.Sort', type: 'function', info: 'Table.Sort(table, {{"col", Order.Descending}})' },
|
|
121
|
+
{ label: 'Order.Ascending', type: 'variable' },
|
|
122
|
+
{ label: 'Order.Descending', type: 'variable' },
|
|
123
|
+
{ label: 'Source', type: 'variable', info: 'The active resource dataset' },
|
|
124
|
+
];
|
|
125
|
+
|
|
126
|
+
const cols = schemaColumns.map(col => ({
|
|
127
|
+
label: `"${col}"`,
|
|
128
|
+
type: 'property',
|
|
129
|
+
info: `Column in ${activeResource}`
|
|
130
|
+
}));
|
|
131
|
+
|
|
132
|
+
return {
|
|
133
|
+
from: word.from,
|
|
134
|
+
options: [...mScriptFunctions, ...cols]
|
|
135
|
+
};
|
|
136
|
+
}
|
|
137
|
+
]
|
|
138
|
+
})
|
|
139
|
+
];
|
|
140
|
+
|
|
141
|
+
return (
|
|
142
|
+
<div className="w-full bg-[var(--bg-primary)] border-b border-[var(--border-color)] px-6 py-3 flex items-center gap-3 shadow-sm z-30" onKeyDown={handleKeyDown}>
|
|
143
|
+
<div className="bg-[var(--brand-primary)]/10 p-1.5 rounded-lg text-[var(--brand-primary)]">
|
|
144
|
+
<FunctionSquare className="w-5 h-5" />
|
|
145
|
+
</div>
|
|
146
|
+
|
|
147
|
+
{isCopilotActive ? (
|
|
148
|
+
<div className="flex-1 relative group border border-purple-400 ring-2 ring-purple-400/20 rounded-xl overflow-hidden shadow-inner bg-purple-50/50 flex items-center transition-all">
|
|
149
|
+
<input
|
|
150
|
+
type="text"
|
|
151
|
+
autoFocus
|
|
152
|
+
value={copilotPrompt}
|
|
153
|
+
onChange={(e) => setCopilotPrompt(e.target.value)}
|
|
154
|
+
placeholder="Dile a Gemini qué necesitas... (Ej: filtra los urgentes y ordena por fecha)"
|
|
155
|
+
className="w-full bg-transparent border-none outline-none py-2.5 px-4 text-sm font-medium text-purple-900 placeholder:text-purple-400/70"
|
|
156
|
+
onKeyDown={(e) => {
|
|
157
|
+
if (e.key === 'Enter') handleGenerateCopilot();
|
|
158
|
+
if (e.key === 'Escape') setIsCopilotActive(false);
|
|
159
|
+
}}
|
|
160
|
+
disabled={isGenerating}
|
|
161
|
+
/>
|
|
162
|
+
<button onClick={() => setIsCopilotActive(false)} className="pr-3 text-purple-400 hover:text-purple-600 transition-colors">
|
|
163
|
+
<XCircle className="w-4 h-4" />
|
|
164
|
+
</button>
|
|
165
|
+
</div>
|
|
166
|
+
) : (
|
|
167
|
+
<div className="flex-1 relative group border border-[var(--border-color)] rounded-xl overflow-hidden shadow-inner focus-within:ring-2 focus-within:ring-[var(--brand-primary)]/30 focus-within:border-[var(--brand-primary)] transition-all bg-[var(--bg-surface)] flex items-center">
|
|
168
|
+
<div className="flex-1">
|
|
169
|
+
<CodeMirror
|
|
170
|
+
value={script}
|
|
171
|
+
height="auto"
|
|
172
|
+
placeholder={activeResource
|
|
173
|
+
? `Ej: Table.SelectRows(Source, "${activeResource}_id", "==", "10") (Ctrl+↑ Histórico)`
|
|
174
|
+
: "Selecciona un recurso para escribir una fórmula M-Script..."}
|
|
175
|
+
extensions={customExtensions}
|
|
176
|
+
onChange={(val) => setScript(val)}
|
|
177
|
+
editable={!!activeResource && !isLoading}
|
|
178
|
+
basicSetup={{
|
|
179
|
+
lineNumbers: false,
|
|
180
|
+
foldGutter: false,
|
|
181
|
+
dropCursor: false,
|
|
182
|
+
allowMultipleSelections: false,
|
|
183
|
+
indentOnInput: false,
|
|
184
|
+
bracketMatching: true,
|
|
185
|
+
closeBrackets: true,
|
|
186
|
+
autocompletion: true,
|
|
187
|
+
highlightActiveLine: false,
|
|
188
|
+
highlightActiveLineGutter: false
|
|
189
|
+
}}
|
|
190
|
+
className="text-sm font-mono text-[var(--text-primary)] w-full [&_.cm-editor]:bg-transparent [&_.cm-scroller]:p-2.5"
|
|
191
|
+
theme="none"
|
|
192
|
+
/>
|
|
193
|
+
</div>
|
|
194
|
+
|
|
195
|
+
{script && (
|
|
196
|
+
<button
|
|
197
|
+
onClick={handleClear}
|
|
198
|
+
className="absolute right-3 top-1/2 -translate-y-1/2 text-[var(--text-secondary)] hover:text-red-500 transition-colors bg-[var(--bg-surface)] pl-2 h-full flex items-center"
|
|
199
|
+
title="Limpiar Fórmula"
|
|
200
|
+
style={{ zIndex: 10 }}
|
|
201
|
+
>
|
|
202
|
+
<XCircle className="w-4 h-4" />
|
|
203
|
+
</button>
|
|
204
|
+
)}
|
|
205
|
+
</div>
|
|
206
|
+
)}
|
|
207
|
+
|
|
208
|
+
<button
|
|
209
|
+
onClick={() => isCopilotActive ? handleGenerateCopilot() : setIsCopilotActive(true)}
|
|
210
|
+
disabled={!activeResource || isLoading || isGenerating}
|
|
211
|
+
className={`flex items-center justify-center w-10 h-10 rounded-xl transition-all ${
|
|
212
|
+
isCopilotActive
|
|
213
|
+
? 'bg-gradient-to-r from-purple-500 to-indigo-600 text-white shadow-md shadow-purple-500/20'
|
|
214
|
+
: 'bg-[var(--bg-surface)] text-[var(--text-secondary)] hover:text-purple-600 hover:bg-purple-50'
|
|
215
|
+
}`}
|
|
216
|
+
title="AI Copilot (M-Script Maker)"
|
|
217
|
+
>
|
|
218
|
+
{isGenerating ? <Loader2 className="w-5 h-5 animate-spin" /> : <Sparkles className="w-5 h-5" />}
|
|
219
|
+
</button>
|
|
220
|
+
|
|
221
|
+
{commandHistory.length > 0 && (
|
|
222
|
+
<div className="text-[10px] text-[var(--text-secondary)] font-medium flex items-center gap-1 cursor-help relative" title="Consultas en el historial (Ctrl+↑ / Ctrl+↓)">
|
|
223
|
+
<History className="w-3 h-3 opacity-60" />
|
|
224
|
+
<span className="opacity-60">{commandHistory.length}</span>
|
|
225
|
+
</div>
|
|
226
|
+
)}
|
|
227
|
+
|
|
228
|
+
<button
|
|
229
|
+
onClick={handleExecute}
|
|
230
|
+
disabled={!activeResource || isLoading || !script.trim()}
|
|
231
|
+
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 text-sm font-bold shadow-md shadow-[var(--brand-primary)]/20 transition-all active:scale-95 disabled:opacity-50 disabled:grayscale ml-2"
|
|
232
|
+
>
|
|
233
|
+
{isLoading ? (
|
|
234
|
+
<div className="w-4 h-4 border-2 border-white/30 border-t-white rounded-full animate-spin" />
|
|
235
|
+
) : (
|
|
236
|
+
<Play className="w-4 h-4 fill-current" />
|
|
237
|
+
)}
|
|
238
|
+
<span>Ejecutar</span>
|
|
239
|
+
</button>
|
|
240
|
+
</div>
|
|
241
|
+
);
|
|
242
|
+
};
|
|
@@ -0,0 +1,104 @@
|
|
|
1
|
+
import { useEffect } from 'react';
|
|
2
|
+
import { io } from 'socket.io-client';
|
|
3
|
+
import { useOrderStore } from '../stores/orderStore';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
import { playUISound } from '../utils/sounds';
|
|
6
|
+
|
|
7
|
+
const SOCKET_URL = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001'; // Madefront backend
|
|
8
|
+
|
|
9
|
+
let socketInstance: any = null;
|
|
10
|
+
|
|
11
|
+
export const useSocketSync = () => {
|
|
12
|
+
const { updateStatus, fetchOrders } = useOrderStore();
|
|
13
|
+
|
|
14
|
+
useEffect(() => {
|
|
15
|
+
if (!socketInstance) {
|
|
16
|
+
socketInstance = io(SOCKET_URL, {
|
|
17
|
+
reconnectionAttempts: 5,
|
|
18
|
+
reconnectionDelay: 2000,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const socket = socketInstance;
|
|
23
|
+
|
|
24
|
+
const onConnect = () => {
|
|
25
|
+
console.log('[Chameleon Socket] Conectado al backend:', socket.id);
|
|
26
|
+
fetchOrders();
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const onOrderUpdate = (data: any) => {
|
|
30
|
+
console.log('[Chameleon Socket] order_update recibido:', data);
|
|
31
|
+
|
|
32
|
+
if (data?.id && data?.stage) {
|
|
33
|
+
const mapStageToStatus = (stage: string): string => {
|
|
34
|
+
switch (stage?.toUpperCase()) {
|
|
35
|
+
case 'NEW':
|
|
36
|
+
case 'DESIGN':
|
|
37
|
+
return 'DISEÑO';
|
|
38
|
+
case 'OPTIMIZATION':
|
|
39
|
+
case 'IN_CUTTING':
|
|
40
|
+
case 'IN_EDGEBANDING':
|
|
41
|
+
case 'IN_ASSEMBLY':
|
|
42
|
+
return 'OPTIMIZACION';
|
|
43
|
+
case 'AUTHORIZED_FOR_PRODUCTION':
|
|
44
|
+
return 'CAJA';
|
|
45
|
+
case 'PRODUCTION':
|
|
46
|
+
return 'PRODUCCION';
|
|
47
|
+
case 'DISPATCH':
|
|
48
|
+
case 'IN_DISPATCH':
|
|
49
|
+
case 'READY_FOR_DISPATCH':
|
|
50
|
+
return 'DESPACHO';
|
|
51
|
+
case 'COMPLETED':
|
|
52
|
+
case 'DELIVERED':
|
|
53
|
+
return 'ENTREGADO';
|
|
54
|
+
default:
|
|
55
|
+
return 'DISEÑO';
|
|
56
|
+
}
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
const mappedStatus = mapStageToStatus(data.stage);
|
|
60
|
+
updateStatus(data.id, mappedStatus as any);
|
|
61
|
+
|
|
62
|
+
const targetStatus = data.stage;
|
|
63
|
+
if (targetStatus === 'IN_CUTTING' || targetStatus === 'PRODUCTION') {
|
|
64
|
+
toast('WhatsApp Automático', {
|
|
65
|
+
description: `📱 Cliente: "Hola! Tu pedido ha entrado a la zona de corte."`,
|
|
66
|
+
icon: '🟢'
|
|
67
|
+
});
|
|
68
|
+
} else if (targetStatus === 'DISPATCH' || targetStatus === 'READY_FOR_DISPATCH') {
|
|
69
|
+
toast('WhatsApp Automático', {
|
|
70
|
+
description: `📱 Cliente: "Buenas noticias! Tu material está listo."`,
|
|
71
|
+
icon: '🟢'
|
|
72
|
+
});
|
|
73
|
+
} else if (targetStatus === 'AUTHORIZED_FOR_PRODUCTION') {
|
|
74
|
+
toast('WhatsApp Interno', {
|
|
75
|
+
description: `💰 Alerta: Nuevo pedido esperando autorización.`,
|
|
76
|
+
icon: '🟡'
|
|
77
|
+
});
|
|
78
|
+
} else {
|
|
79
|
+
toast.success(`Orden ${data.id} sincronizada`, {
|
|
80
|
+
description: `Nuevo estado Kanban: ${mappedStatus}`,
|
|
81
|
+
icon: '🔄'
|
|
82
|
+
});
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
playUISound('swoosh');
|
|
86
|
+
}
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const onDisconnect = () => {
|
|
90
|
+
console.warn('[Chameleon Socket] Desconectado del backend');
|
|
91
|
+
};
|
|
92
|
+
|
|
93
|
+
socket.on('connect', onConnect);
|
|
94
|
+
socket.on('server:order_update', onOrderUpdate);
|
|
95
|
+
socket.on('disconnect', onDisconnect);
|
|
96
|
+
|
|
97
|
+
return () => {
|
|
98
|
+
socket.off('connect', onConnect);
|
|
99
|
+
socket.off('server:order_update', onOrderUpdate);
|
|
100
|
+
socket.off('disconnect', onDisconnect);
|
|
101
|
+
};
|
|
102
|
+
}, [updateStatus, fetchOrders]);
|
|
103
|
+
};
|
|
104
|
+
|
package/src/index.css
ADDED
|
@@ -0,0 +1,18 @@
|
|
|
1
|
+
@import "tailwindcss";
|
|
2
|
+
|
|
3
|
+
@theme {
|
|
4
|
+
--font-sans: "Inter", ui-sans-serif, system-ui, sans-serif;
|
|
5
|
+
--font-mono: "JetBrains Mono", ui-monospace, SFMono-Regular, monospace;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
@layer utilities {
|
|
9
|
+
.perspective-1000 {
|
|
10
|
+
perspective: 1000px;
|
|
11
|
+
}
|
|
12
|
+
.rotate-x-2 {
|
|
13
|
+
transform: rotateX(2deg);
|
|
14
|
+
}
|
|
15
|
+
.rotate-y-2 {
|
|
16
|
+
transform: rotateY(2deg);
|
|
17
|
+
}
|
|
18
|
+
}
|