@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
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,10 @@
1
+ import {StrictMode} from 'react';
2
+ import {createRoot} from 'react-dom/client';
3
+ import App from './App.tsx';
4
+ import './index.css';
5
+
6
+ createRoot(document.getElementById('root')!).render(
7
+ <StrictMode>
8
+ <App />
9
+ </StrictMode>,
10
+ );
@@ -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
+ }));