@decido/plugin-planta-erp 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 +0 -0
- package/dist/assets/index-YxWqJStM.js +75214 -0
- package/dist/assets/style.css +829 -0
- package/dist/index.html +13 -0
- package/dist/manifest.json +16 -0
- package/dist-server/index.cjs +95 -0
- package/dist-server/index.d.cts +25 -0
- package/dist-server/index.d.ts +25 -0
- package/dist-server/index.js +69 -0
- package/index.html +12 -0
- package/index.ts +26 -0
- package/manifest.ts +24 -0
- package/package.json +67 -0
- package/public/manifest.json +16 -0
- package/server/index.d.ts +1 -0
- package/server/index.js +1 -0
- package/server/package.json +4 -0
- package/src/components/DigitalTwin/PhysicalCanvas.tsx +80 -0
- package/src/components/DigitalTwin/nodes/MachineNode3D.tsx +61 -0
- package/src/components/DigitalTwin/nodes/OrderProduct3D.tsx +54 -0
- package/src/components/SwarmCanvas.tsx +133 -0
- package/src/dev-mount.tsx +10 -0
- package/src/index.css +3 -0
- package/src/logic/rules.ts +39 -0
- package/src/logic/workflowGuard.ts +45 -0
- package/src/stores/digitalTwinStore.ts +66 -0
- package/src/stores/uiStore.ts +19 -0
- package/src/types.ts +38 -0
- package/src/utils/sounds.ts +52 -0
- package/src-backend/index.ts +62 -0
- package/src-backend/migrations.ts +33 -0
- package/tsconfig.json +23 -0
- package/tsup.config.ts +8 -0
- package/vite.config.ts +39 -0
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import { kernel } from '@decido/kernel-bridge';
|
|
3
|
+
import { ReactFlow, Background, BackgroundVariant, Controls } from '@xyflow/react';
|
|
4
|
+
import { nodeTypes, edgeTypes } from '@decido/canvas-core';
|
|
5
|
+
import '@xyflow/react/dist/style.css';
|
|
6
|
+
|
|
7
|
+
// Pre-posiciones estáticas para los agentes más comunes en el clúster
|
|
8
|
+
const KNOWN_NODE_POSITIONS: Record<string, { x: number, y: number }> = {
|
|
9
|
+
'user-client': { x: 50, y: 250 },
|
|
10
|
+
'dev-master': { x: 400, y: 100 },
|
|
11
|
+
'ui-morpher': { x: 400, y: 400 },
|
|
12
|
+
'system-orchestrator': { x: 750, y: 250 }
|
|
13
|
+
};
|
|
14
|
+
|
|
15
|
+
export const SwarmCanvas: React.FC = () => {
|
|
16
|
+
const [nodes, setNodes] = useState<any[]>([]);
|
|
17
|
+
const [edges, setEdges] = useState<any[]>([]);
|
|
18
|
+
|
|
19
|
+
const [activeAgents, setActiveAgents] = useState<Set<string>>(new Set(['user-client', 'dev-master']));
|
|
20
|
+
|
|
21
|
+
useEffect(() => {
|
|
22
|
+
// Al montar, registrar nodes base
|
|
23
|
+
const baseNodes = Array.from(activeAgents).map((id, i) => ({
|
|
24
|
+
id,
|
|
25
|
+
type: 'moduleNode',
|
|
26
|
+
position: KNOWN_NODE_POSITIONS[id] || { x: 200 + (i * 100), y: 200 + (Math.random() * 100) },
|
|
27
|
+
data: {
|
|
28
|
+
label: id === 'user-client' ? 'Web Client' : id,
|
|
29
|
+
status: 'active',
|
|
30
|
+
description: id === 'dev-master' ? 'Rust Orchestrator' : 'Enjambre'
|
|
31
|
+
}
|
|
32
|
+
}));
|
|
33
|
+
setNodes(baseNodes);
|
|
34
|
+
}, []);
|
|
35
|
+
|
|
36
|
+
// Escuchar el Kernel Universal Córtex para Eventos del Enjambre
|
|
37
|
+
useEffect(() => {
|
|
38
|
+
const unsubscribe = kernel.onEvent((evt: any) => {
|
|
39
|
+
if (!evt || !evt.event_type) return;
|
|
40
|
+
|
|
41
|
+
// Detectamos si es un mensaje de chat o un Action command
|
|
42
|
+
if (evt.event_type === 'MessageSent' || evt.event_type === 'AgentCommand') {
|
|
43
|
+
const messageData = evt.data || {};
|
|
44
|
+
const from = evt.agent_id || messageData.sender || 'unknown';
|
|
45
|
+
const to = messageData.target || 'broadcast';
|
|
46
|
+
const intent = messageData.intent || evt.event_type;
|
|
47
|
+
|
|
48
|
+
// Si descubrimos un agente nuevo, lo agreamos al canvas
|
|
49
|
+
setActiveAgents(prev => {
|
|
50
|
+
const next = new Set(prev);
|
|
51
|
+
let changed = false;
|
|
52
|
+
if (!next.has(from)) { next.add(from); changed = true; }
|
|
53
|
+
if (to !== 'broadcast' && !next.has(to)) { next.add(to); changed = true; }
|
|
54
|
+
|
|
55
|
+
if (changed) {
|
|
56
|
+
// Actualizar Nodos
|
|
57
|
+
const allAgents = Array.from(next);
|
|
58
|
+
setNodes(allAgents.map((id) => ({
|
|
59
|
+
id,
|
|
60
|
+
type: 'moduleNode',
|
|
61
|
+
position: KNOWN_NODE_POSITIONS[id] || { x: 300 + (Math.random() * 300), y: 150 + (Math.random() * 300) },
|
|
62
|
+
data: { label: id, status: 'active' }
|
|
63
|
+
})));
|
|
64
|
+
}
|
|
65
|
+
return next;
|
|
66
|
+
});
|
|
67
|
+
|
|
68
|
+
// Crear Edge Animado (Sonda Visual)
|
|
69
|
+
const edgeId = `edge-${Date.now()}-${Math.random()}`;
|
|
70
|
+
const newEdge = {
|
|
71
|
+
id: edgeId,
|
|
72
|
+
source: from,
|
|
73
|
+
target: to === 'broadcast' ? 'dev-master' : to, // Fallback visual
|
|
74
|
+
type: 'animatedEdge',
|
|
75
|
+
animated: true,
|
|
76
|
+
data: { label: intent }
|
|
77
|
+
};
|
|
78
|
+
|
|
79
|
+
setEdges(prev => [...prev, newEdge]);
|
|
80
|
+
|
|
81
|
+
// Remover el edge después de 4 segundos para no saturar el canvas (efecto "pulso")
|
|
82
|
+
setTimeout(() => {
|
|
83
|
+
setEdges(prev => prev.filter(e => e.id !== edgeId));
|
|
84
|
+
}, 4000);
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
// También escuchamos nuestro propio evento sintético (como fallback a eventos en la memoria local del navegador)
|
|
89
|
+
const handleSynthetic = (_e: any) => {
|
|
90
|
+
// Este fallback es solo por si kernel.onEvent falla en la configuración hibrida.
|
|
91
|
+
// Pero con el Proxy Relay de Node esto debería llegar nativamente por redis stream.
|
|
92
|
+
};
|
|
93
|
+
|
|
94
|
+
window.addEventListener('kernel-synthetic-message', handleSynthetic);
|
|
95
|
+
|
|
96
|
+
return () => {
|
|
97
|
+
unsubscribe();
|
|
98
|
+
window.removeEventListener('kernel-synthetic-message', handleSynthetic);
|
|
99
|
+
};
|
|
100
|
+
}, []);
|
|
101
|
+
|
|
102
|
+
return (
|
|
103
|
+
<div className="w-full h-full bg-[#0a0a0a] rounded-xl overflow-hidden border border-white/10 relative">
|
|
104
|
+
<div className="absolute top-4 left-4 z-10 bg-black/60 backdrop-blur-md px-4 py-2 rounded-lg border border-primary/20">
|
|
105
|
+
<h3 className="text-white font-bold flex items-center gap-2">
|
|
106
|
+
<span className="w-2 h-2 rounded-full bg-green-500 animate-pulse"></span>
|
|
107
|
+
Swarm Flow Probes
|
|
108
|
+
</h3>
|
|
109
|
+
<p className="text-xs text-gray-400 mt-1">Interceptando Redis/RPC Bus</p>
|
|
110
|
+
</div>
|
|
111
|
+
<ReactFlow
|
|
112
|
+
nodes={nodes}
|
|
113
|
+
edges={edges}
|
|
114
|
+
onNodesChange={(changes: any) => {
|
|
115
|
+
// Mapear cambios localmente si se arrastran interactuando
|
|
116
|
+
setNodes(nds => nds.map(n => {
|
|
117
|
+
const change = changes.find((c: any) => c.id === n.id);
|
|
118
|
+
if (change && change.type === 'position' && change.position) {
|
|
119
|
+
return { ...n, position: change.position };
|
|
120
|
+
}
|
|
121
|
+
return n;
|
|
122
|
+
}));
|
|
123
|
+
}}
|
|
124
|
+
nodeTypes={nodeTypes}
|
|
125
|
+
edgeTypes={edgeTypes as any}
|
|
126
|
+
fitView
|
|
127
|
+
>
|
|
128
|
+
<Background variant={BackgroundVariant.Dots} color="rgba(0, 255, 136, 0.15)" />
|
|
129
|
+
<Controls />
|
|
130
|
+
</ReactFlow>
|
|
131
|
+
</div>
|
|
132
|
+
);
|
|
133
|
+
};
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
import React from 'react';
|
|
2
|
+
import ReactDOM from 'react-dom/client';
|
|
3
|
+
import { PhysicalCanvas } from './components/DigitalTwin/PhysicalCanvas';
|
|
4
|
+
import './index.css';
|
|
5
|
+
|
|
6
|
+
ReactDOM.createRoot(document.getElementById('root')!).render(
|
|
7
|
+
<React.StrictMode>
|
|
8
|
+
<PhysicalCanvas />
|
|
9
|
+
</React.StrictMode>
|
|
10
|
+
);
|
package/src/index.css
ADDED
|
@@ -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'],
|
|
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
|
+
};
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
import { create } from 'zustand';
|
|
2
|
+
|
|
3
|
+
export type MachineState = 'idle' | 'processing' | 'error';
|
|
4
|
+
export type ObjectType = 'machine' | 'order' | 'operator';
|
|
5
|
+
|
|
6
|
+
export interface SpatialNode {
|
|
7
|
+
id: string;
|
|
8
|
+
type: ObjectType;
|
|
9
|
+
position: [number, number, number]; // [x, y, z]
|
|
10
|
+
rotation?: [number, number, number];
|
|
11
|
+
state?: string;
|
|
12
|
+
metadata?: Record<string, any>;
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
interface DigitalTwinState {
|
|
16
|
+
nodes: Record<string, SpatialNode>;
|
|
17
|
+
addNode: (node: SpatialNode) => void;
|
|
18
|
+
updateNodePosition: (id: string, position: [number, number, number], rotation?: [number, number, number]) => void;
|
|
19
|
+
updateNodeState: (id: string, state: string, metadata?: any) => void;
|
|
20
|
+
removeNode: (id: string) => void;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
export const useDigitalTwinStore = create<DigitalTwinState>((set) => ({
|
|
24
|
+
nodes: {
|
|
25
|
+
// Mock default physical layout: Workstations
|
|
26
|
+
'corte-1': { id: 'corte-1', type: 'machine', position: [-4, 0, -2], state: 'idle', metadata: { name: 'Sierra Escuadradora' } },
|
|
27
|
+
'corte-2': { id: 'corte-2', type: 'machine', position: [4, 0, -2], state: 'processing', metadata: { name: 'CNC Router' } },
|
|
28
|
+
'ensamble-1': { id: 'ensamble-1', type: 'machine', position: [0, 0, 4], state: 'idle', metadata: { name: 'Zona Ensamble' } },
|
|
29
|
+
},
|
|
30
|
+
|
|
31
|
+
addNode: (node) => set((state) => ({
|
|
32
|
+
nodes: { ...state.nodes, [node.id]: node }
|
|
33
|
+
})),
|
|
34
|
+
|
|
35
|
+
updateNodePosition: (id, position, rotation) => set((state) => {
|
|
36
|
+
const node = state.nodes[id];
|
|
37
|
+
if (!node) return state;
|
|
38
|
+
return {
|
|
39
|
+
nodes: {
|
|
40
|
+
...state.nodes,
|
|
41
|
+
[id]: { ...node, position, rotation: rotation || node.rotation }
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}),
|
|
45
|
+
|
|
46
|
+
updateNodeState: (id, newState, metadata) => set((state) => {
|
|
47
|
+
const node = state.nodes[id];
|
|
48
|
+
if (!node) return state;
|
|
49
|
+
return {
|
|
50
|
+
nodes: {
|
|
51
|
+
...state.nodes,
|
|
52
|
+
[id]: {
|
|
53
|
+
...node,
|
|
54
|
+
state: newState,
|
|
55
|
+
metadata: metadata ? { ...node.metadata, ...metadata } : node.metadata
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
};
|
|
59
|
+
}),
|
|
60
|
+
|
|
61
|
+
removeNode: (id) => set((state) => {
|
|
62
|
+
const newNodes = { ...state.nodes };
|
|
63
|
+
delete newNodes[id];
|
|
64
|
+
return { nodes: newNodes };
|
|
65
|
+
})
|
|
66
|
+
}));
|
|
@@ -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
|
+
}));
|
package/src/types.ts
ADDED
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
// 1. Vistas Disponibles en la App
|
|
2
|
+
export type ViewType = 'login' | 'madefront_chat' | 'madefront_kanban' | 'madefront_excel' | 'workflow_admin';
|
|
3
|
+
|
|
4
|
+
// 2. Roles del Sistema (Los Cortex)
|
|
5
|
+
export type Role = 'NONE' | 'GERENCIA' | 'CAJA' | 'PLANTA' | 'DESPACHO';
|
|
6
|
+
|
|
7
|
+
// 3. Estados del Pedido (El Flujo)
|
|
8
|
+
export type OrderStatus = 'DISEÑO' | 'OPTIMIZACION' | 'CAJA' | 'PRODUCCION' | 'DESPACHO' | 'ENTREGADO';
|
|
9
|
+
|
|
10
|
+
// 4. Máquinas Disponibles
|
|
11
|
+
export type MachineType = 'SIGMA' | 'GABBIANI' | 'HOMAG' | 'PENDIENTE';
|
|
12
|
+
|
|
13
|
+
// 5. El Pedido Maestro
|
|
14
|
+
export interface MadefrontOrder {
|
|
15
|
+
id: string; // MF-2026-001
|
|
16
|
+
client: string;
|
|
17
|
+
whatsapp: string; // +57...
|
|
18
|
+
material: string; // Ej: Rh 15mm Blanco
|
|
19
|
+
measurements: string; // Texto o Link
|
|
20
|
+
sheets: number; // Cantidad de láminas (Para el SLA)
|
|
21
|
+
|
|
22
|
+
// Datos Calculados por el "Agente"
|
|
23
|
+
status: OrderStatus;
|
|
24
|
+
machine: MachineType;
|
|
25
|
+
slaHours: number; // 24 o 72
|
|
26
|
+
createdAt: string; // ISO Date
|
|
27
|
+
isUrgent?: boolean; // Bandera de prioridad
|
|
28
|
+
|
|
29
|
+
// Trazabilidad
|
|
30
|
+
isPaid: boolean; // Bloqueo de Caja
|
|
31
|
+
logs: string[]; // Historial de cambios
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
export interface AgentCommand {
|
|
35
|
+
action: 'CHANGE_VIEW' | 'SEARCH_ORDER' | 'UNKNOWN';
|
|
36
|
+
payload?: any;
|
|
37
|
+
message?: string;
|
|
38
|
+
}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
export const playUISound = (type: 'pop' | 'swoosh' | 'success' | 'error') => {
|
|
2
|
+
try {
|
|
3
|
+
const AudioContextClass = window.AudioContext || (window as any).webkitAudioContext;
|
|
4
|
+
if (!AudioContextClass) return;
|
|
5
|
+
|
|
6
|
+
const ctx = new AudioContextClass();
|
|
7
|
+
const osc = ctx.createOscillator();
|
|
8
|
+
const gain = ctx.createGain();
|
|
9
|
+
|
|
10
|
+
osc.connect(gain);
|
|
11
|
+
gain.connect(ctx.destination);
|
|
12
|
+
|
|
13
|
+
const now = ctx.currentTime;
|
|
14
|
+
|
|
15
|
+
if (type === 'pop') {
|
|
16
|
+
osc.type = 'sine';
|
|
17
|
+
osc.frequency.setValueAtTime(400, now);
|
|
18
|
+
osc.frequency.exponentialRampToValueAtTime(600, now + 0.1);
|
|
19
|
+
gain.gain.setValueAtTime(0.1, now);
|
|
20
|
+
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.1);
|
|
21
|
+
osc.start(now);
|
|
22
|
+
osc.stop(now + 0.1);
|
|
23
|
+
} else if (type === 'swoosh') {
|
|
24
|
+
osc.type = 'triangle';
|
|
25
|
+
osc.frequency.setValueAtTime(150, now);
|
|
26
|
+
osc.frequency.exponentialRampToValueAtTime(300, now + 0.25);
|
|
27
|
+
gain.gain.setValueAtTime(0.05, now);
|
|
28
|
+
gain.gain.exponentialRampToValueAtTime(0.01, now + 0.25);
|
|
29
|
+
osc.start(now);
|
|
30
|
+
osc.stop(now + 0.25);
|
|
31
|
+
} else if (type === 'success') {
|
|
32
|
+
osc.type = 'sine';
|
|
33
|
+
osc.frequency.setValueAtTime(440, now); // A4
|
|
34
|
+
osc.frequency.setValueAtTime(554.37, now + 0.1); // C#5
|
|
35
|
+
gain.gain.setValueAtTime(0.1, now);
|
|
36
|
+
gain.gain.setValueAtTime(0.1, now + 0.1);
|
|
37
|
+
gain.gain.linearRampToValueAtTime(0, now + 0.3);
|
|
38
|
+
osc.start(now);
|
|
39
|
+
osc.stop(now + 0.3);
|
|
40
|
+
} else if (type === 'error') {
|
|
41
|
+
osc.type = 'sawtooth';
|
|
42
|
+
osc.frequency.setValueAtTime(200, now);
|
|
43
|
+
osc.frequency.exponentialRampToValueAtTime(100, now + 0.2);
|
|
44
|
+
gain.gain.setValueAtTime(0.1, now);
|
|
45
|
+
gain.gain.linearRampToValueAtTime(0, now + 0.2);
|
|
46
|
+
osc.start(now);
|
|
47
|
+
osc.stop(now + 0.2);
|
|
48
|
+
}
|
|
49
|
+
} catch (e) {
|
|
50
|
+
console.error("Audio playback failed", e);
|
|
51
|
+
}
|
|
52
|
+
};
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
import { Router } from 'express';
|
|
2
|
+
import { Server } from 'socket.io';
|
|
3
|
+
import { IServerPlugin } from '@decido/sdk';
|
|
4
|
+
import { plantaMigrations } from './migrations';
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Dependencias físicas de la industria privada proporcionadas
|
|
8
|
+
* por el Host para inyección y enrutamiento en el Gemelo Digital.
|
|
9
|
+
*/
|
|
10
|
+
export interface PlantaErpBackendDeps {
|
|
11
|
+
digitalTwinRouter: Router;
|
|
12
|
+
simulationRouter: Router;
|
|
13
|
+
simulationStudioRouter: Router;
|
|
14
|
+
trackingRouter: Router;
|
|
15
|
+
whatsappAutomationRouter: Router;
|
|
16
|
+
whatsappWebhookRouter: Router;
|
|
17
|
+
initServicesHook?: () => Promise<void>;
|
|
18
|
+
initSocketsHook?: (io: Server) => void;
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
/**
|
|
22
|
+
* Crea la Extensión B2B para Planta Industria, conectando
|
|
23
|
+
* el Engine de Renderizado, Simulaciones y Automatización WA.
|
|
24
|
+
*/
|
|
25
|
+
export const createPlantaErpServerPlugin = (deps: PlantaErpBackendDeps): IServerPlugin => ({
|
|
26
|
+
name: 'planta_erp_backend',
|
|
27
|
+
|
|
28
|
+
mountRoutes: (apiRouter: Router) => {
|
|
29
|
+
// Enrutamiento Patentado Industrial
|
|
30
|
+
apiRouter.use('/twin', deps.digitalTwinRouter);
|
|
31
|
+
apiRouter.use('/simulation', deps.simulationRouter);
|
|
32
|
+
apiRouter.use('/scenarios', deps.simulationStudioRouter);
|
|
33
|
+
|
|
34
|
+
// Alias de retrocompatibilidad
|
|
35
|
+
apiRouter.use('/simulation/scenarios', deps.simulationStudioRouter);
|
|
36
|
+
|
|
37
|
+
apiRouter.use('/track', deps.trackingRouter);
|
|
38
|
+
apiRouter.use('/whatsapp', deps.whatsappAutomationRouter);
|
|
39
|
+
apiRouter.use('/webhooks/whatsapp', deps.whatsappWebhookRouter);
|
|
40
|
+
},
|
|
41
|
+
|
|
42
|
+
mountSockets: (io: Server) => {
|
|
43
|
+
if (deps.initSocketsHook) {
|
|
44
|
+
deps.initSocketsHook(io);
|
|
45
|
+
}
|
|
46
|
+
},
|
|
47
|
+
|
|
48
|
+
initServices: async () => {
|
|
49
|
+
console.log('[PlantaErpServerPlugin] Initializing Industrial services...');
|
|
50
|
+
if (deps.initServicesHook) {
|
|
51
|
+
await deps.initServicesHook();
|
|
52
|
+
}
|
|
53
|
+
},
|
|
54
|
+
|
|
55
|
+
getMigrations: async () => {
|
|
56
|
+
return {
|
|
57
|
+
pluginName: 'planta_erp_backend',
|
|
58
|
+
schemaName: 'planta',
|
|
59
|
+
migrations: plantaMigrations
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
});
|
|
@@ -0,0 +1,33 @@
|
|
|
1
|
+
export const plantaMigrations = [
|
|
2
|
+
{
|
|
3
|
+
name: '001_init_planta_schema',
|
|
4
|
+
sql: `
|
|
5
|
+
-- 1. Create Schema
|
|
6
|
+
CREATE SCHEMA IF NOT EXISTS planta;
|
|
7
|
+
|
|
8
|
+
-- 2. Create Base Machine Nodes Table (Industrial Property)
|
|
9
|
+
CREATE TABLE IF NOT EXISTS planta.machine_nodes (
|
|
10
|
+
id UUID PRIMARY KEY DEFAULT gen_random_uuid(),
|
|
11
|
+
tenant_id UUID NOT NULL,
|
|
12
|
+
name VARCHAR(255) NOT NULL,
|
|
13
|
+
node_type VARCHAR(100) NOT NULL,
|
|
14
|
+
metadata JSONB DEFAULT '{}',
|
|
15
|
+
created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
|
|
16
|
+
);
|
|
17
|
+
|
|
18
|
+
-- 3. Enforce RLS (Zero-Trust Security)
|
|
19
|
+
ALTER TABLE planta.machine_nodes ENABLE ROW LEVEL SECURITY;
|
|
20
|
+
|
|
21
|
+
DO $$
|
|
22
|
+
BEGIN
|
|
23
|
+
IF NOT EXISTS (
|
|
24
|
+
SELECT 1 FROM pg_policies
|
|
25
|
+
WHERE schemaname = 'planta' AND tablename = 'machine_nodes' AND policyname = 'tenant_isolation_policy'
|
|
26
|
+
) THEN
|
|
27
|
+
CREATE POLICY tenant_isolation_policy ON planta.machine_nodes
|
|
28
|
+
USING (tenant_id = nullif(current_setting('app.current_tenant', true), '')::uuid);
|
|
29
|
+
END IF;
|
|
30
|
+
END $$;
|
|
31
|
+
`
|
|
32
|
+
}
|
|
33
|
+
];
|
package/tsconfig.json
ADDED
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
{
|
|
2
|
+
"extends": "../../../tsconfig.base.json",
|
|
3
|
+
"compilerOptions": {
|
|
4
|
+
"outDir": "dist",
|
|
5
|
+
"rootDir": ".",
|
|
6
|
+
"moduleResolution": "Bundler",
|
|
7
|
+
"lib": [
|
|
8
|
+
"DOM",
|
|
9
|
+
"DOM.Iterable",
|
|
10
|
+
"ES2023"
|
|
11
|
+
]
|
|
12
|
+
},
|
|
13
|
+
"include": [
|
|
14
|
+
"index.ts",
|
|
15
|
+
"manifest.ts",
|
|
16
|
+
"src/**/*.ts",
|
|
17
|
+
"src/**/*.tsx"
|
|
18
|
+
],
|
|
19
|
+
"exclude": [
|
|
20
|
+
"node_modules",
|
|
21
|
+
"dist"
|
|
22
|
+
]
|
|
23
|
+
}
|
package/tsup.config.ts
ADDED
package/vite.config.ts
ADDED
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
import { defineConfig } from 'vite';
|
|
2
|
+
import react from '@vitejs/plugin-react';
|
|
3
|
+
import tailwindcss from '@tailwindcss/vite';
|
|
4
|
+
|
|
5
|
+
export default defineConfig({
|
|
6
|
+
base: 'http://localhost:5004/',
|
|
7
|
+
plugins: [
|
|
8
|
+
tailwindcss(),
|
|
9
|
+
react()
|
|
10
|
+
],
|
|
11
|
+
server: {
|
|
12
|
+
host: true,
|
|
13
|
+
port: 5004,
|
|
14
|
+
strictPort: true,
|
|
15
|
+
cors: true
|
|
16
|
+
},
|
|
17
|
+
preview: {
|
|
18
|
+
host: true,
|
|
19
|
+
port: 5004,
|
|
20
|
+
strictPort: true,
|
|
21
|
+
cors: true
|
|
22
|
+
},
|
|
23
|
+
build: {
|
|
24
|
+
modulePreload: false,
|
|
25
|
+
target: ['chrome89', 'edge89', 'firefox89', 'safari15'],
|
|
26
|
+
minify: false,
|
|
27
|
+
cssCodeSplit: false,
|
|
28
|
+
rollupOptions: {
|
|
29
|
+
output: {
|
|
30
|
+
assetFileNames: (assetInfo) => {
|
|
31
|
+
if (assetInfo.name === 'style.css' || assetInfo.name?.endsWith('.css')) {
|
|
32
|
+
return 'assets/style.css';
|
|
33
|
+
}
|
|
34
|
+
return 'assets/[name]-[hash][extname]';
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
});
|