@quantyapp/quanty-mcp-server 1.0.11 → 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 +111 -2
- package/dist/core/budgetService.d.ts +190 -0
- package/dist/core/budgetService.d.ts.map +1 -1
- package/dist/core/budgetService.js +606 -1
- 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,
|
|
@@ -140,6 +141,41 @@ export function prepareAddItem(budgetId, item) {
|
|
|
140
141
|
data: { budgetId, item }
|
|
141
142
|
};
|
|
142
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
|
+
}
|
|
143
179
|
/**
|
|
144
180
|
* Executes adding an item
|
|
145
181
|
* CORRIGIDO: Busca dados RAW e preserva estrutura original do campo 'data'
|
|
@@ -151,7 +187,7 @@ export async function executeAddItem(auth, data) {
|
|
|
151
187
|
throw new Error('Budget not found');
|
|
152
188
|
const newRow = {
|
|
153
189
|
id: crypto.randomUUID(),
|
|
154
|
-
level: 0,
|
|
190
|
+
level: data.item.level ?? 0,
|
|
155
191
|
code: data.item.code || '',
|
|
156
192
|
description: data.item.description,
|
|
157
193
|
unit: data.item.unit,
|
|
@@ -266,3 +302,572 @@ export async function getCompositionDetails(auth, budgetId, itemId) {
|
|
|
266
302
|
is_composicao: !!(child.composition && child.composition.length > 0)
|
|
267
303
|
}));
|
|
268
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
|
+
}
|