@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
package/src/index.ts
ADDED
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
import { definePlugin } from '@decido/sdk';
|
|
2
|
+
import type { DecidoApp } from '@decido/sdk';
|
|
3
|
+
import { VentasView } from './views/roles/VentasView';
|
|
4
|
+
import { CajaView } from './views/roles/CajaView';
|
|
5
|
+
import { PlantaView } from './views/roles/PlantaView';
|
|
6
|
+
import { DespachoView } from './views/roles/DespachoView';
|
|
7
|
+
import { DiscoveryAdminView } from './views/DiscoveryAdminView';
|
|
8
|
+
import { MadefrontKanbanView } from './views/MadefrontKanbanView';
|
|
9
|
+
import { MadefrontExcelView } from './views/MadefrontExcelView';
|
|
10
|
+
import { DatabaseAdminView } from './views/DatabaseAdminView';
|
|
11
|
+
import { DatawayStudioView } from './views/DatawayStudioView';
|
|
12
|
+
|
|
13
|
+
let eventUnsubscribers: (() => void)[] = [];
|
|
14
|
+
|
|
15
|
+
export const ChameleonPlugin = definePlugin({
|
|
16
|
+
manifest: {
|
|
17
|
+
id: 'macia-chameleon-agent',
|
|
18
|
+
version: '2.1.0',
|
|
19
|
+
name: 'Madefront',
|
|
20
|
+
description: 'Proactive Intelligence & Liquid UI Engine',
|
|
21
|
+
author: 'Decido AI',
|
|
22
|
+
permissions: ['cortex:view', 'os:capture_screen'],
|
|
23
|
+
intents: [
|
|
24
|
+
'chameleon-change-view'
|
|
25
|
+
],
|
|
26
|
+
widgets: [
|
|
27
|
+
{
|
|
28
|
+
id: 'chameleon-kanban-view',
|
|
29
|
+
name: 'Madefront Kanban',
|
|
30
|
+
defaultZone: 'ide-editor',
|
|
31
|
+
component: MadefrontKanbanView,
|
|
32
|
+
},
|
|
33
|
+
{
|
|
34
|
+
id: 'chameleon-ventas-view',
|
|
35
|
+
name: 'Ventas & Diseño',
|
|
36
|
+
defaultZone: 'ide-editor',
|
|
37
|
+
component: VentasView,
|
|
38
|
+
},
|
|
39
|
+
{
|
|
40
|
+
id: 'chameleon-caja-view',
|
|
41
|
+
name: 'Caja & Pagos',
|
|
42
|
+
defaultZone: 'ide-editor',
|
|
43
|
+
component: CajaView,
|
|
44
|
+
},
|
|
45
|
+
{
|
|
46
|
+
id: 'chameleon-planta-view',
|
|
47
|
+
name: 'Producción',
|
|
48
|
+
defaultZone: 'ide-editor',
|
|
49
|
+
component: PlantaView,
|
|
50
|
+
},
|
|
51
|
+
{
|
|
52
|
+
id: 'chameleon-despacho-view',
|
|
53
|
+
name: 'Logística',
|
|
54
|
+
defaultZone: 'ide-editor',
|
|
55
|
+
component: DespachoView,
|
|
56
|
+
},
|
|
57
|
+
{
|
|
58
|
+
id: 'chameleon-discovery-view',
|
|
59
|
+
name: 'Studio Admin',
|
|
60
|
+
defaultZone: 'ide-editor',
|
|
61
|
+
component: DiscoveryAdminView,
|
|
62
|
+
},
|
|
63
|
+
{
|
|
64
|
+
id: 'chameleon-sheet-view',
|
|
65
|
+
name: 'Vista Excel (Datos)',
|
|
66
|
+
defaultZone: 'ide-editor',
|
|
67
|
+
component: MadefrontExcelView,
|
|
68
|
+
},
|
|
69
|
+
{
|
|
70
|
+
id: 'chameleon-database-admin',
|
|
71
|
+
name: 'Database Admin (CRUD)',
|
|
72
|
+
defaultZone: 'ide-editor',
|
|
73
|
+
component: DatabaseAdminView,
|
|
74
|
+
},
|
|
75
|
+
{
|
|
76
|
+
id: 'chameleon-dataway-studio',
|
|
77
|
+
name: 'Dataway M-Script Studio',
|
|
78
|
+
defaultZone: 'ide-editor',
|
|
79
|
+
component: DatawayStudioView,
|
|
80
|
+
}
|
|
81
|
+
],
|
|
82
|
+
blueprints: [],
|
|
83
|
+
},
|
|
84
|
+
|
|
85
|
+
onMount: (app: DecidoApp) => {
|
|
86
|
+
app.logger.log('Chameleon AI mounting...');
|
|
87
|
+
|
|
88
|
+
// Listen for view-change intents via SDK event bridge
|
|
89
|
+
const unsub = app.subscribeToEvent('intent:chameleon-change-view', (payload: any) => {
|
|
90
|
+
const matches = payload?.matches;
|
|
91
|
+
if (!matches || !matches[0]) return;
|
|
92
|
+
|
|
93
|
+
const text = matches[0].toLowerCase();
|
|
94
|
+
|
|
95
|
+
// Activate plugin via kernel RPC instead of direct shell store access
|
|
96
|
+
const activate = (pluginId: string) => {
|
|
97
|
+
app.callKernel('activate_plugin', { pluginId }).catch((err) => {
|
|
98
|
+
app.logger.warn(`Failed to activate ${pluginId}:`, err);
|
|
99
|
+
});
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
if (text.includes('venta')) activate('cortex-omnichannel');
|
|
103
|
+
if (text.includes('caja')) activate('cortex-commander');
|
|
104
|
+
if (text.includes('planta')) activate('cortex-forge');
|
|
105
|
+
if (text.includes('admin')) activate('cortex-admin');
|
|
106
|
+
});
|
|
107
|
+
|
|
108
|
+
eventUnsubscribers = [unsub];
|
|
109
|
+
},
|
|
110
|
+
|
|
111
|
+
onUnmount: () => {
|
|
112
|
+
eventUnsubscribers.forEach(unsub => unsub());
|
|
113
|
+
eventUnsubscribers = [];
|
|
114
|
+
console.log('[Chameleon] Cleaned up — all subscriptions removed.');
|
|
115
|
+
},
|
|
116
|
+
|
|
117
|
+
onError: (error, phase) => {
|
|
118
|
+
console.error(`[Chameleon] Error during ${phase}:`, error);
|
|
119
|
+
},
|
|
120
|
+
});
|
|
121
|
+
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { MachineType } from '../types';
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Agente: SLA Sentinel
|
|
5
|
+
* Regla: < 5 láminas = 24h, >= 5 láminas = 72h
|
|
6
|
+
*/
|
|
7
|
+
export const calculateSLA = (sheets: number): number => {
|
|
8
|
+
return sheets < 5 ? 24 : 72;
|
|
9
|
+
};
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Agente: Router AI
|
|
13
|
+
* Regla: Asignación de máquinas según material y complejidad
|
|
14
|
+
*/
|
|
15
|
+
export const assignMachine = (material: string, sheets: number): MachineType => {
|
|
16
|
+
const mat = material.toLowerCase();
|
|
17
|
+
|
|
18
|
+
if (mat.includes('perforacion') || mat.includes('diseño') || mat.includes('cnc')) {
|
|
19
|
+
return 'HOMAG'; // CNC para cosas complejas
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
if (sheets > 10 || mat.includes('grueso') || mat.includes('18mm')) {
|
|
23
|
+
return 'GABBIANI'; // Trabajo pesado
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
return 'SIGMA'; // Corte rápido estándar
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
/**
|
|
30
|
+
* Agente: Phone Doctor
|
|
31
|
+
* Regla: Asegurar formato +57 para WhatsApp
|
|
32
|
+
*/
|
|
33
|
+
export const sanitizePhone = (phone: string): string => {
|
|
34
|
+
const digits = phone.replace(/\D/g, '');
|
|
35
|
+
if (!digits.startsWith('57')) {
|
|
36
|
+
return `+57 ${digits}`;
|
|
37
|
+
}
|
|
38
|
+
return `+${digits}`;
|
|
39
|
+
};
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import { MadefrontOrder, OrderStatus } from '../types';
|
|
2
|
+
|
|
3
|
+
interface TransitionResult {
|
|
4
|
+
allowed: boolean;
|
|
5
|
+
reason?: string;
|
|
6
|
+
}
|
|
7
|
+
|
|
8
|
+
// Mapa de transiciones permitidas (Máquina de Estados)
|
|
9
|
+
const ALLOWED_TRANSITIONS: Record<OrderStatus, OrderStatus[]> = {
|
|
10
|
+
'DISEÑO': ['OPTIMIZACION', 'CAJA'], // Puede saltar a caja si no requiere despiece
|
|
11
|
+
'OPTIMIZACION': ['CAJA', 'PRODUCCION'],
|
|
12
|
+
'CAJA': ['PRODUCCION'],
|
|
13
|
+
'PRODUCCION': ['DESPACHO'],
|
|
14
|
+
'DESPACHO': ['ENTREGADO'],
|
|
15
|
+
'ENTREGADO': []
|
|
16
|
+
};
|
|
17
|
+
|
|
18
|
+
export const canTransition = (order: MadefrontOrder, targetStatus: OrderStatus): TransitionResult => {
|
|
19
|
+
const currentStatus = order.status;
|
|
20
|
+
|
|
21
|
+
// 1. Validar que la ruta exista en el grafo
|
|
22
|
+
const validNextStates = ALLOWED_TRANSITIONS[currentStatus] || [];
|
|
23
|
+
if (!validNextStates.includes(targetStatus)) {
|
|
24
|
+
return {
|
|
25
|
+
allowed: false,
|
|
26
|
+
reason: `Flujo inválido: No se puede pasar de ${currentStatus} a ${targetStatus}.`
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
// 2. Reglas Hardcore de Negocio (Las que nos dio Ana)
|
|
31
|
+
if (targetStatus === 'PRODUCCION') {
|
|
32
|
+
if (!order.isPaid) {
|
|
33
|
+
return { allowed: false, reason: 'Bloqueo de Caja: El pedido debe estar pagado para entrar a Planta.' };
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
if (targetStatus === 'DESPACHO') {
|
|
38
|
+
// En el futuro: validar que la máquina (Sigma/Homag) haya emitido el evento de fin
|
|
39
|
+
if (order.status !== 'PRODUCCION') {
|
|
40
|
+
return { allowed: false, reason: 'El pedido debe salir de producción antes de despacharse.' };
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
return { allowed: true };
|
|
45
|
+
};
|
package/src/main.tsx
ADDED
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
export interface GeminiMessage {
|
|
2
|
+
role: 'user' | 'model' | 'system';
|
|
3
|
+
content: string;
|
|
4
|
+
}
|
|
5
|
+
|
|
6
|
+
export interface GeminiOptions {
|
|
7
|
+
apiKey?: string;
|
|
8
|
+
temperature?: number;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
const SYSTEM_PROMPT = `
|
|
12
|
+
Eres el "Chameleon UI Engine", un arquitecto experto en React, TypeScript y TailwindCSS.
|
|
13
|
+
Tu misión es generar o modificar componentes de UI para una plataforma inteligente.
|
|
14
|
+
|
|
15
|
+
REGLAS ESTRICTAS:
|
|
16
|
+
1. Debes generar código válido en React (Functional Components) usando TypeScript.
|
|
17
|
+
2. Usa Tailwind CSS para los estilos. El diseño debe ser industrial, limpio, con paletas de colores legibles (ej. gris pizarra, azul corporativo, o colores de alto contraste si es para la Planta de producción).
|
|
18
|
+
3. Lucide-React está disponible para iconos.
|
|
19
|
+
4. El sistema usa Zustand para manejar el estado (\`useOrderStore\`, \`useAuthStore\`).
|
|
20
|
+
5. Devuelve SOLO EL CÓDIGO del componente. No agregues explicaciones, markdown de código (\`\`\`tsx) a menos que se solicite, ni texto adicional. Solo el raw text del componente.
|
|
21
|
+
6. Si vas a crear una nueva vista (Cortex), asegúrate de que siga el patrón de las vistas existentes: ocupa el \`h-full w-full\`, usa \`bg-slate-50\` o \`bg-white\`, padding apropiado, y estructura clara de encabezado y contenido.
|
|
22
|
+
`;
|
|
23
|
+
|
|
24
|
+
export class GeminiService {
|
|
25
|
+
private apiKey: string;
|
|
26
|
+
private endpoint = 'https://generativelanguage.googleapis.com/v1beta/models/gemini-flash-lite-latest:generateContent';
|
|
27
|
+
|
|
28
|
+
constructor(options: GeminiOptions = {}) {
|
|
29
|
+
// Para entornos reales, usar import.meta.env.VITE_GEMINI_API_KEY
|
|
30
|
+
this.apiKey = options.apiKey || (import.meta as any).env?.VITE_GEMINI_API_KEY || '';
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async generateUIComponent(prompt: string, history: GeminiMessage[] = []): Promise<string> {
|
|
34
|
+
if (!this.apiKey) {
|
|
35
|
+
console.warn("No Gemini API Key provided. Returning mock component.");
|
|
36
|
+
return this.getMockResponse(prompt);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
try {
|
|
40
|
+
const contents = history.map(msg => ({
|
|
41
|
+
role: msg.role === 'system' ? 'user' : msg.role,
|
|
42
|
+
parts: [{ text: msg.content }]
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
// Inyectar System Prompt si no está (API v1beta usa instructions o primer mensaje format)
|
|
46
|
+
// Simplificado: lo agregamos al inicio de los contenidos
|
|
47
|
+
const fullContents = [
|
|
48
|
+
{ role: 'user', parts: [{ text: SYSTEM_PROMPT }] },
|
|
49
|
+
{ role: 'model', parts: [{ text: 'Entendido. Actuaré como el Chameleon UI Engine.' }] },
|
|
50
|
+
...contents,
|
|
51
|
+
{ role: 'user', parts: [{ text: prompt }] }
|
|
52
|
+
];
|
|
53
|
+
|
|
54
|
+
const response = await fetch(`${this.endpoint}?key=${this.apiKey}`, {
|
|
55
|
+
method: 'POST',
|
|
56
|
+
headers: {
|
|
57
|
+
'Content-Type': 'application/json',
|
|
58
|
+
},
|
|
59
|
+
body: JSON.stringify({
|
|
60
|
+
contents: fullContents,
|
|
61
|
+
generationConfig: {
|
|
62
|
+
temperature: 0.2, // Baja temperatura para código más determinista
|
|
63
|
+
}
|
|
64
|
+
})
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!response.ok) {
|
|
68
|
+
throw new Error(`API Error: ${response.status}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
const data = await response.json();
|
|
72
|
+
let text = data.candidates?.[0]?.content?.parts?.[0]?.text || '';
|
|
73
|
+
|
|
74
|
+
// Limpiar markdown residual si existe
|
|
75
|
+
text = text.replace(/^\`\`\`tsx?/g, '').replace(/\`\`\`$/g, '').trim();
|
|
76
|
+
|
|
77
|
+
return text;
|
|
78
|
+
} catch (error) {
|
|
79
|
+
console.error("Gemini API Error:", error);
|
|
80
|
+
throw error;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
private getMockResponse(prompt: string): string {
|
|
85
|
+
return `
|
|
86
|
+
import { Activity } from 'lucide-react';
|
|
87
|
+
|
|
88
|
+
export default function GeneratedView() {
|
|
89
|
+
return (
|
|
90
|
+
<div className="h-full w-full bg-slate-50 p-8 flex flex-col items-center justify-center">
|
|
91
|
+
<div className="bg-white p-6 rounded-2xl shadow-xl flex flex-col items-center max-w-md text-center border border-indigo-100">
|
|
92
|
+
<div className="w-16 h-16 bg-indigo-100 text-indigo-600 rounded-full flex items-center justify-center mb-4">
|
|
93
|
+
<Activity size={32} />
|
|
94
|
+
</div>
|
|
95
|
+
<h2 className="text-2xl font-bold text-slate-800 mb-2">Vista Generada Dinámicamente</h2>
|
|
96
|
+
<p className="text-slate-500 mb-6">
|
|
97
|
+
Esta vista mock fue generada en respuesta al prompt: "\${prompt.substring(0, 50)}..."
|
|
98
|
+
</p>
|
|
99
|
+
<button className="px-6 py-2 bg-indigo-600 text-white rounded-lg font-medium hover:bg-indigo-700 transition">
|
|
100
|
+
Interactuar
|
|
101
|
+
</button>
|
|
102
|
+
</div>
|
|
103
|
+
</div>
|
|
104
|
+
);
|
|
105
|
+
}
|
|
106
|
+
`.trim();
|
|
107
|
+
}
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
export const geminiService = new GeminiService();
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
|
|
4
|
+
interface DatawayState {
|
|
5
|
+
commandHistory: string[];
|
|
6
|
+
addCommand: (cmd: string) => void;
|
|
7
|
+
clearHistory: () => void;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export const useDatawayStore = create<DatawayState>()(
|
|
11
|
+
persist(
|
|
12
|
+
(set) => ({
|
|
13
|
+
commandHistory: [],
|
|
14
|
+
addCommand: (cmd) => set((state) => {
|
|
15
|
+
const trimmed = cmd.trim();
|
|
16
|
+
if (!trimmed || state.commandHistory[state.commandHistory.length - 1] === trimmed) {
|
|
17
|
+
return state;
|
|
18
|
+
}
|
|
19
|
+
const newHistory = [...state.commandHistory, trimmed].slice(-50); // Keep last 50 queries
|
|
20
|
+
return { commandHistory: newHistory };
|
|
21
|
+
}),
|
|
22
|
+
clearHistory: () => set({ commandHistory: [] })
|
|
23
|
+
}),
|
|
24
|
+
{ name: 'decido-dataway-history-v1' }
|
|
25
|
+
)
|
|
26
|
+
);
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { Role } from '../types';
|
|
3
|
+
|
|
4
|
+
interface InternalLicenseTarget {
|
|
5
|
+
type: 'personal' | 'enterprise';
|
|
6
|
+
id: string;
|
|
7
|
+
name: string;
|
|
8
|
+
systemPrompt?: string;
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
interface AuthState {
|
|
12
|
+
currentRole: Role;
|
|
13
|
+
currentUser: string;
|
|
14
|
+
activeContext?: InternalLicenseTarget;
|
|
15
|
+
setRole: (role: Role, user: string, context?: InternalLicenseTarget) => void;
|
|
16
|
+
logout: () => void;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
export const useAuthStore = create<AuthState>((set) => ({
|
|
20
|
+
currentRole: 'NONE',
|
|
21
|
+
currentUser: '',
|
|
22
|
+
activeContext: undefined,
|
|
23
|
+
|
|
24
|
+
setRole: (role, user, context) => set({ currentRole: role, currentUser: user, activeContext: context }),
|
|
25
|
+
logout: () => set({ currentRole: 'NONE', currentUser: '', activeContext: undefined })
|
|
26
|
+
}));
|
|
@@ -0,0 +1,263 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { persist } from 'zustand/middleware';
|
|
3
|
+
import { MadefrontOrder, OrderStatus } from '../types';
|
|
4
|
+
import { calculateSLA, assignMachine, sanitizePhone } from '../logic/rules';
|
|
5
|
+
import { canTransition } from '../logic/workflowGuard';
|
|
6
|
+
import { toast } from 'sonner';
|
|
7
|
+
import { kernel } from '@decido/kernel-bridge';
|
|
8
|
+
import * as Y from 'yjs';
|
|
9
|
+
import { WebsocketProvider } from 'y-websocket';
|
|
10
|
+
|
|
11
|
+
interface OrderState {
|
|
12
|
+
orders: MadefrontOrder[];
|
|
13
|
+
|
|
14
|
+
// Acciones (Lo que hacen los usuarios)
|
|
15
|
+
createOrder: (data: any) => void;
|
|
16
|
+
updateStatus: (id: string, status: OrderStatus) => void;
|
|
17
|
+
updateOrder: (order: MadefrontOrder) => void;
|
|
18
|
+
approvePayment: (id: string) => void; // Acción de Caja
|
|
19
|
+
fetchOrders: () => Promise<void>;
|
|
20
|
+
setOrders: (orders: MadefrontOrder[]) => void;
|
|
21
|
+
|
|
22
|
+
// Selectores (Consultas)
|
|
23
|
+
getOrdersByStage: (status: OrderStatus) => MadefrontOrder[];
|
|
24
|
+
getPendingPayments: () => MadefrontOrder[];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
// -----------------------------------------------------
|
|
28
|
+
// 🛡️ ANILLO 2: La Verdad de los Datos (CRDTs con Yjs)
|
|
29
|
+
// -----------------------------------------------------
|
|
30
|
+
const ydoc = new Y.Doc();
|
|
31
|
+
const yOrders = ydoc.getMap<MadefrontOrder>('orders');
|
|
32
|
+
|
|
33
|
+
const getWsUrl = () => {
|
|
34
|
+
const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
|
|
35
|
+
return baseUrl.replace(/^https?/, match => match === 'https' ? 'wss' : 'ws') + '/api/collaboration';
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const provider = new WebsocketProvider(
|
|
39
|
+
getWsUrl(),
|
|
40
|
+
'madefront-kanban-room',
|
|
41
|
+
ydoc,
|
|
42
|
+
{ connect: true }
|
|
43
|
+
);
|
|
44
|
+
|
|
45
|
+
kernel.onEvent((event: any) => {
|
|
46
|
+
if (event && (event.type === 'MadefrontOrderUpdated' || event.event_type === 'MadefrontOrderUpdated' || event.message?.includes('MadefrontOrderUpdated'))) {
|
|
47
|
+
// Reload offline cache fully from MCP or rely on CRDT push sync
|
|
48
|
+
useOrderStore.getState().fetchOrders();
|
|
49
|
+
}
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// En chameleon lo envolvemos en persist para preservar sesión offline
|
|
53
|
+
export const useOrderStore = create<OrderState>()(
|
|
54
|
+
persist(
|
|
55
|
+
(set, get) => {
|
|
56
|
+
// Observer CRDT -> Zustand
|
|
57
|
+
yOrders.observe(() => {
|
|
58
|
+
set({ orders: Array.from(yOrders.values()) });
|
|
59
|
+
});
|
|
60
|
+
|
|
61
|
+
return {
|
|
62
|
+
orders: Array.from(yOrders.values()),
|
|
63
|
+
|
|
64
|
+
setOrders: (orders) => set({ orders }),
|
|
65
|
+
|
|
66
|
+
fetchOrders: async () => {
|
|
67
|
+
try {
|
|
68
|
+
const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
|
|
69
|
+
|
|
70
|
+
const response = await fetch(`${baseUrl}/api/orders`, {
|
|
71
|
+
headers: { 'x-api-key': 'dev_ak_chameleon_studio_99x' }
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (!response.ok) {
|
|
75
|
+
throw new Error(`Error HTTP: ${response.status}`);
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
let backendOrders: any = await response.json();
|
|
79
|
+
|
|
80
|
+
if (backendOrders.error) {
|
|
81
|
+
throw new Error(backendOrders.error);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
const mapStageToStatus = (stage: string): OrderStatus => {
|
|
85
|
+
switch (stage?.toUpperCase()) {
|
|
86
|
+
case 'NEW':
|
|
87
|
+
case 'DESIGN': return 'DISEÑO';
|
|
88
|
+
case 'OPTIMIZATION':
|
|
89
|
+
case 'IN_CUTTING':
|
|
90
|
+
case 'IN_EDGEBANDING':
|
|
91
|
+
case 'IN_ASSEMBLY': return 'OPTIMIZACION';
|
|
92
|
+
case 'AUTHORIZED_FOR_PRODUCTION': return 'CAJA';
|
|
93
|
+
case 'PRODUCTION': return 'PRODUCCION';
|
|
94
|
+
case 'DISPATCH':
|
|
95
|
+
case 'IN_DISPATCH':
|
|
96
|
+
case 'READY_FOR_DISPATCH': return 'DESPACHO';
|
|
97
|
+
case 'COMPLETED':
|
|
98
|
+
case 'DELIVERED': return 'ENTREGADO';
|
|
99
|
+
default: return 'DISEÑO';
|
|
100
|
+
}
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
ydoc.transact(() => {
|
|
104
|
+
// Find all incoming IDs
|
|
105
|
+
const incomingIds = new Set(backendOrders.map((o: any) => o.legacyId || o.id));
|
|
106
|
+
|
|
107
|
+
// Purge deleted/stale keys from Yjs that no longer exist in Postgres
|
|
108
|
+
Array.from(yOrders.keys()).forEach(key => {
|
|
109
|
+
if (!incomingIds.has(key)) {
|
|
110
|
+
yOrders.delete(key);
|
|
111
|
+
}
|
|
112
|
+
});
|
|
113
|
+
|
|
114
|
+
backendOrders.forEach((o: any) => {
|
|
115
|
+
const id = o.legacyId || o.id;
|
|
116
|
+
const status = mapStageToStatus(o.stage);
|
|
117
|
+
yOrders.set(id, {
|
|
118
|
+
id,
|
|
119
|
+
client: o.clientName || o.clientId || 'Desconocido',
|
|
120
|
+
whatsapp: o.clientPhone ? String(o.clientPhone) : (o.whatsapp ? String(o.whatsapp) : '+57 000 000 0000'),
|
|
121
|
+
material: o.salesObservation || o.material || o.items?.[0]?.woodType || 'Desconocido',
|
|
122
|
+
measurements: o.items?.[0]?.dimensions || 'Ver O.T.',
|
|
123
|
+
sheets: o.items?.[0]?.quantity || 1,
|
|
124
|
+
status,
|
|
125
|
+
rawStage: o.stage,
|
|
126
|
+
machine: o.machine || 'Sin Asignar',
|
|
127
|
+
slaHours: o.is24Hours ? 24 : 72,
|
|
128
|
+
isUrgent: o.isUrgent || false,
|
|
129
|
+
isPaid: o.isPaid || false,
|
|
130
|
+
createdAt: new Date(o.createdAt).toLocaleDateString(),
|
|
131
|
+
logs: yOrders.get(id)?.logs || []
|
|
132
|
+
});
|
|
133
|
+
});
|
|
134
|
+
});
|
|
135
|
+
} catch (error: any) {
|
|
136
|
+
console.error('[Chameleon Store] Error fetching orders:', error);
|
|
137
|
+
toast.error(`SysError: ${error.message}`);
|
|
138
|
+
}
|
|
139
|
+
},
|
|
140
|
+
|
|
141
|
+
createOrder: (data) => {
|
|
142
|
+
const sla = calculateSLA(data.sheets);
|
|
143
|
+
const machine = assignMachine(data.material, data.sheets);
|
|
144
|
+
const phone = sanitizePhone(data.whatsapp);
|
|
145
|
+
const id = `MF-${Math.floor(1000 + Math.random() * 9000)}`;
|
|
146
|
+
|
|
147
|
+
const newOrder: MadefrontOrder = {
|
|
148
|
+
id,
|
|
149
|
+
client: data.client,
|
|
150
|
+
whatsapp: phone,
|
|
151
|
+
material: data.material,
|
|
152
|
+
measurements: data.measurements,
|
|
153
|
+
sheets: parseInt(data.sheets),
|
|
154
|
+
status: 'DISEÑO', // Siempre arranca aquí
|
|
155
|
+
machine,
|
|
156
|
+
slaHours: sla,
|
|
157
|
+
isPaid: false,
|
|
158
|
+
createdAt: new Date().toISOString(),
|
|
159
|
+
logs: [`Pedido creado. SLA calculado: ${sla}h. Máquina sugerida: ${machine}`]
|
|
160
|
+
};
|
|
161
|
+
|
|
162
|
+
yOrders.set(id, newOrder);
|
|
163
|
+
|
|
164
|
+
kernel.execute('broadcast_message', {
|
|
165
|
+
message: JSON.stringify({ type: 'ORDER_CREATED', payload: newOrder })
|
|
166
|
+
}).catch(e => console.error("Sync Error", e));
|
|
167
|
+
},
|
|
168
|
+
|
|
169
|
+
updateStatus: async (id, targetStatus) => {
|
|
170
|
+
const order = yOrders.get(id);
|
|
171
|
+
if (!order) return;
|
|
172
|
+
|
|
173
|
+
const transition = canTransition(order, targetStatus);
|
|
174
|
+
if (!transition.allowed) {
|
|
175
|
+
console.warn(`[Workflow Engine] Bloqueo: ${transition.reason}`);
|
|
176
|
+
setTimeout(() => toast.error('Transición Bloqueada', { description: transition.reason }), 0);
|
|
177
|
+
return;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
let newBackendStage = 'DESIGN';
|
|
181
|
+
switch (targetStatus) {
|
|
182
|
+
case 'CAJA': newBackendStage = 'AUTHORIZED_FOR_PRODUCTION'; break;
|
|
183
|
+
case 'PRODUCCION': newBackendStage = 'PRODUCTION'; break;
|
|
184
|
+
case 'DESPACHO': newBackendStage = 'DISPATCH'; break;
|
|
185
|
+
case 'ENTREGADO': newBackendStage = 'DELIVERED'; break;
|
|
186
|
+
case 'OPTIMIZACION': newBackendStage = 'IN_CUTTING'; break;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
try {
|
|
190
|
+
const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
|
|
191
|
+
|
|
192
|
+
const response = await fetch(`${baseUrl}/api/orders/${order.id}/dynamic-move`, {
|
|
193
|
+
method: 'POST',
|
|
194
|
+
headers: {
|
|
195
|
+
'Content-Type': 'application/json',
|
|
196
|
+
'x-api-key': 'dev_ak_chameleon_studio_99x'
|
|
197
|
+
},
|
|
198
|
+
body: JSON.stringify({ newStage: newBackendStage })
|
|
199
|
+
});
|
|
200
|
+
|
|
201
|
+
if (!response.ok) {
|
|
202
|
+
const errData = await response.json().catch(() => ({}));
|
|
203
|
+
throw new Error(errData.error || `Error HTTP: ${response.status}`);
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
const logEntry = `[${new Date().toLocaleTimeString()}] Movido a ${targetStatus}`;
|
|
207
|
+
const updatedOrder = { ...order, status: targetStatus, logs: [...order.logs, logEntry] };
|
|
208
|
+
|
|
209
|
+
yOrders.set(id, updatedOrder);
|
|
210
|
+
|
|
211
|
+
await kernel.execute('broadcast_message', {
|
|
212
|
+
message: JSON.stringify({ type: 'MadefrontOrderUpdated', data: updatedOrder })
|
|
213
|
+
});
|
|
214
|
+
} catch (err: any) {
|
|
215
|
+
toast.error(`Error persistiendo estado: ${err.message}`);
|
|
216
|
+
}
|
|
217
|
+
},
|
|
218
|
+
|
|
219
|
+
updateOrder: (updatedOrder: MadefrontOrder) => {
|
|
220
|
+
if (yOrders.has(updatedOrder.id)) {
|
|
221
|
+
yOrders.set(updatedOrder.id, updatedOrder);
|
|
222
|
+
}
|
|
223
|
+
},
|
|
224
|
+
|
|
225
|
+
approvePayment: async (id: string) => {
|
|
226
|
+
const order = yOrders.get(id);
|
|
227
|
+
if (!order) return;
|
|
228
|
+
|
|
229
|
+
const updatedOrder = {
|
|
230
|
+
...order,
|
|
231
|
+
isPaid: true,
|
|
232
|
+
status: 'PRODUCCION' as OrderStatus,
|
|
233
|
+
logs: [...order.logs, '💰 Pago Aprobado por Caja -> Enviado a Planta']
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
yOrders.set(id, updatedOrder);
|
|
237
|
+
|
|
238
|
+
try {
|
|
239
|
+
const baseUrl = (import.meta as any).env?.VITE_API_BASE_URL || 'http://localhost:3001';
|
|
240
|
+
|
|
241
|
+
await fetch(`${baseUrl}/api/orders/${order.id}/dynamic-move`, {
|
|
242
|
+
method: 'POST',
|
|
243
|
+
headers: {
|
|
244
|
+
'Content-Type': 'application/json',
|
|
245
|
+
'x-api-key': 'dev_ak_chameleon_studio_99x'
|
|
246
|
+
},
|
|
247
|
+
body: JSON.stringify({ newStage: 'PRODUCTION' })
|
|
248
|
+
});
|
|
249
|
+
await kernel.execute('broadcast_message', {
|
|
250
|
+
message: JSON.stringify({ type: 'MadefrontOrderUpdated', data: updatedOrder })
|
|
251
|
+
});
|
|
252
|
+
} catch (e) { }
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
getOrdersByStage: (status: OrderStatus) => get().orders.filter((o) => o.status === status),
|
|
256
|
+
getPendingPayments: () => get().orders.filter((o) => o.status === 'CAJA' && !o.isPaid),
|
|
257
|
+
};
|
|
258
|
+
},
|
|
259
|
+
{
|
|
260
|
+
name: 'madefront-db-v2', // Nombre en localStorage
|
|
261
|
+
}
|
|
262
|
+
)
|
|
263
|
+
);
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
import { ViewType } from '../types';
|
|
3
|
+
|
|
4
|
+
interface UIState {
|
|
5
|
+
currentView: ViewType;
|
|
6
|
+
searchTerm: string;
|
|
7
|
+
|
|
8
|
+
// Actions
|
|
9
|
+
setCurrentView: (view: ViewType) => void;
|
|
10
|
+
setSearchTerm: (term: string) => void;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
export const useUIStore = create<UIState>((set) => ({
|
|
14
|
+
currentView: 'login',
|
|
15
|
+
searchTerm: '',
|
|
16
|
+
|
|
17
|
+
setCurrentView: (view) => set({ currentView: view }),
|
|
18
|
+
setSearchTerm: (term) => set({ searchTerm: term })
|
|
19
|
+
}));
|