@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,292 @@
|
|
|
1
|
+
import React, { useState, useEffect } from 'react';
|
|
2
|
+
import {
|
|
3
|
+
DndContext,
|
|
4
|
+
closestCenter,
|
|
5
|
+
KeyboardSensor,
|
|
6
|
+
PointerSensor,
|
|
7
|
+
useSensor,
|
|
8
|
+
useSensors,
|
|
9
|
+
DragEndEvent
|
|
10
|
+
} from '@dnd-kit/core';
|
|
11
|
+
import {
|
|
12
|
+
SortableContext,
|
|
13
|
+
sortableKeyboardCoordinates,
|
|
14
|
+
verticalListSortingStrategy,
|
|
15
|
+
useSortable
|
|
16
|
+
} from '@dnd-kit/sortable';
|
|
17
|
+
import { CSS } from '@dnd-kit/utilities';
|
|
18
|
+
import { MadefrontOrder, OrderStatus } from '../types';
|
|
19
|
+
import { Phone, Calendar, Ruler, User } from 'lucide-react';
|
|
20
|
+
import { useOrderStore } from '../stores/orderStore';
|
|
21
|
+
|
|
22
|
+
const COLUMNS: { id: OrderStatus; title: string; color: string; coordinator: string }[] = [
|
|
23
|
+
{ id: 'DISEÑO', title: 'Diseño', color: 'bg-blue-500/10 border-blue-500/20 text-blue-700 dark:text-blue-400', coordinator: 'Paola' },
|
|
24
|
+
{ id: 'OPTIMIZACION', title: 'Optimización y Fact.', color: 'bg-purple-500/10 border-purple-500/20 text-purple-700 dark:text-purple-400', coordinator: 'Diego' },
|
|
25
|
+
{ id: 'CAJA', title: 'Caja', color: 'bg-yellow-500/10 border-yellow-500/20 text-yellow-700 dark:text-yellow-400', coordinator: 'Gloria' },
|
|
26
|
+
{ id: 'PRODUCCION', title: 'Producción', color: 'bg-orange-500/10 border-orange-500/20 text-orange-700 dark:text-orange-400', coordinator: 'Gabriel, Diego' },
|
|
27
|
+
{ id: 'DESPACHO', title: 'Despacho', color: 'bg-green-500/10 border-green-500/20 text-green-700 dark:text-green-400', coordinator: 'Manuel' },
|
|
28
|
+
{ id: 'ENTREGADO', title: 'Entregado', color: 'bg-teal-500/10 border-teal-500/20 text-teal-700 dark:text-teal-400', coordinator: 'Local' },
|
|
29
|
+
];
|
|
30
|
+
|
|
31
|
+
function SortableItem({ order }: { order: MadefrontOrder }) {
|
|
32
|
+
const {
|
|
33
|
+
attributes,
|
|
34
|
+
listeners,
|
|
35
|
+
setNodeRef,
|
|
36
|
+
transform,
|
|
37
|
+
transition,
|
|
38
|
+
isDragging
|
|
39
|
+
} = useSortable({ id: order.id });
|
|
40
|
+
|
|
41
|
+
const style = {
|
|
42
|
+
transform: CSS.Transform.toString(transform),
|
|
43
|
+
transition,
|
|
44
|
+
zIndex: isDragging ? 10 : 1,
|
|
45
|
+
opacity: isDragging ? 0.5 : 1,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
return (
|
|
49
|
+
<div
|
|
50
|
+
ref={setNodeRef}
|
|
51
|
+
style={style}
|
|
52
|
+
{...attributes}
|
|
53
|
+
{...listeners}
|
|
54
|
+
className="bg-surface-secondary p-3 rounded-xl shadow-sm lg:shadow border border-border-default mb-3 cursor-grab active:cursor-grabbing hover:border-border-hover hover:shadow-md transition-all group"
|
|
55
|
+
>
|
|
56
|
+
<div className="flex justify-between items-start mb-2">
|
|
57
|
+
<div className="flex flex-col">
|
|
58
|
+
<span className="font-bold text-text-primary text-sm">{order.id}</span>
|
|
59
|
+
{order.rawStage && (
|
|
60
|
+
<span className="text-[9px] uppercase tracking-wider text-text-tertiary mt-0.5 font-bold">
|
|
61
|
+
{order.rawStage.replace(/_/g, ' ')}
|
|
62
|
+
</span>
|
|
63
|
+
)}
|
|
64
|
+
</div>
|
|
65
|
+
<span className="text-[10px] font-medium px-2 py-0.5 bg-surface-tertiary rounded-full text-text-secondary border border-border-default">
|
|
66
|
+
{order.material.substring(0, 15)}...
|
|
67
|
+
</span>
|
|
68
|
+
</div>
|
|
69
|
+
|
|
70
|
+
<div className="space-y-1.5 text-xs text-text-secondary">
|
|
71
|
+
<div className="flex items-center gap-2">
|
|
72
|
+
<User size={13} className="text-text-tertiary" />
|
|
73
|
+
<span className="truncate">{order.client}</span>
|
|
74
|
+
</div>
|
|
75
|
+
<div className="flex items-center gap-2">
|
|
76
|
+
<Ruler size={13} className="text-text-tertiary" />
|
|
77
|
+
<span>{order.measurements}</span>
|
|
78
|
+
</div>
|
|
79
|
+
<div className="flex items-center gap-2">
|
|
80
|
+
<Calendar size={13} className="text-text-tertiary" />
|
|
81
|
+
<span>{order.createdAt}</span>
|
|
82
|
+
</div>
|
|
83
|
+
<div
|
|
84
|
+
className="flex items-center gap-2 cursor-pointer hover:bg-surface-tertiary p-1 -ml-1 rounded transition-colors group/phone mt-1"
|
|
85
|
+
onClick={(e) => {
|
|
86
|
+
e.stopPropagation();
|
|
87
|
+
if (typeof window !== 'undefined') {
|
|
88
|
+
window.dispatchEvent(new CustomEvent('studio:open-plugin', { detail: { pluginId: 'chameleon' } }));
|
|
89
|
+
}
|
|
90
|
+
}}
|
|
91
|
+
title="Abrir WhatsApp Studio"
|
|
92
|
+
>
|
|
93
|
+
<Phone size={13} className="text-green-500 group-hover/phone:animate-pulse" />
|
|
94
|
+
<span className="text-green-600 dark:text-green-400 font-medium group-hover/phone:underline">{order.whatsapp}</span>
|
|
95
|
+
</div>
|
|
96
|
+
</div>
|
|
97
|
+
</div>
|
|
98
|
+
);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
export function MadefrontKanbanView() {
|
|
102
|
+
const { orders, updateStatus } = useOrderStore();
|
|
103
|
+
const [showFilters, setShowFilters] = useState(false);
|
|
104
|
+
const [searchQuery, setSearchQuery] = useState('');
|
|
105
|
+
|
|
106
|
+
// Filter out "ENTREGADO" by default (Pending orders only)
|
|
107
|
+
const DEFAULT_COLS = ['DISEÑO', 'OPTIMIZACION', 'CAJA', 'PRODUCCION', 'DESPACHO'];
|
|
108
|
+
const [visibleColumns, setVisibleColumns] = useState<string[]>(() => {
|
|
109
|
+
if (typeof window !== 'undefined') {
|
|
110
|
+
const saved = localStorage.getItem('mf_kanban_cols');
|
|
111
|
+
if (saved) return JSON.parse(saved);
|
|
112
|
+
}
|
|
113
|
+
return DEFAULT_COLS;
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
useEffect(() => {
|
|
117
|
+
localStorage.setItem('mf_kanban_cols', JSON.stringify(visibleColumns));
|
|
118
|
+
}, [visibleColumns]);
|
|
119
|
+
|
|
120
|
+
useEffect(() => {
|
|
121
|
+
const handleSearch = (e: any) => {
|
|
122
|
+
if (e.detail?.query) {
|
|
123
|
+
setSearchQuery(e.detail.query);
|
|
124
|
+
// Force the Kanban plugin to open if it's hidden behind another view
|
|
125
|
+
window.dispatchEvent(new CustomEvent('studio:open-plugin', { detail: { pluginId: 'chameleon', viewId: 'chameleon-kanban-view' } }));
|
|
126
|
+
}
|
|
127
|
+
};
|
|
128
|
+
window.addEventListener('studio:search-order', handleSearch);
|
|
129
|
+
return () => window.removeEventListener('studio:search-order', handleSearch);
|
|
130
|
+
}, []);
|
|
131
|
+
|
|
132
|
+
const toggleColumn = (colId: string) => {
|
|
133
|
+
setVisibleColumns(prev =>
|
|
134
|
+
prev.includes(colId) ? prev.filter(c => c !== colId) : [...prev, colId]
|
|
135
|
+
);
|
|
136
|
+
};
|
|
137
|
+
|
|
138
|
+
const sensors = useSensors(
|
|
139
|
+
useSensor(PointerSensor, {
|
|
140
|
+
activationConstraint: {
|
|
141
|
+
distance: 5,
|
|
142
|
+
},
|
|
143
|
+
}),
|
|
144
|
+
useSensor(KeyboardSensor, {
|
|
145
|
+
coordinateGetter: sortableKeyboardCoordinates,
|
|
146
|
+
})
|
|
147
|
+
);
|
|
148
|
+
|
|
149
|
+
const handleDragEnd = async (event: DragEndEvent) => {
|
|
150
|
+
const { active, over } = event;
|
|
151
|
+
|
|
152
|
+
if (!over) return;
|
|
153
|
+
|
|
154
|
+
const activeId = active.id;
|
|
155
|
+
const overId = over.id;
|
|
156
|
+
|
|
157
|
+
const activeOrder = orders.find(o => o.id === activeId);
|
|
158
|
+
if (!activeOrder) return;
|
|
159
|
+
|
|
160
|
+
// Determine target column
|
|
161
|
+
let targetStatus: OrderStatus | null = null;
|
|
162
|
+
const overColumn = COLUMNS.find(c => c.id === overId);
|
|
163
|
+
if (overColumn) targetStatus = overColumn.id;
|
|
164
|
+
else {
|
|
165
|
+
const overOrder = orders.find(o => o.id === overId);
|
|
166
|
+
if (overOrder) targetStatus = overOrder.status;
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (targetStatus && activeOrder.status !== targetStatus) {
|
|
170
|
+
// Optimizacion UI Optimista delegada 100% al store
|
|
171
|
+
updateStatus(activeOrder.id, targetStatus as any);
|
|
172
|
+
}
|
|
173
|
+
};
|
|
174
|
+
|
|
175
|
+
return (
|
|
176
|
+
<div className="h-full w-full bg-transparent p-4 flex flex-col overflow-hidden min-h-0 relative">
|
|
177
|
+
<div className="flex justify-between items-center mb-4 shrink-0">
|
|
178
|
+
<h2 className="text-lg font-bold text-text-primary flex items-center gap-2">
|
|
179
|
+
Kanban de Pedidos
|
|
180
|
+
<span className="text-xs bg-brand-primary/10 text-brand-primary px-2 py-0.5 rounded-full border border-brand-primary/20">
|
|
181
|
+
{orders.length} Totales
|
|
182
|
+
</span>
|
|
183
|
+
</h2>
|
|
184
|
+
|
|
185
|
+
<div className="flex items-center gap-3 relative">
|
|
186
|
+
<div className="relative flex items-center">
|
|
187
|
+
<input
|
|
188
|
+
type="text"
|
|
189
|
+
placeholder="Buscar pedido, cliente..."
|
|
190
|
+
value={searchQuery}
|
|
191
|
+
onChange={(e) => setSearchQuery(e.target.value)}
|
|
192
|
+
className="pl-3 pr-8 py-1.5 w-64 bg-surface-secondary border border-border-default rounded-lg text-sm text-text-primary placeholder:text-text-muted focus:outline-none focus:border-brand-primary/50 transition-colors"
|
|
193
|
+
/>
|
|
194
|
+
{searchQuery && (
|
|
195
|
+
<button
|
|
196
|
+
onClick={() => setSearchQuery('')}
|
|
197
|
+
className="absolute right-2 text-text-muted hover:text-text-primary"
|
|
198
|
+
>
|
|
199
|
+
✕
|
|
200
|
+
</button>
|
|
201
|
+
)}
|
|
202
|
+
</div>
|
|
203
|
+
|
|
204
|
+
<button
|
|
205
|
+
onClick={() => setShowFilters(!showFilters)}
|
|
206
|
+
className="flex items-center gap-2 px-3 py-1.5 bg-surface-secondary border border-border-default rounded-lg text-sm font-medium text-text-secondary hover:text-text-primary transition-colors"
|
|
207
|
+
>
|
|
208
|
+
Filtros <span className="opacity-50">▾</span>
|
|
209
|
+
</button>
|
|
210
|
+
|
|
211
|
+
{showFilters && (
|
|
212
|
+
<div className="absolute right-0 top-full mt-2 w-56 bg-surface-elevated border border-border-default shadow-xl rounded-xl p-3 z-50">
|
|
213
|
+
<h3 className="text-xs font-bold text-text-secondary uppercase mb-2">Columnas Visibles</h3>
|
|
214
|
+
<div className="flex flex-col gap-1.5">
|
|
215
|
+
{COLUMNS.map(col => (
|
|
216
|
+
<label key={col.id} className="flex items-center gap-2 text-sm text-text-primary cursor-pointer hover:bg-surface-tertiary p-1.5 rounded-md transition-colors">
|
|
217
|
+
<input
|
|
218
|
+
type="checkbox"
|
|
219
|
+
className="rounded border-border-default bg-surface-secondary text-brand-primary focus:ring-brand-primary/50"
|
|
220
|
+
checked={visibleColumns.includes(col.id)}
|
|
221
|
+
onChange={() => toggleColumn(col.id)}
|
|
222
|
+
/>
|
|
223
|
+
{col.title}
|
|
224
|
+
</label>
|
|
225
|
+
))}
|
|
226
|
+
</div>
|
|
227
|
+
</div>
|
|
228
|
+
)}
|
|
229
|
+
</div>
|
|
230
|
+
</div>
|
|
231
|
+
|
|
232
|
+
<DndContext
|
|
233
|
+
sensors={sensors}
|
|
234
|
+
collisionDetection={closestCenter}
|
|
235
|
+
onDragEnd={handleDragEnd}
|
|
236
|
+
>
|
|
237
|
+
<div className="flex-1 min-h-0 flex gap-6 overflow-x-auto pb-4 pr-4 scrollbar-thin">
|
|
238
|
+
{COLUMNS.filter(c => visibleColumns.includes(c.id)).map(column => {
|
|
239
|
+
const columnOrders = orders.filter(o => {
|
|
240
|
+
if (o.status !== column.id) return false;
|
|
241
|
+
if (searchQuery) {
|
|
242
|
+
const q = searchQuery.toLowerCase();
|
|
243
|
+
return o.id.toLowerCase().includes(q)
|
|
244
|
+
|| o.client.toLowerCase().includes(q)
|
|
245
|
+
|| o.whatsapp.toLowerCase().includes(q)
|
|
246
|
+
|| o.material.toLowerCase().includes(q);
|
|
247
|
+
}
|
|
248
|
+
return true;
|
|
249
|
+
});
|
|
250
|
+
|
|
251
|
+
const MAX_RENDER = 100;
|
|
252
|
+
const displayOrders = columnOrders.slice(0, MAX_RENDER);
|
|
253
|
+
|
|
254
|
+
return (
|
|
255
|
+
<div key={column.id} className="flex flex-col shrink-0 w-[280px] bg-surface-primary/30 rounded-2xl border border-border-default overflow-hidden">
|
|
256
|
+
<div className={`p-3 border-b border-border-default ${column.color}`}>
|
|
257
|
+
<h2 className="font-bold text-base">{column.title}</h2>
|
|
258
|
+
<p className="text-[10px] opacity-80 mt-0.5 flex items-center gap-1 uppercase tracking-wider font-semibold">
|
|
259
|
+
<User size={10} /> {column.coordinator}
|
|
260
|
+
</p>
|
|
261
|
+
<div className="mt-2 text-xs font-medium bg-black/5 dark:bg-white/10 px-2 py-0.5 rounded-md inline-block">
|
|
262
|
+
{columnOrders.length} pedidos
|
|
263
|
+
</div>
|
|
264
|
+
</div>
|
|
265
|
+
|
|
266
|
+
<div className="flex-1 p-4 overflow-y-auto min-h-0">
|
|
267
|
+
<SortableContext
|
|
268
|
+
id={column.id}
|
|
269
|
+
items={displayOrders.map(o => o.id)}
|
|
270
|
+
strategy={verticalListSortingStrategy}
|
|
271
|
+
>
|
|
272
|
+
<div className="min-h-[100px] h-full flex flex-col gap-0 pb-10">
|
|
273
|
+
{displayOrders.map(order => (
|
|
274
|
+
<SortableItem key={order.id} order={order} />
|
|
275
|
+
))}
|
|
276
|
+
{columnOrders.length > MAX_RENDER && (
|
|
277
|
+
<div className="p-3 mt-2 text-center text-xs font-medium text-text-tertiary bg-surface-tertiary/50 rounded-xl border border-border-subtle border-dashed">
|
|
278
|
+
+ {columnOrders.length - MAX_RENDER} pedidos ocultos<br/>
|
|
279
|
+
<span className="text-[10px] text-brand-primary opacity-80 mt-1 inline-block">Usa el buscador para filtrar</span>
|
|
280
|
+
</div>
|
|
281
|
+
)}
|
|
282
|
+
</div>
|
|
283
|
+
</SortableContext>
|
|
284
|
+
</div>
|
|
285
|
+
</div>
|
|
286
|
+
);
|
|
287
|
+
})}
|
|
288
|
+
</div>
|
|
289
|
+
</DndContext>
|
|
290
|
+
</div>
|
|
291
|
+
);
|
|
292
|
+
}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { useOrderStore } from '../../stores/orderStore';
|
|
2
|
+
import { useAuthStore } from '../../stores/authStore';
|
|
3
|
+
import { CheckCircle, DollarSign, LogOut } from 'lucide-react';
|
|
4
|
+
|
|
5
|
+
export const CajaView = () => {
|
|
6
|
+
const { getPendingPayments, approvePayment } = useOrderStore();
|
|
7
|
+
const { logout, currentUser } = useAuthStore();
|
|
8
|
+
const pendingOrders = getPendingPayments();
|
|
9
|
+
|
|
10
|
+
return (
|
|
11
|
+
<div className="min-h-screen bg-slate-50 text-slate-900 font-sans">
|
|
12
|
+
{/* Header Específico de Caja */}
|
|
13
|
+
<header className="bg-white border-b border-emerald-100 px-8 py-4 flex justify-between items-center sticky top-0 z-10">
|
|
14
|
+
<div className="flex items-center gap-3">
|
|
15
|
+
<div className="p-2 bg-emerald-100 text-emerald-600 rounded-lg">
|
|
16
|
+
<DollarSign size={24} />
|
|
17
|
+
</div>
|
|
18
|
+
<div>
|
|
19
|
+
<h1 className="text-xl font-bold text-emerald-950">Terminal de Caja</h1>
|
|
20
|
+
<p className="text-xs text-emerald-600 font-medium uppercase tracking-wider">Operador: {currentUser}</p>
|
|
21
|
+
</div>
|
|
22
|
+
</div>
|
|
23
|
+
<button onClick={logout} className="text-sm text-slate-400 hover:text-red-500 flex items-center gap-2 font-medium">
|
|
24
|
+
<LogOut size={16} /> Salir
|
|
25
|
+
</button>
|
|
26
|
+
</header>
|
|
27
|
+
|
|
28
|
+
{/* Lista de Pagos Pendientes */}
|
|
29
|
+
<main className="p-8 max-w-5xl mx-auto">
|
|
30
|
+
<div className="flex justify-between items-end mb-6">
|
|
31
|
+
<h2 className="text-2xl font-bold text-slate-800">Pendientes de Aprobación</h2>
|
|
32
|
+
<span className="text-sm bg-slate-200 px-3 py-1 rounded-full text-slate-600 font-bold">{pendingOrders.length} PEDIDOS</span>
|
|
33
|
+
</div>
|
|
34
|
+
|
|
35
|
+
<div className="grid gap-4">
|
|
36
|
+
{pendingOrders.map(order => (
|
|
37
|
+
<div key={order.id} className="bg-white p-6 rounded-2xl shadow-sm border border-slate-100 flex justify-between items-center hover:shadow-md transition-shadow">
|
|
38
|
+
<div>
|
|
39
|
+
<div className="flex items-center gap-3 mb-1">
|
|
40
|
+
<span className="text-lg font-black text-slate-800">{order.id}</span>
|
|
41
|
+
<span className="px-2 py-0.5 bg-blue-50 text-blue-600 text-xs font-bold rounded uppercase">{order.client}</span>
|
|
42
|
+
</div>
|
|
43
|
+
<div className="text-sm text-slate-500 flex gap-4">
|
|
44
|
+
<span>📅 SLA: {order.slaHours}h</span>
|
|
45
|
+
<span>🪵 {order.material}</span>
|
|
46
|
+
<span>📏 {order.sheets} Láminas</span>
|
|
47
|
+
</div>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="flex items-center gap-6">
|
|
51
|
+
<div className="text-right">
|
|
52
|
+
<p className="text-xs text-slate-400 uppercase font-bold">Total a Pagar</p>
|
|
53
|
+
<p className="text-xl font-bold text-slate-900">$ {((order.sheets * 250000) + 50000).toLocaleString()}</p>
|
|
54
|
+
</div>
|
|
55
|
+
<button
|
|
56
|
+
onClick={() => approvePayment(order.id)}
|
|
57
|
+
className="bg-emerald-500 hover:bg-emerald-600 text-white px-6 py-3 rounded-xl font-bold flex items-center gap-2 shadow-lg shadow-emerald-200 transition-all active:scale-95"
|
|
58
|
+
>
|
|
59
|
+
<CheckCircle size={20} />
|
|
60
|
+
CONFIRMAR PAGO
|
|
61
|
+
</button>
|
|
62
|
+
</div>
|
|
63
|
+
</div>
|
|
64
|
+
))}
|
|
65
|
+
|
|
66
|
+
{pendingOrders.length === 0 && (
|
|
67
|
+
<div className="text-center py-20 bg-white rounded-3xl border border-dashed border-slate-200">
|
|
68
|
+
<CheckCircle size={48} className="mx-auto text-slate-200 mb-4" />
|
|
69
|
+
<p className="text-slate-400">Todo al día. No hay pagos pendientes.</p>
|
|
70
|
+
</div>
|
|
71
|
+
)}
|
|
72
|
+
</div>
|
|
73
|
+
</main>
|
|
74
|
+
</div>
|
|
75
|
+
);
|
|
76
|
+
};
|
|
@@ -0,0 +1,100 @@
|
|
|
1
|
+
import { useOrderStore } from '../../stores/orderStore';
|
|
2
|
+
import { useAuthStore } from '../../stores/authStore';
|
|
3
|
+
import { Truck, LogOut, CheckCircle, Navigation } from 'lucide-react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
export const DespachoView = () => {
|
|
7
|
+
const { getOrdersByStage, updateStatus } = useOrderStore();
|
|
8
|
+
const { logout, currentUser } = useAuthStore();
|
|
9
|
+
|
|
10
|
+
// Pedidos listos para entregar
|
|
11
|
+
const dispatchOrders = getOrdersByStage('DESPACHO');
|
|
12
|
+
|
|
13
|
+
const handleDelivery = (id: string, whatsapp: string) => {
|
|
14
|
+
updateStatus(id, 'ENTREGADO');
|
|
15
|
+
// MOCK PROACTIVO (Día 5): Toast de WhatsApp al Carpintero/Cliente
|
|
16
|
+
toast.success(`Mensaje enviado a ${whatsapp}`, {
|
|
17
|
+
description: `📦 ¡Tu pedido ${id} ha sido entregado exitosamente!`,
|
|
18
|
+
icon: '✅'
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen bg-slate-900 text-slate-100 font-sans">
|
|
24
|
+
<header className="bg-slate-950 border-b border-purple-500/30 px-6 py-4 flex justify-between items-center sticky top-0 z-10 shadow-lg shadow-purple-500/10">
|
|
25
|
+
<div className="flex items-center gap-3">
|
|
26
|
+
<div className="p-2 bg-purple-500/20 text-purple-400 rounded-lg">
|
|
27
|
+
<Truck size={24} />
|
|
28
|
+
</div>
|
|
29
|
+
<div>
|
|
30
|
+
<h1 className="text-xl font-bold text-white">Logística & Despacho</h1>
|
|
31
|
+
<p className="text-xs text-purple-400 font-medium uppercase tracking-wider">Operador: {currentUser}</p>
|
|
32
|
+
</div>
|
|
33
|
+
</div>
|
|
34
|
+
<button onClick={logout} className="text-sm text-slate-400 hover:text-red-400 flex items-center gap-2 font-medium bg-slate-800 px-4 py-2 rounded-lg transition-colors">
|
|
35
|
+
<LogOut size={16} /> Salir
|
|
36
|
+
</button>
|
|
37
|
+
</header>
|
|
38
|
+
|
|
39
|
+
<main className="p-8 max-w-6xl mx-auto">
|
|
40
|
+
<div className="flex justify-between items-end mb-8">
|
|
41
|
+
<div>
|
|
42
|
+
<h2 className="text-3xl font-bold text-white mb-2">Pedidos Listos para Entrega</h2>
|
|
43
|
+
<p className="text-slate-400">Verifica la identidad antes de entregar el paquete.</p>
|
|
44
|
+
</div>
|
|
45
|
+
<span className="text-sm bg-purple-500/20 border border-purple-500/30 px-4 py-2 rounded-full text-purple-300 font-bold tracking-widest">{dispatchOrders.length} PAQUETES</span>
|
|
46
|
+
</div>
|
|
47
|
+
|
|
48
|
+
<div className="grid grid-cols-1 md:grid-cols-2 gap-6">
|
|
49
|
+
{dispatchOrders.map(order => (
|
|
50
|
+
<div key={order.id} className="bg-slate-800 border border-slate-700/50 rounded-2xl p-6 relative group overflow-hidden hover:border-purple-500/30 transition-colors">
|
|
51
|
+
<div className="flex justify-between items-start mb-6">
|
|
52
|
+
<div>
|
|
53
|
+
<h3 className="text-2xl font-black text-white mb-1 flex items-center gap-2">
|
|
54
|
+
{order.id}
|
|
55
|
+
<span className="bg-green-500/20 text-green-400 text-[10px] px-2 py-1 rounded-full uppercase tracking-widest font-bold">PAGADO</span>
|
|
56
|
+
</h3>
|
|
57
|
+
<p className="text-slate-400 flex items-center gap-2">
|
|
58
|
+
<Navigation size={14} className="text-purple-400" />
|
|
59
|
+
Destino: <strong className="text-white">{order.client}</strong>
|
|
60
|
+
</p>
|
|
61
|
+
</div>
|
|
62
|
+
</div>
|
|
63
|
+
|
|
64
|
+
<div className="grid grid-cols-2 gap-4 mb-6 text-sm bg-slate-900/50 p-4 rounded-xl">
|
|
65
|
+
<div>
|
|
66
|
+
<span className="block text-slate-500 text-xs mb-1 uppercase font-bold tracking-wider">Material</span>
|
|
67
|
+
<span className="font-medium text-slate-200">{order.material}</span>
|
|
68
|
+
</div>
|
|
69
|
+
<div>
|
|
70
|
+
<span className="block text-slate-500 text-xs mb-1 uppercase font-bold tracking-wider">Láminas (Volumen)</span>
|
|
71
|
+
<span className="font-medium text-slate-200">{order.sheets} unid.</span>
|
|
72
|
+
</div>
|
|
73
|
+
<div className="col-span-2 border-t border-slate-700/50 pt-3 mt-1">
|
|
74
|
+
<span className="block text-slate-500 text-xs mb-1 uppercase font-bold tracking-wider">Contacto WhatsApp</span>
|
|
75
|
+
<span className="font-mono text-purple-300">{order.whatsapp}</span>
|
|
76
|
+
</div>
|
|
77
|
+
</div>
|
|
78
|
+
|
|
79
|
+
<button
|
|
80
|
+
onClick={() => handleDelivery(order.id, order.whatsapp)}
|
|
81
|
+
className="w-full bg-purple-600 hover:bg-purple-500 text-white py-4 rounded-xl font-bold flex items-center justify-center gap-2 shadow-lg shadow-purple-900/50 transition-all active:scale-95"
|
|
82
|
+
>
|
|
83
|
+
<CheckCircle size={20} />
|
|
84
|
+
CONFIRMAR ENTREGA
|
|
85
|
+
</button>
|
|
86
|
+
</div>
|
|
87
|
+
))}
|
|
88
|
+
|
|
89
|
+
{dispatchOrders.length === 0 && (
|
|
90
|
+
<div className="col-span-full h-[40vh] flex flex-col items-center justify-center text-slate-500 border-2 border-dashed border-slate-700 rounded-3xl">
|
|
91
|
+
<Truck size={64} className="mb-4 opacity-20" />
|
|
92
|
+
<p className="text-lg font-medium">Bandeja de salida vacía</p>
|
|
93
|
+
<p className="text-sm">Todo ha sido despachado a los clientes.</p>
|
|
94
|
+
</div>
|
|
95
|
+
)}
|
|
96
|
+
</div>
|
|
97
|
+
</main>
|
|
98
|
+
</div>
|
|
99
|
+
);
|
|
100
|
+
};
|
|
@@ -0,0 +1,83 @@
|
|
|
1
|
+
import { useOrderStore } from '../../stores/orderStore';
|
|
2
|
+
import { useAuthStore } from '../../stores/authStore';
|
|
3
|
+
import { Hammer, LogOut, AlertTriangle, CheckSquare } from 'lucide-react';
|
|
4
|
+
import { toast } from 'sonner';
|
|
5
|
+
|
|
6
|
+
export const PlantaView = () => {
|
|
7
|
+
const { getOrdersByStage, updateStatus } = useOrderStore();
|
|
8
|
+
const { logout } = useAuthStore();
|
|
9
|
+
|
|
10
|
+
// Filtramos lo que está listo para corte
|
|
11
|
+
const productionOrders = getOrdersByStage('PRODUCCION');
|
|
12
|
+
|
|
13
|
+
const handleFinish = (id: string) => {
|
|
14
|
+
updateStatus(id, 'DESPACHO');
|
|
15
|
+
// MOCK PROACTIVO (Día 5): Toast simulando envío a WhatsApp
|
|
16
|
+
toast.success(`Enviando WhatsApp a +57...: El pedido ${id} pasó a Despacho`, {
|
|
17
|
+
description: 'Notificación de Producción',
|
|
18
|
+
icon: '📱'
|
|
19
|
+
});
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
return (
|
|
23
|
+
<div className="min-h-screen bg-neutral-900 text-white font-sans selection:bg-orange-500 selection:text-white">
|
|
24
|
+
{/* Header Industrial */}
|
|
25
|
+
<header className="bg-black border-b border-orange-600/30 px-6 py-4 flex justify-between items-center">
|
|
26
|
+
<div className="flex items-center gap-4">
|
|
27
|
+
<div className="w-3 h-3 rounded-full bg-green-500 animate-pulse shadow-[0_0_10px_#22c55e]"></div>
|
|
28
|
+
<h1 className="text-2xl font-black tracking-tighter text-orange-500">KIOSCO DE PLANTA</h1>
|
|
29
|
+
</div>
|
|
30
|
+
<button onClick={logout} className="bg-neutral-800 p-3 rounded-lg hover:bg-neutral-700">
|
|
31
|
+
<LogOut size={24} />
|
|
32
|
+
</button>
|
|
33
|
+
</header>
|
|
34
|
+
|
|
35
|
+
<main className="p-6">
|
|
36
|
+
{productionOrders.length === 0 ? (
|
|
37
|
+
<div className="h-[60vh] flex flex-col items-center justify-center text-neutral-600">
|
|
38
|
+
<Hammer size={80} className="mb-6 opacity-20" />
|
|
39
|
+
<h2 className="text-3xl font-black uppercase">Máquinas Libres</h2>
|
|
40
|
+
<p className="mt-2">Esperando asignación desde Caja...</p>
|
|
41
|
+
</div>
|
|
42
|
+
) : (
|
|
43
|
+
<div className="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-3 gap-6">
|
|
44
|
+
{productionOrders.map(order => (
|
|
45
|
+
<div key={order.id} className="bg-neutral-800 border-l-8 border-orange-500 rounded-r-2xl p-6 relative overflow-hidden group">
|
|
46
|
+
<div className="absolute top-0 right-0 bg-neutral-700 px-4 py-2 rounded-bl-xl text-xs font-bold text-neutral-400">
|
|
47
|
+
MAQUINA: <span className="text-orange-400 text-base">{order.machine}</span>
|
|
48
|
+
</div>
|
|
49
|
+
|
|
50
|
+
<div className="mb-6 mt-2">
|
|
51
|
+
<h2 className="text-5xl font-black text-white mb-2">{order.id}</h2>
|
|
52
|
+
<p className="text-xl text-neutral-300 font-medium truncate">{order.material}</p>
|
|
53
|
+
<div className="mt-4 flex gap-2">
|
|
54
|
+
<span className="px-3 py-1 bg-neutral-700 rounded text-sm font-mono text-orange-200">
|
|
55
|
+
{order.sheets} LÁMINAS
|
|
56
|
+
</span>
|
|
57
|
+
<span className="px-3 py-1 bg-neutral-700 rounded text-sm font-mono text-orange-200">
|
|
58
|
+
{order.measurements}
|
|
59
|
+
</span>
|
|
60
|
+
</div>
|
|
61
|
+
</div>
|
|
62
|
+
|
|
63
|
+
<div className="grid grid-cols-2 gap-3">
|
|
64
|
+
<button className="bg-neutral-700 hover:bg-neutral-600 text-neutral-300 py-4 rounded-xl font-bold flex flex-col items-center justify-center gap-1">
|
|
65
|
+
<AlertTriangle size={24} />
|
|
66
|
+
<span className="text-xs">REPORTAR</span>
|
|
67
|
+
</button>
|
|
68
|
+
<button
|
|
69
|
+
onClick={() => handleFinish(order.id)}
|
|
70
|
+
className="bg-orange-600 hover:bg-orange-500 text-white py-4 rounded-xl font-black text-lg flex flex-col items-center justify-center gap-1 shadow-[0_0_20px_rgba(234,88,12,0.3)] active:scale-95 transition-all"
|
|
71
|
+
>
|
|
72
|
+
<CheckSquare size={28} />
|
|
73
|
+
TERMINADO
|
|
74
|
+
</button>
|
|
75
|
+
</div>
|
|
76
|
+
</div>
|
|
77
|
+
))}
|
|
78
|
+
</div>
|
|
79
|
+
)}
|
|
80
|
+
</main>
|
|
81
|
+
</div>
|
|
82
|
+
);
|
|
83
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
import { useState, useEffect } from 'react';
|
|
2
|
+
import { useOrderStore } from '../../stores/orderStore';
|
|
3
|
+
import { useAuthStore } from '../../stores/authStore';
|
|
4
|
+
import { useUIStore } from '../../stores/uiStore';
|
|
5
|
+
import { Plus, LogOut, Package, GitMerge, FileSpreadsheet } from 'lucide-react';
|
|
6
|
+
import { toast } from 'sonner';
|
|
7
|
+
import { MadefrontKanbanView } from '../MadefrontKanbanView';
|
|
8
|
+
|
|
9
|
+
import { MadefrontExcelView } from '../MadefrontExcelView';
|
|
10
|
+
|
|
11
|
+
export const VentasView = () => {
|
|
12
|
+
const { createOrder } = useOrderStore();
|
|
13
|
+
const { logout } = useAuthStore();
|
|
14
|
+
const { currentView, setCurrentView } = useUIStore();
|
|
15
|
+
const [form, setForm] = useState({ client: '', whatsapp: '', material: '', sheets: '1', measurements: 'Adjunto' });
|
|
16
|
+
|
|
17
|
+
// Al entrar por primera vez, forzamos la vista Kanban (si no estamos probando otra cosa)
|
|
18
|
+
useEffect(() => {
|
|
19
|
+
if (currentView !== 'madefront_kanban' && currentView !== 'madefront_excel') {
|
|
20
|
+
setCurrentView('madefront_kanban');
|
|
21
|
+
}
|
|
22
|
+
}, [currentView, setCurrentView]);
|
|
23
|
+
|
|
24
|
+
const handleSubmit = (e: React.FormEvent) => {
|
|
25
|
+
e.preventDefault();
|
|
26
|
+
createOrder(form);
|
|
27
|
+
|
|
28
|
+
// Toast Feedack visual
|
|
29
|
+
toast.success("Pedido Creado", {
|
|
30
|
+
description: `El pedido para ${form.client} fue enviado a Diseño/Caja.`,
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
setForm({ client: '', whatsapp: '', material: '', sheets: '1', measurements: 'Adjunto' });
|
|
34
|
+
};
|
|
35
|
+
|
|
36
|
+
return (
|
|
37
|
+
<div className="h-screen w-full bg-blue-50/50 font-sans text-slate-800 flex flex-col">
|
|
38
|
+
<header className="bg-white px-8 py-4 border-b border-blue-100 flex justify-between items-center z-10 shadow-sm shrink-0">
|
|
39
|
+
<h1 className="text-xl font-bold text-blue-900 flex items-center gap-2">
|
|
40
|
+
<Package className="text-blue-500" /> Gerencia & Ventas
|
|
41
|
+
</h1>
|
|
42
|
+
|
|
43
|
+
{/* BOTONERA CENTRAL: Kanban vs Digital Twin vs Excel */}
|
|
44
|
+
<div className="flex bg-slate-100 p-1 rounded-xl">
|
|
45
|
+
<button
|
|
46
|
+
onClick={() => setCurrentView('madefront_kanban')}
|
|
47
|
+
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all ${currentView === 'madefront_kanban' ? 'bg-white shadow-sm text-blue-600' : 'text-slate-500 hover:text-slate-700'}`}
|
|
48
|
+
>
|
|
49
|
+
Kanban Operativo
|
|
50
|
+
</button>
|
|
51
|
+
<button
|
|
52
|
+
onClick={() => setCurrentView('madefront_excel')}
|
|
53
|
+
className={`px-4 py-2 rounded-lg text-sm font-bold transition-all flex items-center gap-2 ${currentView === 'madefront_excel' ? 'bg-white shadow-sm text-green-600' : 'text-slate-500 hover:text-slate-700'}`}
|
|
54
|
+
>
|
|
55
|
+
<FileSpreadsheet size={16} /> Datos
|
|
56
|
+
</button>
|
|
57
|
+
</div>
|
|
58
|
+
|
|
59
|
+
<button onClick={logout} className="text-sm text-slate-500 hover:text-blue-600 flex items-center gap-2 bg-slate-100 px-4 py-2 rounded-lg">
|
|
60
|
+
<LogOut size={16} /> Salir
|
|
61
|
+
</button>
|
|
62
|
+
</header>
|
|
63
|
+
|
|
64
|
+
<main className="flex-1 overflow-hidden flex flex-col w-full relative">
|
|
65
|
+
{currentView === 'madefront_kanban' ? (
|
|
66
|
+
<div className="flex-1 flex flex-col p-4 md:p-6 overflow-hidden">
|
|
67
|
+
{/* Formulario rápido colapsable o en modal (simplificado para que quede espacio al Kanban) */}
|
|
68
|
+
<div className="mb-4 bg-white p-4 rounded-2xl shadow-sm border border-blue-100 flex gap-4 items-end shrink-0 overflow-x-auto">
|
|
69
|
+
<div className="flex-1 min-w-[200px]">
|
|
70
|
+
<label className="text-[10px] font-bold text-slate-400 uppercase">Cliente Nuevo</label>
|
|
71
|
+
<input className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm" value={form.client} onChange={e => setForm({ ...form, client: e.target.value })} placeholder="Ej: Arq. Juan..." />
|
|
72
|
+
</div>
|
|
73
|
+
<div className="w-40 shrink-0">
|
|
74
|
+
<label className="text-[10px] font-bold text-slate-400 uppercase">Teléfono</label>
|
|
75
|
+
<input className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm" value={form.whatsapp} onChange={e => setForm({ ...form, whatsapp: e.target.value })} placeholder="300..." />
|
|
76
|
+
</div>
|
|
77
|
+
<div className="w-24 shrink-0">
|
|
78
|
+
<label className="text-[10px] font-bold text-slate-400 uppercase">Láminas</label>
|
|
79
|
+
<input type="number" className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm" value={form.sheets} onChange={e => setForm({ ...form, sheets: e.target.value })} />
|
|
80
|
+
</div>
|
|
81
|
+
<div className="w-48 shrink-0">
|
|
82
|
+
<label className="text-[10px] font-bold text-slate-400 uppercase">Material</label>
|
|
83
|
+
<input className="w-full bg-slate-50 border border-slate-200 rounded-lg px-3 py-2 text-sm" value={form.material} onChange={e => setForm({ ...form, material: e.target.value })} placeholder="Ej: RH 15mm" />
|
|
84
|
+
</div>
|
|
85
|
+
<button onClick={handleSubmit} className="bg-blue-600 hover:bg-blue-700 text-white font-bold py-2 px-6 rounded-lg shadow-md transition-all flex items-center gap-2 shrink-0 h-[38px]">
|
|
86
|
+
<Plus size={16} /> Crear
|
|
87
|
+
</button>
|
|
88
|
+
</div>
|
|
89
|
+
|
|
90
|
+
{/* El Kanban Component */}
|
|
91
|
+
<div className="flex-1 min-h-0 border border-slate-200 rounded-2xl overflow-hidden shadow-xl bg-white flex flex-col">
|
|
92
|
+
<MadefrontKanbanView />
|
|
93
|
+
</div>
|
|
94
|
+
</div>
|
|
95
|
+
) : (
|
|
96
|
+
<MadefrontExcelView />
|
|
97
|
+
)}
|
|
98
|
+
</main>
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
};
|