@quantyapp/quanty-mcp-server 1.0.10 → 1.2.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/dist/bin/quanty-mcp-server.exe +0 -0
- package/dist/core/analysisService.d.ts +27 -0
- package/dist/core/analysisService.d.ts.map +1 -1
- package/dist/core/analysisService.js +84 -0
- package/dist/core/bankService.d.ts +26 -1
- package/dist/core/bankService.d.ts.map +1 -1
- package/dist/core/bankService.js +114 -5
- package/dist/core/budgetService.d.ts +190 -0
- package/dist/core/budgetService.d.ts.map +1 -1
- package/dist/core/budgetService.js +628 -4
- package/dist/core/learningService.d.ts +94 -0
- package/dist/core/learningService.d.ts.map +1 -0
- package/dist/core/learningService.js +462 -0
- package/dist/core/matchingService.d.ts +76 -0
- package/dist/core/matchingService.d.ts.map +1 -0
- package/dist/core/matchingService.js +437 -0
- package/dist/mcp/index.js +1180 -189
- package/package.json +10 -1
|
@@ -46,6 +46,7 @@ export async function getBudget(auth, budgetId) {
|
|
|
46
46
|
return {
|
|
47
47
|
id: data.id,
|
|
48
48
|
title: data.title,
|
|
49
|
+
baseDate: data.data?.baseDate,
|
|
49
50
|
createdAt: data.created_at,
|
|
50
51
|
lastModified: data.last_modified,
|
|
51
52
|
creator: data.creator,
|
|
@@ -99,7 +100,18 @@ export async function executeCreateBudget(auth, data) {
|
|
|
99
100
|
deleted: false,
|
|
100
101
|
data: {
|
|
101
102
|
title: data.title,
|
|
102
|
-
|
|
103
|
+
// Cria 50 linhas vazias
|
|
104
|
+
rows: Array.from({ length: 50 }, () => ({
|
|
105
|
+
id: crypto.randomUUID(),
|
|
106
|
+
level: 0,
|
|
107
|
+
code: '',
|
|
108
|
+
description: '',
|
|
109
|
+
unit: '',
|
|
110
|
+
quantity: 0,
|
|
111
|
+
unitCost: 0,
|
|
112
|
+
isComposition: false,
|
|
113
|
+
bdiType: 'Principal'
|
|
114
|
+
})),
|
|
103
115
|
bdi: 0,
|
|
104
116
|
bdiItems: [],
|
|
105
117
|
bdiPrincipal: 0,
|
|
@@ -129,6 +141,41 @@ export function prepareAddItem(budgetId, item) {
|
|
|
129
141
|
data: { budgetId, item }
|
|
130
142
|
};
|
|
131
143
|
}
|
|
144
|
+
/**
|
|
145
|
+
* Prepares adding a COMPOSITION (with inputs) to a budget
|
|
146
|
+
*/
|
|
147
|
+
export function prepareAddComposition(budgetId, composition) {
|
|
148
|
+
// Calcula o custo unitário da composição somando os insumos
|
|
149
|
+
const unitCost = composition.insumos.reduce((sum, insumo) => sum + (insumo.quantity * insumo.unitCost), 0);
|
|
150
|
+
const total = composition.quantity * unitCost;
|
|
151
|
+
// Transforma insumos no formato do BudgetRow.composition
|
|
152
|
+
const compositionItems = composition.insumos.map(insumo => ({
|
|
153
|
+
id: crypto.randomUUID(),
|
|
154
|
+
code: insumo.code || '',
|
|
155
|
+
description: insumo.description,
|
|
156
|
+
unit: insumo.unit,
|
|
157
|
+
quantity: insumo.quantity,
|
|
158
|
+
unitCost: insumo.unitCost,
|
|
159
|
+
itemType: insumo.itemType || 'INSUMO'
|
|
160
|
+
}));
|
|
161
|
+
return {
|
|
162
|
+
action: 'add_item',
|
|
163
|
+
preview: `Criar composição "${composition.description}" (${composition.quantity} ${composition.unit} x R$ ${unitCost.toFixed(2)} = R$ ${total.toFixed(2)}) com ${composition.insumos.length} insumo(s)`,
|
|
164
|
+
data: {
|
|
165
|
+
budgetId,
|
|
166
|
+
item: {
|
|
167
|
+
code: composition.code || '',
|
|
168
|
+
description: composition.description,
|
|
169
|
+
unit: composition.unit,
|
|
170
|
+
quantity: composition.quantity,
|
|
171
|
+
unitCost: unitCost,
|
|
172
|
+
level: composition.level ?? 0,
|
|
173
|
+
isComposition: true,
|
|
174
|
+
composition: compositionItems
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
};
|
|
178
|
+
}
|
|
132
179
|
/**
|
|
133
180
|
* Executes adding an item
|
|
134
181
|
* CORRIGIDO: Busca dados RAW e preserva estrutura original do campo 'data'
|
|
@@ -140,7 +187,7 @@ export async function executeAddItem(auth, data) {
|
|
|
140
187
|
throw new Error('Budget not found');
|
|
141
188
|
const newRow = {
|
|
142
189
|
id: crypto.randomUUID(),
|
|
143
|
-
level: 0,
|
|
190
|
+
level: data.item.level ?? 0,
|
|
144
191
|
code: data.item.code || '',
|
|
145
192
|
description: data.item.description,
|
|
146
193
|
unit: data.item.unit,
|
|
@@ -149,10 +196,18 @@ export async function executeAddItem(auth, data) {
|
|
|
149
196
|
isComposition: data.item.isComposition || false,
|
|
150
197
|
composition: data.item.composition || undefined
|
|
151
198
|
};
|
|
152
|
-
|
|
199
|
+
let rows = [...(rawData.rows || [])];
|
|
200
|
+
// Encontra primeira linha vazia e substitui
|
|
201
|
+
const emptyIndex = rows.findIndex((r) => !r.description && !r.code && (!r.quantity || r.quantity === 0));
|
|
202
|
+
if (emptyIndex >= 0) {
|
|
203
|
+
rows[emptyIndex] = newRow;
|
|
204
|
+
}
|
|
205
|
+
else {
|
|
206
|
+
rows.push(newRow);
|
|
207
|
+
}
|
|
153
208
|
const updatedData = {
|
|
154
209
|
...rawData,
|
|
155
|
-
rows
|
|
210
|
+
rows
|
|
156
211
|
};
|
|
157
212
|
const { error } = await supabase
|
|
158
213
|
.from('budgets')
|
|
@@ -247,3 +302,572 @@ export async function getCompositionDetails(auth, budgetId, itemId) {
|
|
|
247
302
|
is_composicao: !!(child.composition && child.composition.length > 0)
|
|
248
303
|
}));
|
|
249
304
|
}
|
|
305
|
+
// ================ BDI FUNCTIONS ================
|
|
306
|
+
/**
|
|
307
|
+
* Prepares updating BDI values for a budget
|
|
308
|
+
*/
|
|
309
|
+
export function prepareUpdateBdi(budgetId, bdiPrincipal, bdiDiferenciado) {
|
|
310
|
+
const changes = [];
|
|
311
|
+
if (bdiPrincipal !== undefined)
|
|
312
|
+
changes.push(`Principal: ${bdiPrincipal}%`);
|
|
313
|
+
if (bdiDiferenciado !== undefined)
|
|
314
|
+
changes.push(`Diferenciado: ${bdiDiferenciado}%`);
|
|
315
|
+
return {
|
|
316
|
+
action: 'update_bdi',
|
|
317
|
+
preview: `Ajustar BDI: ${changes.join(', ')}`,
|
|
318
|
+
data: { budgetId, bdiPrincipal, bdiDiferenciado }
|
|
319
|
+
};
|
|
320
|
+
}
|
|
321
|
+
/**
|
|
322
|
+
* Executes BDI update
|
|
323
|
+
*/
|
|
324
|
+
export async function executeUpdateBdi(auth, data) {
|
|
325
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
326
|
+
if (!rawData)
|
|
327
|
+
throw new Error('Budget not found');
|
|
328
|
+
const updatedData = {
|
|
329
|
+
...rawData,
|
|
330
|
+
bdiPrincipal: data.bdiPrincipal ?? rawData.bdiPrincipal ?? 0,
|
|
331
|
+
bdiDiferenciado: data.bdiDiferenciado ?? rawData.bdiDiferenciado ?? 0
|
|
332
|
+
};
|
|
333
|
+
const { error } = await supabase
|
|
334
|
+
.from('budgets')
|
|
335
|
+
.update({
|
|
336
|
+
data: updatedData,
|
|
337
|
+
last_modified: new Date().toISOString()
|
|
338
|
+
})
|
|
339
|
+
.eq('id', data.budgetId);
|
|
340
|
+
if (error)
|
|
341
|
+
throw new Error(`Failed to update BDI: ${error.message}`);
|
|
342
|
+
}
|
|
343
|
+
// ================ EDIT ITEM FUNCTIONS ================
|
|
344
|
+
/**
|
|
345
|
+
* Prepares editing an existing item
|
|
346
|
+
*/
|
|
347
|
+
export function prepareEditItem(budgetId, itemId, updates) {
|
|
348
|
+
const changes = [];
|
|
349
|
+
if (updates.description !== undefined)
|
|
350
|
+
changes.push(`descrição`);
|
|
351
|
+
if (updates.unit !== undefined)
|
|
352
|
+
changes.push(`unidade: ${updates.unit}`);
|
|
353
|
+
if (updates.quantity !== undefined)
|
|
354
|
+
changes.push(`qtd: ${updates.quantity}`);
|
|
355
|
+
if (updates.unitCost !== undefined)
|
|
356
|
+
changes.push(`custo: R$ ${updates.unitCost.toFixed(2)}`);
|
|
357
|
+
if (updates.level !== undefined)
|
|
358
|
+
changes.push(`nível: ${updates.level}`);
|
|
359
|
+
if (updates.code !== undefined)
|
|
360
|
+
changes.push(`código: ${updates.code}`);
|
|
361
|
+
return {
|
|
362
|
+
action: 'edit_item',
|
|
363
|
+
preview: `Editar item: ${changes.join(', ')}`,
|
|
364
|
+
data: { budgetId, itemId, updates }
|
|
365
|
+
};
|
|
366
|
+
}
|
|
367
|
+
/**
|
|
368
|
+
* Executes item edit
|
|
369
|
+
*/
|
|
370
|
+
export async function executeEditItem(auth, data) {
|
|
371
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
372
|
+
if (!rawData)
|
|
373
|
+
throw new Error('Budget not found');
|
|
374
|
+
const rows = [...(rawData.rows || [])];
|
|
375
|
+
const itemIndex = rows.findIndex((r) => r.id === data.itemId);
|
|
376
|
+
if (itemIndex === -1)
|
|
377
|
+
throw new Error('Item not found in budget');
|
|
378
|
+
// Apply updates
|
|
379
|
+
const item = rows[itemIndex];
|
|
380
|
+
const updates = data.updates;
|
|
381
|
+
if (updates.description !== undefined)
|
|
382
|
+
item.description = updates.description;
|
|
383
|
+
if (updates.unit !== undefined)
|
|
384
|
+
item.unit = updates.unit;
|
|
385
|
+
if (updates.quantity !== undefined)
|
|
386
|
+
item.quantity = updates.quantity;
|
|
387
|
+
if (updates.unitCost !== undefined)
|
|
388
|
+
item.unitCost = updates.unitCost;
|
|
389
|
+
if (updates.level !== undefined)
|
|
390
|
+
item.level = updates.level;
|
|
391
|
+
if (updates.code !== undefined)
|
|
392
|
+
item.code = updates.code;
|
|
393
|
+
rows[itemIndex] = item;
|
|
394
|
+
const updatedData = {
|
|
395
|
+
...rawData,
|
|
396
|
+
rows
|
|
397
|
+
};
|
|
398
|
+
const { error } = await supabase
|
|
399
|
+
.from('budgets')
|
|
400
|
+
.update({
|
|
401
|
+
data: updatedData,
|
|
402
|
+
last_modified: new Date().toISOString()
|
|
403
|
+
})
|
|
404
|
+
.eq('id', data.budgetId);
|
|
405
|
+
if (error)
|
|
406
|
+
throw new Error(`Failed to edit item: ${error.message}`);
|
|
407
|
+
}
|
|
408
|
+
// ================ DUPLICATE BUDGET FUNCTIONS ================
|
|
409
|
+
/**
|
|
410
|
+
* Prepares duplicating a budget
|
|
411
|
+
*/
|
|
412
|
+
export function prepareDuplicateBudget(auth, budgetId, newTitle) {
|
|
413
|
+
return {
|
|
414
|
+
action: 'duplicate_budget',
|
|
415
|
+
preview: `Duplicar orçamento como "${newTitle}"`,
|
|
416
|
+
data: { budgetId, newTitle, tenantId: auth.tenantId, userId: auth.userId, creator: auth.userName }
|
|
417
|
+
};
|
|
418
|
+
}
|
|
419
|
+
/**
|
|
420
|
+
* Executes budget duplication
|
|
421
|
+
*/
|
|
422
|
+
export async function executeDuplicateBudget(auth, data) {
|
|
423
|
+
// Get original budget
|
|
424
|
+
const { data: original, error: fetchError } = await supabase
|
|
425
|
+
.from('budgets')
|
|
426
|
+
.select('*')
|
|
427
|
+
.eq('id', data.budgetId)
|
|
428
|
+
.single();
|
|
429
|
+
if (fetchError || !original)
|
|
430
|
+
throw new Error('Original budget not found');
|
|
431
|
+
const now = new Date().toISOString();
|
|
432
|
+
const newBudget = {
|
|
433
|
+
title: data.newTitle,
|
|
434
|
+
tenant_id: data.tenantId,
|
|
435
|
+
user_id: data.userId,
|
|
436
|
+
creator: data.creator,
|
|
437
|
+
created_at: now,
|
|
438
|
+
last_modified: now,
|
|
439
|
+
shared: false,
|
|
440
|
+
deleted: false,
|
|
441
|
+
data: {
|
|
442
|
+
...original.data,
|
|
443
|
+
title: data.newTitle
|
|
444
|
+
}
|
|
445
|
+
};
|
|
446
|
+
const { data: result, error } = await supabase
|
|
447
|
+
.from('budgets')
|
|
448
|
+
.insert(newBudget)
|
|
449
|
+
.select('id')
|
|
450
|
+
.single();
|
|
451
|
+
if (error)
|
|
452
|
+
throw new Error(`Failed to duplicate budget: ${error.message}`);
|
|
453
|
+
return result.id;
|
|
454
|
+
}
|
|
455
|
+
// ================ MOVE ITEM (CHANGE LEVEL) ================
|
|
456
|
+
/**
|
|
457
|
+
* Prepares moving an item to a different level
|
|
458
|
+
*/
|
|
459
|
+
export function prepareMoveItem(budgetId, itemId, newLevel, itemDescription) {
|
|
460
|
+
return {
|
|
461
|
+
action: 'edit_item',
|
|
462
|
+
preview: `Mover "${itemDescription}" para nível ${newLevel}`,
|
|
463
|
+
data: {
|
|
464
|
+
budgetId,
|
|
465
|
+
itemId,
|
|
466
|
+
updates: { level: newLevel }
|
|
467
|
+
}
|
|
468
|
+
};
|
|
469
|
+
}
|
|
470
|
+
// ================ EDIT BUDGET (Title, BDI, Data Base) ================
|
|
471
|
+
/**
|
|
472
|
+
* Prepares editing budget metadata
|
|
473
|
+
*/
|
|
474
|
+
export function prepareEditBudget(budgetId, updates) {
|
|
475
|
+
const changes = [];
|
|
476
|
+
if (updates.title !== undefined)
|
|
477
|
+
changes.push(`título: "${updates.title}"`);
|
|
478
|
+
if (updates.baseDate !== undefined)
|
|
479
|
+
changes.push(`data base: ${updates.baseDate}`);
|
|
480
|
+
if (updates.bdiPrincipal !== undefined)
|
|
481
|
+
changes.push(`BDI Principal: ${updates.bdiPrincipal}%`);
|
|
482
|
+
if (updates.bdiDiferenciado !== undefined)
|
|
483
|
+
changes.push(`BDI Diferenciado: ${updates.bdiDiferenciado}%`);
|
|
484
|
+
return {
|
|
485
|
+
action: 'edit_budget',
|
|
486
|
+
preview: `Editar orçamento: ${changes.join(', ')}`,
|
|
487
|
+
data: { budgetId, updates }
|
|
488
|
+
};
|
|
489
|
+
}
|
|
490
|
+
/**
|
|
491
|
+
* Executes budget metadata editing
|
|
492
|
+
*/
|
|
493
|
+
export async function executeEditBudget(auth, data) {
|
|
494
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
495
|
+
if (!rawData)
|
|
496
|
+
throw new Error('Budget not found');
|
|
497
|
+
const updates = data.updates;
|
|
498
|
+
const updatePayload = {
|
|
499
|
+
last_modified: new Date().toISOString()
|
|
500
|
+
};
|
|
501
|
+
// Handle title (it's a column, not in data JSONB)
|
|
502
|
+
if (updates.title !== undefined) {
|
|
503
|
+
updatePayload.title = updates.title;
|
|
504
|
+
}
|
|
505
|
+
// Handle data JSONB fields
|
|
506
|
+
const newData = { ...rawData };
|
|
507
|
+
if (updates.baseDate !== undefined)
|
|
508
|
+
newData.baseDate = updates.baseDate;
|
|
509
|
+
if (updates.bdiPrincipal !== undefined)
|
|
510
|
+
newData.bdiPrincipal = updates.bdiPrincipal;
|
|
511
|
+
if (updates.bdiDiferenciado !== undefined)
|
|
512
|
+
newData.bdiDiferenciado = updates.bdiDiferenciado;
|
|
513
|
+
updatePayload.data = newData;
|
|
514
|
+
const { error } = await supabase
|
|
515
|
+
.from('budgets')
|
|
516
|
+
.update(updatePayload)
|
|
517
|
+
.eq('id', data.budgetId);
|
|
518
|
+
if (error)
|
|
519
|
+
throw new Error(`Failed to edit budget: ${error.message}`);
|
|
520
|
+
}
|
|
521
|
+
// ================ SET ITEM BDI TYPE ================
|
|
522
|
+
/**
|
|
523
|
+
* Prepares setting BDI type for an item
|
|
524
|
+
*/
|
|
525
|
+
export function prepareSetBdiType(budgetId, itemId, bdiType, itemDescription) {
|
|
526
|
+
return {
|
|
527
|
+
action: 'edit_item',
|
|
528
|
+
preview: `Definir BDI ${bdiType} para "${itemDescription}"`,
|
|
529
|
+
data: {
|
|
530
|
+
budgetId,
|
|
531
|
+
itemId,
|
|
532
|
+
updates: { bdiType }
|
|
533
|
+
}
|
|
534
|
+
};
|
|
535
|
+
}
|
|
536
|
+
// ================ SET ABC CLASSIFICATION ================
|
|
537
|
+
/**
|
|
538
|
+
* Prepares ABC classification for an item
|
|
539
|
+
*/
|
|
540
|
+
export function prepareSetAbcClassification(budgetId, itemId, classification, itemDescription) {
|
|
541
|
+
return {
|
|
542
|
+
action: 'set_abc_classification',
|
|
543
|
+
preview: `Classificar "${itemDescription}" como ${classification}`,
|
|
544
|
+
data: { budgetId, itemId, classification }
|
|
545
|
+
};
|
|
546
|
+
}
|
|
547
|
+
/**
|
|
548
|
+
* Executes ABC classification setting
|
|
549
|
+
*/
|
|
550
|
+
export async function executeSetAbcClassification(auth, data) {
|
|
551
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
552
|
+
if (!rawData)
|
|
553
|
+
throw new Error('Budget not found');
|
|
554
|
+
const abcClassifications = rawData.abcClassifications || {};
|
|
555
|
+
abcClassifications[data.itemId] = data.classification;
|
|
556
|
+
const updatedData = {
|
|
557
|
+
...rawData,
|
|
558
|
+
abcClassifications
|
|
559
|
+
};
|
|
560
|
+
const { error } = await supabase
|
|
561
|
+
.from('budgets')
|
|
562
|
+
.update({
|
|
563
|
+
data: updatedData,
|
|
564
|
+
last_modified: new Date().toISOString()
|
|
565
|
+
})
|
|
566
|
+
.eq('id', data.budgetId);
|
|
567
|
+
if (error)
|
|
568
|
+
throw new Error(`Failed to set ABC classification: ${error.message}`);
|
|
569
|
+
}
|
|
570
|
+
// ================ EDIT COMPOSITION INPUTS ================
|
|
571
|
+
/**
|
|
572
|
+
* Prepares editing inputs of a composition
|
|
573
|
+
*/
|
|
574
|
+
export function prepareEditCompositionInput(budgetId, compositionId, inputId, updates, inputDescription) {
|
|
575
|
+
const changes = [];
|
|
576
|
+
if (updates.description !== undefined)
|
|
577
|
+
changes.push(`descrição`);
|
|
578
|
+
if (updates.quantity !== undefined)
|
|
579
|
+
changes.push(`qtd: ${updates.quantity}`);
|
|
580
|
+
if (updates.unitCost !== undefined)
|
|
581
|
+
changes.push(`custo: R$ ${updates.unitCost.toFixed(2)}`);
|
|
582
|
+
if (updates.unit !== undefined)
|
|
583
|
+
changes.push(`unidade: ${updates.unit}`);
|
|
584
|
+
return {
|
|
585
|
+
action: 'edit_composition_input',
|
|
586
|
+
preview: `Editar insumo "${inputDescription}": ${changes.join(', ')}`,
|
|
587
|
+
data: { budgetId, compositionId, inputId, updates }
|
|
588
|
+
};
|
|
589
|
+
}
|
|
590
|
+
/**
|
|
591
|
+
* Executes editing composition input
|
|
592
|
+
*/
|
|
593
|
+
export async function executeEditCompositionInput(auth, data) {
|
|
594
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
595
|
+
if (!rawData)
|
|
596
|
+
throw new Error('Budget not found');
|
|
597
|
+
const rows = [...(rawData.rows || [])];
|
|
598
|
+
// Find the composition
|
|
599
|
+
const findAndUpdateInput = (items) => {
|
|
600
|
+
for (let i = 0; i < items.length; i++) {
|
|
601
|
+
const item = items[i];
|
|
602
|
+
if (item.id === data.compositionId && item.composition) {
|
|
603
|
+
// Found the composition, now find and update the input
|
|
604
|
+
const inputIndex = item.composition.findIndex((inp) => inp.id === data.inputId);
|
|
605
|
+
if (inputIndex !== -1) {
|
|
606
|
+
const input = item.composition[inputIndex];
|
|
607
|
+
const updates = data.updates;
|
|
608
|
+
if (updates.code !== undefined)
|
|
609
|
+
input.code = updates.code;
|
|
610
|
+
if (updates.description !== undefined)
|
|
611
|
+
input.description = updates.description;
|
|
612
|
+
if (updates.unit !== undefined)
|
|
613
|
+
input.unit = updates.unit;
|
|
614
|
+
if (updates.quantity !== undefined)
|
|
615
|
+
input.quantity = updates.quantity;
|
|
616
|
+
if (updates.unitCost !== undefined)
|
|
617
|
+
input.unitCost = updates.unitCost;
|
|
618
|
+
if (updates.itemType !== undefined)
|
|
619
|
+
input.itemType = updates.itemType;
|
|
620
|
+
item.composition[inputIndex] = input;
|
|
621
|
+
// Recalculate composition unitCost
|
|
622
|
+
item.unitCost = item.composition.reduce((sum, inp) => sum + (inp.quantity || 0) * (inp.unitCost || 0), 0);
|
|
623
|
+
items[i] = item;
|
|
624
|
+
return true;
|
|
625
|
+
}
|
|
626
|
+
}
|
|
627
|
+
// Recursively check nested compositions
|
|
628
|
+
if (item.composition && findAndUpdateInput(item.composition)) {
|
|
629
|
+
return true;
|
|
630
|
+
}
|
|
631
|
+
}
|
|
632
|
+
return false;
|
|
633
|
+
};
|
|
634
|
+
if (!findAndUpdateInput(rows)) {
|
|
635
|
+
throw new Error('Input not found in composition');
|
|
636
|
+
}
|
|
637
|
+
const updatedData = { ...rawData, rows };
|
|
638
|
+
const { error } = await supabase
|
|
639
|
+
.from('budgets')
|
|
640
|
+
.update({
|
|
641
|
+
data: updatedData,
|
|
642
|
+
last_modified: new Date().toISOString()
|
|
643
|
+
})
|
|
644
|
+
.eq('id', data.budgetId);
|
|
645
|
+
if (error)
|
|
646
|
+
throw new Error(`Failed to edit composition input: ${error.message}`);
|
|
647
|
+
}
|
|
648
|
+
/**
|
|
649
|
+
* Prepares adding an input to a composition
|
|
650
|
+
*/
|
|
651
|
+
export function prepareAddCompositionInput(budgetId, compositionId, input) {
|
|
652
|
+
const total = input.quantity * input.unitCost;
|
|
653
|
+
return {
|
|
654
|
+
action: 'add_composition_input',
|
|
655
|
+
preview: `Adicionar insumo "${input.description}" (${input.quantity} ${input.unit} x R$ ${input.unitCost.toFixed(2)} = R$ ${total.toFixed(2)})`,
|
|
656
|
+
data: { budgetId, compositionId, input }
|
|
657
|
+
};
|
|
658
|
+
}
|
|
659
|
+
/**
|
|
660
|
+
* Executes adding input to composition
|
|
661
|
+
*/
|
|
662
|
+
export async function executeAddCompositionInput(auth, data) {
|
|
663
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
664
|
+
if (!rawData)
|
|
665
|
+
throw new Error('Budget not found');
|
|
666
|
+
const rows = [...(rawData.rows || [])];
|
|
667
|
+
const findAndAddInput = (items) => {
|
|
668
|
+
for (let i = 0; i < items.length; i++) {
|
|
669
|
+
const item = items[i];
|
|
670
|
+
if (item.id === data.compositionId) {
|
|
671
|
+
if (!item.composition)
|
|
672
|
+
item.composition = [];
|
|
673
|
+
const newInput = {
|
|
674
|
+
id: crypto.randomUUID(),
|
|
675
|
+
code: data.input.code || '',
|
|
676
|
+
description: data.input.description,
|
|
677
|
+
unit: data.input.unit,
|
|
678
|
+
quantity: data.input.quantity,
|
|
679
|
+
unitCost: data.input.unitCost,
|
|
680
|
+
itemType: data.input.itemType || 'INSUMO'
|
|
681
|
+
};
|
|
682
|
+
item.composition.push(newInput);
|
|
683
|
+
// Recalculate unitCost
|
|
684
|
+
item.unitCost = item.composition.reduce((sum, inp) => sum + (inp.quantity || 0) * (inp.unitCost || 0), 0);
|
|
685
|
+
item.isComposition = true;
|
|
686
|
+
items[i] = item;
|
|
687
|
+
return true;
|
|
688
|
+
}
|
|
689
|
+
if (item.composition && findAndAddInput(item.composition)) {
|
|
690
|
+
return true;
|
|
691
|
+
}
|
|
692
|
+
}
|
|
693
|
+
return false;
|
|
694
|
+
};
|
|
695
|
+
if (!findAndAddInput(rows)) {
|
|
696
|
+
throw new Error('Composition not found');
|
|
697
|
+
}
|
|
698
|
+
const updatedData = { ...rawData, rows };
|
|
699
|
+
const { error } = await supabase
|
|
700
|
+
.from('budgets')
|
|
701
|
+
.update({
|
|
702
|
+
data: updatedData,
|
|
703
|
+
last_modified: new Date().toISOString()
|
|
704
|
+
})
|
|
705
|
+
.eq('id', data.budgetId);
|
|
706
|
+
if (error)
|
|
707
|
+
throw new Error(`Failed to add composition input: ${error.message}`);
|
|
708
|
+
}
|
|
709
|
+
/**
|
|
710
|
+
* Prepares removing an input from a composition
|
|
711
|
+
*/
|
|
712
|
+
export function prepareRemoveCompositionInput(budgetId, compositionId, inputId, inputDescription) {
|
|
713
|
+
return {
|
|
714
|
+
action: 'remove_composition_input',
|
|
715
|
+
preview: `Remover insumo "${inputDescription}" da composição`,
|
|
716
|
+
data: { budgetId, compositionId, inputId }
|
|
717
|
+
};
|
|
718
|
+
}
|
|
719
|
+
/**
|
|
720
|
+
* Executes removing input from composition
|
|
721
|
+
*/
|
|
722
|
+
export async function executeRemoveCompositionInput(auth, data) {
|
|
723
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
724
|
+
if (!rawData)
|
|
725
|
+
throw new Error('Budget not found');
|
|
726
|
+
const rows = [...(rawData.rows || [])];
|
|
727
|
+
const findAndRemoveInput = (items) => {
|
|
728
|
+
for (let i = 0; i < items.length; i++) {
|
|
729
|
+
const item = items[i];
|
|
730
|
+
if (item.id === data.compositionId && item.composition) {
|
|
731
|
+
const inputIndex = item.composition.findIndex((inp) => inp.id === data.inputId);
|
|
732
|
+
if (inputIndex !== -1) {
|
|
733
|
+
item.composition.splice(inputIndex, 1);
|
|
734
|
+
// Recalculate unitCost
|
|
735
|
+
item.unitCost = item.composition.reduce((sum, inp) => sum + (inp.quantity || 0) * (inp.unitCost || 0), 0);
|
|
736
|
+
items[i] = item;
|
|
737
|
+
return true;
|
|
738
|
+
}
|
|
739
|
+
}
|
|
740
|
+
if (item.composition && findAndRemoveInput(item.composition)) {
|
|
741
|
+
return true;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
return false;
|
|
745
|
+
};
|
|
746
|
+
if (!findAndRemoveInput(rows)) {
|
|
747
|
+
throw new Error('Input not found in composition');
|
|
748
|
+
}
|
|
749
|
+
const updatedData = { ...rawData, rows };
|
|
750
|
+
const { error } = await supabase
|
|
751
|
+
.from('budgets')
|
|
752
|
+
.update({
|
|
753
|
+
data: updatedData,
|
|
754
|
+
last_modified: new Date().toISOString()
|
|
755
|
+
})
|
|
756
|
+
.eq('id', data.budgetId);
|
|
757
|
+
if (error)
|
|
758
|
+
throw new Error(`Failed to remove composition input: ${error.message}`);
|
|
759
|
+
}
|
|
760
|
+
// ================ CONVERT COST TO COMPOSITION ================
|
|
761
|
+
/**
|
|
762
|
+
* Prepares converting a simple cost item to a composition
|
|
763
|
+
*/
|
|
764
|
+
export function prepareConvertToComposition(budgetId, itemId, itemDescription) {
|
|
765
|
+
return {
|
|
766
|
+
action: 'convert_to_composition',
|
|
767
|
+
preview: `Converter "${itemDescription}" para composição`,
|
|
768
|
+
data: { budgetId, itemId }
|
|
769
|
+
};
|
|
770
|
+
}
|
|
771
|
+
/**
|
|
772
|
+
* Executes converting a cost item to a composition
|
|
773
|
+
*/
|
|
774
|
+
export async function executeConvertToComposition(auth, data) {
|
|
775
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
776
|
+
if (!rawData)
|
|
777
|
+
throw new Error('Budget not found');
|
|
778
|
+
const rows = [...(rawData.rows || [])];
|
|
779
|
+
const findAndConvert = (items) => {
|
|
780
|
+
for (let i = 0; i < items.length; i++) {
|
|
781
|
+
const item = items[i];
|
|
782
|
+
if (item.id === data.itemId) {
|
|
783
|
+
// Convert to composition: set isComposition flag and initialize empty composition array
|
|
784
|
+
item.isComposition = true;
|
|
785
|
+
if (!item.composition) {
|
|
786
|
+
// Create an initial input based on the item's current cost
|
|
787
|
+
if (item.unitCost > 0) {
|
|
788
|
+
item.composition = [{
|
|
789
|
+
id: crypto.randomUUID(),
|
|
790
|
+
code: '',
|
|
791
|
+
description: 'Custo base',
|
|
792
|
+
unit: item.unit || 'un',
|
|
793
|
+
quantity: 1,
|
|
794
|
+
unitCost: item.unitCost,
|
|
795
|
+
itemType: 'INSUMO'
|
|
796
|
+
}];
|
|
797
|
+
}
|
|
798
|
+
else {
|
|
799
|
+
item.composition = [];
|
|
800
|
+
}
|
|
801
|
+
}
|
|
802
|
+
items[i] = item;
|
|
803
|
+
return true;
|
|
804
|
+
}
|
|
805
|
+
if (item.composition && findAndConvert(item.composition)) {
|
|
806
|
+
return true;
|
|
807
|
+
}
|
|
808
|
+
}
|
|
809
|
+
return false;
|
|
810
|
+
};
|
|
811
|
+
if (!findAndConvert(rows)) {
|
|
812
|
+
throw new Error('Item not found');
|
|
813
|
+
}
|
|
814
|
+
const updatedData = { ...rawData, rows };
|
|
815
|
+
const { error } = await supabase
|
|
816
|
+
.from('budgets')
|
|
817
|
+
.update({
|
|
818
|
+
data: updatedData,
|
|
819
|
+
last_modified: new Date().toISOString()
|
|
820
|
+
})
|
|
821
|
+
.eq('id', data.budgetId);
|
|
822
|
+
if (error)
|
|
823
|
+
throw new Error(`Failed to convert to composition: ${error.message}`);
|
|
824
|
+
}
|
|
825
|
+
// ================ CONVERT COMPOSITION TO COST ================
|
|
826
|
+
/**
|
|
827
|
+
* Prepares converting a composition back to simple cost
|
|
828
|
+
*/
|
|
829
|
+
export function prepareConvertToCost(budgetId, itemId, itemDescription) {
|
|
830
|
+
return {
|
|
831
|
+
action: 'convert_to_cost',
|
|
832
|
+
preview: `Converter "${itemDescription}" para custo simples (remove insumos)`,
|
|
833
|
+
data: { budgetId, itemId }
|
|
834
|
+
};
|
|
835
|
+
}
|
|
836
|
+
/**
|
|
837
|
+
* Executes converting a composition back to cost
|
|
838
|
+
*/
|
|
839
|
+
export async function executeConvertToCost(auth, data) {
|
|
840
|
+
const rawData = await getRawBudgetData(data.budgetId);
|
|
841
|
+
if (!rawData)
|
|
842
|
+
throw new Error('Budget not found');
|
|
843
|
+
const rows = [...(rawData.rows || [])];
|
|
844
|
+
const findAndConvert = (items) => {
|
|
845
|
+
for (let i = 0; i < items.length; i++) {
|
|
846
|
+
const item = items[i];
|
|
847
|
+
if (item.id === data.itemId) {
|
|
848
|
+
// Convert to cost: remove composition flag and array, keep current unitCost
|
|
849
|
+
item.isComposition = false;
|
|
850
|
+
delete item.composition;
|
|
851
|
+
items[i] = item;
|
|
852
|
+
return true;
|
|
853
|
+
}
|
|
854
|
+
if (item.composition && findAndConvert(item.composition)) {
|
|
855
|
+
return true;
|
|
856
|
+
}
|
|
857
|
+
}
|
|
858
|
+
return false;
|
|
859
|
+
};
|
|
860
|
+
if (!findAndConvert(rows)) {
|
|
861
|
+
throw new Error('Item not found');
|
|
862
|
+
}
|
|
863
|
+
const updatedData = { ...rawData, rows };
|
|
864
|
+
const { error } = await supabase
|
|
865
|
+
.from('budgets')
|
|
866
|
+
.update({
|
|
867
|
+
data: updatedData,
|
|
868
|
+
last_modified: new Date().toISOString()
|
|
869
|
+
})
|
|
870
|
+
.eq('id', data.budgetId);
|
|
871
|
+
if (error)
|
|
872
|
+
throw new Error(`Failed to convert to cost: ${error.message}`);
|
|
873
|
+
}
|