@quantyapp/quanty-mcp-server 1.0.11 → 1.2.6

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.
@@ -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,
@@ -53,7 +54,9 @@ export async function getBudget(auth, budgetId) {
53
54
  projectId: data.project_id,
54
55
  rows: data.data?.rows || [],
55
56
  bdiPrincipal: data.data?.bdiPrincipal || 0,
56
- bdiDiferenciado: data.data?.bdiDiferenciado || 0
57
+ bdiDiferenciado: data.data?.bdiDiferenciado || 0,
58
+ bdiItemsPrincipal: data.data?.bdiItemsPrincipal || [],
59
+ bdiItemsDiferenciado: data.data?.bdiItemsDiferenciado || []
57
60
  };
58
61
  }
59
62
  /**
@@ -88,6 +91,9 @@ export function prepareCreateBudget(auth, title) {
88
91
  */
89
92
  export async function executeCreateBudget(auth, data) {
90
93
  const now = new Date().toISOString();
94
+ // Gera a data base padrão no formato MM/AAAA
95
+ const nowDate = new Date();
96
+ const defaultBaseDate = `${String(nowDate.getMonth() + 1).padStart(2, '0')}/${nowDate.getFullYear()}`;
91
97
  const budgetData = {
92
98
  title: data.title,
93
99
  tenant_id: data.tenantId,
@@ -99,6 +105,7 @@ export async function executeCreateBudget(auth, data) {
99
105
  deleted: false,
100
106
  data: {
101
107
  title: data.title,
108
+ baseDate: defaultBaseDate, // Data Base padrão = data de criação
102
109
  // Cria 50 linhas vazias
103
110
  rows: Array.from({ length: 50 }, () => ({
104
111
  id: crypto.randomUUID(),
@@ -140,6 +147,41 @@ export function prepareAddItem(budgetId, item) {
140
147
  data: { budgetId, item }
141
148
  };
142
149
  }
150
+ /**
151
+ * Prepares adding a COMPOSITION (with inputs) to a budget
152
+ */
153
+ export function prepareAddComposition(budgetId, composition) {
154
+ // Calcula o custo unitário da composição somando os insumos
155
+ const unitCost = composition.insumos.reduce((sum, insumo) => sum + (insumo.quantity * insumo.unitCost), 0);
156
+ const total = composition.quantity * unitCost;
157
+ // Transforma insumos no formato do BudgetRow.composition
158
+ const compositionItems = composition.insumos.map(insumo => ({
159
+ id: crypto.randomUUID(),
160
+ code: insumo.code || '',
161
+ description: insumo.description,
162
+ unit: insumo.unit,
163
+ quantity: insumo.quantity,
164
+ unitCost: insumo.unitCost,
165
+ itemType: insumo.itemType || 'INSUMO'
166
+ }));
167
+ return {
168
+ action: 'add_item',
169
+ 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)`,
170
+ data: {
171
+ budgetId,
172
+ item: {
173
+ code: composition.code || '',
174
+ description: composition.description,
175
+ unit: composition.unit,
176
+ quantity: composition.quantity,
177
+ unitCost: unitCost,
178
+ level: composition.level ?? 0,
179
+ isComposition: true,
180
+ composition: compositionItems
181
+ }
182
+ }
183
+ };
184
+ }
143
185
  /**
144
186
  * Executes adding an item
145
187
  * CORRIGIDO: Busca dados RAW e preserva estrutura original do campo 'data'
@@ -151,7 +193,7 @@ export async function executeAddItem(auth, data) {
151
193
  throw new Error('Budget not found');
152
194
  const newRow = {
153
195
  id: crypto.randomUUID(),
154
- level: 0,
196
+ level: data.item.level ?? 0,
155
197
  code: data.item.code || '',
156
198
  description: data.item.description,
157
199
  unit: data.item.unit,
@@ -266,3 +308,979 @@ export async function getCompositionDetails(auth, budgetId, itemId) {
266
308
  is_composicao: !!(child.composition && child.composition.length > 0)
267
309
  }));
268
310
  }
311
+ // ================ BDI FUNCTIONS ================
312
+ /**
313
+ * Prepares updating BDI values for a budget
314
+ */
315
+ export function prepareUpdateBdi(budgetId, bdiPrincipal, bdiDiferenciado) {
316
+ const changes = [];
317
+ if (bdiPrincipal !== undefined)
318
+ changes.push(`Principal: ${bdiPrincipal}%`);
319
+ if (bdiDiferenciado !== undefined)
320
+ changes.push(`Diferenciado: ${bdiDiferenciado}%`);
321
+ return {
322
+ action: 'update_bdi',
323
+ preview: `Ajustar BDI: ${changes.join(', ')}`,
324
+ data: { budgetId, bdiPrincipal, bdiDiferenciado }
325
+ };
326
+ }
327
+ /**
328
+ * Executes BDI update
329
+ */
330
+ export async function executeUpdateBdi(auth, data) {
331
+ const rawData = await getRawBudgetData(data.budgetId);
332
+ if (!rawData)
333
+ throw new Error('Budget not found');
334
+ const updatedData = {
335
+ ...rawData,
336
+ bdiPrincipal: data.bdiPrincipal ?? rawData.bdiPrincipal ?? 0,
337
+ bdiDiferenciado: data.bdiDiferenciado ?? rawData.bdiDiferenciado ?? 0
338
+ };
339
+ const { error } = await supabase
340
+ .from('budgets')
341
+ .update({
342
+ data: updatedData,
343
+ last_modified: new Date().toISOString()
344
+ })
345
+ .eq('id', data.budgetId);
346
+ if (error)
347
+ throw new Error(`Failed to update BDI: ${error.message}`);
348
+ }
349
+ /**
350
+ * Calcula o BDI total baseado nos itens de composição
351
+ * Fórmula: BDI = ((1 + i_custo) / (1 - i_venda) - 1) * 100
352
+ *
353
+ * Onde:
354
+ * - i_custo = soma dos % que incidem sobre o Custo (ex: Administração, Risco)
355
+ * - i_venda = soma dos % que incidem sobre a Venda (ex: ISS, PIS, COFINS, Lucro)
356
+ */
357
+ export function calculateBdiTotal(items) {
358
+ if (!items || items.length === 0)
359
+ return 0;
360
+ let i_custo = 0;
361
+ let i_venda = 0;
362
+ items.forEach(item => {
363
+ const val = item.percentage / 100;
364
+ if (item.incidence === 'Custo') {
365
+ i_custo += val;
366
+ }
367
+ else {
368
+ // 'Venda' ou qualquer outro valor incide sobre venda
369
+ i_venda += val;
370
+ }
371
+ });
372
+ // Safety check to prevent division by zero or negative results
373
+ if (i_venda >= 1) {
374
+ return 0;
375
+ }
376
+ const bdi = (((1 + i_custo) / (1 - i_venda)) - 1) * 100;
377
+ return bdi;
378
+ }
379
+ /**
380
+ * Prepares adding an item to BDI composition
381
+ */
382
+ export function prepareAddBdiItem(budgetId, bdiType, item) {
383
+ return {
384
+ action: 'add_bdi_item',
385
+ preview: `Adicionar item ao BDI ${bdiType}: "${item.description}" (${item.percentage}%)`,
386
+ data: { budgetId, bdiType, item }
387
+ };
388
+ }
389
+ /**
390
+ * Executes adding item to BDI composition
391
+ * ATUALIZADO: Recalcula automaticamente o BDI total
392
+ */
393
+ export async function executeAddBdiItem(auth, data) {
394
+ const rawData = await getRawBudgetData(data.budgetId);
395
+ if (!rawData)
396
+ throw new Error('Budget not found');
397
+ const bdiField = data.bdiType === 'Principal' ? 'bdiItemsPrincipal' : 'bdiItemsDiferenciado';
398
+ const bdiValueField = data.bdiType === 'Principal' ? 'bdiPrincipal' : 'bdiDiferenciado';
399
+ const currentItems = rawData[bdiField] || [];
400
+ const newItem = {
401
+ id: crypto.randomUUID(),
402
+ description: data.item.description,
403
+ percentage: data.item.percentage,
404
+ incidence: data.item.incidence
405
+ };
406
+ const updatedItems = [...currentItems, newItem];
407
+ const newBdiTotal = calculateBdiTotal(updatedItems);
408
+ const updatedData = {
409
+ ...rawData,
410
+ [bdiField]: updatedItems,
411
+ [bdiValueField]: newBdiTotal
412
+ };
413
+ const { error } = await supabase
414
+ .from('budgets')
415
+ .update({
416
+ data: updatedData,
417
+ last_modified: new Date().toISOString()
418
+ })
419
+ .eq('id', data.budgetId);
420
+ if (error)
421
+ throw new Error(`Failed to add BDI item: ${error.message}`);
422
+ }
423
+ /**
424
+ * Prepares editing an existing BDI item
425
+ */
426
+ export function prepareEditBdiItem(budgetId, bdiType, itemId, updates) {
427
+ const changes = [];
428
+ if (updates.description !== undefined)
429
+ changes.push(`descrição: "${updates.description}"`);
430
+ if (updates.percentage !== undefined)
431
+ changes.push(`percentual: ${updates.percentage}%`);
432
+ if (updates.incidence !== undefined)
433
+ changes.push(`incidência: ${updates.incidence}`);
434
+ return {
435
+ action: 'edit_bdi_item',
436
+ preview: `Editar item do BDI ${bdiType}: ${changes.join(', ')}`,
437
+ data: { budgetId, bdiType, itemId, updates }
438
+ };
439
+ }
440
+ /**
441
+ * Executes editing BDI item
442
+ * ATUALIZADO: Recalcula automaticamente o BDI total
443
+ */
444
+ export async function executeEditBdiItem(auth, data) {
445
+ const rawData = await getRawBudgetData(data.budgetId);
446
+ if (!rawData)
447
+ throw new Error('Budget not found');
448
+ const bdiField = data.bdiType === 'Principal' ? 'bdiItemsPrincipal' : 'bdiItemsDiferenciado';
449
+ const bdiValueField = data.bdiType === 'Principal' ? 'bdiPrincipal' : 'bdiDiferenciado';
450
+ const items = [...(rawData[bdiField] || [])];
451
+ const itemIndex = items.findIndex((item) => item.id === data.itemId);
452
+ if (itemIndex === -1)
453
+ throw new Error('BDI item not found');
454
+ items[itemIndex] = {
455
+ ...items[itemIndex],
456
+ ...data.updates
457
+ };
458
+ const newBdiTotal = calculateBdiTotal(items);
459
+ const updatedData = {
460
+ ...rawData,
461
+ [bdiField]: items,
462
+ [bdiValueField]: newBdiTotal
463
+ };
464
+ const { error } = await supabase
465
+ .from('budgets')
466
+ .update({
467
+ data: updatedData,
468
+ last_modified: new Date().toISOString()
469
+ })
470
+ .eq('id', data.budgetId);
471
+ if (error)
472
+ throw new Error(`Failed to edit BDI item: ${error.message}`);
473
+ }
474
+ /**
475
+ * Prepares removing an item from BDI composition
476
+ */
477
+ export function prepareRemoveBdiItem(budgetId, bdiType, itemId, itemDescription) {
478
+ return {
479
+ action: 'remove_bdi_item',
480
+ preview: `Remover item do BDI ${bdiType}: "${itemDescription}"`,
481
+ data: { budgetId, bdiType, itemId }
482
+ };
483
+ }
484
+ /**
485
+ * Executes removing BDI item
486
+ * ATUALIZADO: Recalcula automaticamente o BDI total
487
+ */
488
+ export async function executeRemoveBdiItem(auth, data) {
489
+ const rawData = await getRawBudgetData(data.budgetId);
490
+ if (!rawData)
491
+ throw new Error('Budget not found');
492
+ const bdiField = data.bdiType === 'Principal' ? 'bdiItemsPrincipal' : 'bdiItemsDiferenciado';
493
+ const bdiValueField = data.bdiType === 'Principal' ? 'bdiPrincipal' : 'bdiDiferenciado';
494
+ const items = [...(rawData[bdiField] || [])];
495
+ const filteredItems = items.filter((item) => item.id !== data.itemId);
496
+ const newBdiTotal = calculateBdiTotal(filteredItems);
497
+ const updatedData = {
498
+ ...rawData,
499
+ [bdiField]: filteredItems,
500
+ [bdiValueField]: newBdiTotal
501
+ };
502
+ const { error } = await supabase
503
+ .from('budgets')
504
+ .update({
505
+ data: updatedData,
506
+ last_modified: new Date().toISOString()
507
+ })
508
+ .eq('id', data.budgetId);
509
+ if (error)
510
+ throw new Error(`Failed to remove BDI item: ${error.message}`);
511
+ }
512
+ // ================ EDIT ITEM FUNCTIONS ================
513
+ /**
514
+ * Prepares editing an existing item
515
+ */
516
+ export function prepareEditItem(budgetId, itemId, updates) {
517
+ const changes = [];
518
+ if (updates.description !== undefined)
519
+ changes.push(`descrição`);
520
+ if (updates.unit !== undefined)
521
+ changes.push(`unidade: ${updates.unit}`);
522
+ if (updates.quantity !== undefined)
523
+ changes.push(`qtd: ${updates.quantity}`);
524
+ if (updates.unitCost !== undefined)
525
+ changes.push(`custo: R$ ${updates.unitCost.toFixed(2)}`);
526
+ if (updates.level !== undefined)
527
+ changes.push(`nível: ${updates.level}`);
528
+ if (updates.code !== undefined)
529
+ changes.push(`código: ${updates.code}`);
530
+ return {
531
+ action: 'edit_item',
532
+ preview: `Editar item: ${changes.join(', ')}`,
533
+ data: { budgetId, itemId, updates }
534
+ };
535
+ }
536
+ /**
537
+ * Executes item edit
538
+ */
539
+ export async function executeEditItem(auth, data) {
540
+ const rawData = await getRawBudgetData(data.budgetId);
541
+ if (!rawData)
542
+ throw new Error('Budget not found');
543
+ const rows = [...(rawData.rows || [])];
544
+ const itemIndex = rows.findIndex((r) => r.id === data.itemId);
545
+ if (itemIndex === -1)
546
+ throw new Error('Item not found in budget');
547
+ // Apply updates
548
+ const item = rows[itemIndex];
549
+ const updates = data.updates;
550
+ if (updates.description !== undefined)
551
+ item.description = updates.description;
552
+ if (updates.unit !== undefined)
553
+ item.unit = updates.unit;
554
+ if (updates.quantity !== undefined)
555
+ item.quantity = updates.quantity;
556
+ if (updates.unitCost !== undefined)
557
+ item.unitCost = updates.unitCost;
558
+ if (updates.level !== undefined)
559
+ item.level = updates.level;
560
+ if (updates.code !== undefined)
561
+ item.code = updates.code;
562
+ if (updates.bdiType !== undefined)
563
+ item.bdiType = updates.bdiType;
564
+ rows[itemIndex] = item;
565
+ const updatedData = {
566
+ ...rawData,
567
+ rows
568
+ };
569
+ const { error } = await supabase
570
+ .from('budgets')
571
+ .update({
572
+ data: updatedData,
573
+ last_modified: new Date().toISOString()
574
+ })
575
+ .eq('id', data.budgetId);
576
+ if (error)
577
+ throw new Error(`Failed to edit item: ${error.message}`);
578
+ }
579
+ // ================ DUPLICATE BUDGET FUNCTIONS ================
580
+ /**
581
+ * Prepares duplicating a budget
582
+ */
583
+ export function prepareDuplicateBudget(auth, budgetId, newTitle) {
584
+ return {
585
+ action: 'duplicate_budget',
586
+ preview: `Duplicar orçamento como "${newTitle}"`,
587
+ data: { budgetId, newTitle, tenantId: auth.tenantId, userId: auth.userId, creator: auth.userName }
588
+ };
589
+ }
590
+ /**
591
+ * Executes budget duplication
592
+ */
593
+ export async function executeDuplicateBudget(auth, data) {
594
+ // Get original budget
595
+ const { data: original, error: fetchError } = await supabase
596
+ .from('budgets')
597
+ .select('*')
598
+ .eq('id', data.budgetId)
599
+ .single();
600
+ if (fetchError || !original)
601
+ throw new Error('Original budget not found');
602
+ const now = new Date().toISOString();
603
+ const newBudget = {
604
+ title: data.newTitle,
605
+ tenant_id: data.tenantId,
606
+ user_id: data.userId,
607
+ creator: data.creator,
608
+ created_at: now,
609
+ last_modified: now,
610
+ shared: false,
611
+ deleted: false,
612
+ data: {
613
+ ...original.data,
614
+ title: data.newTitle
615
+ }
616
+ };
617
+ const { data: result, error } = await supabase
618
+ .from('budgets')
619
+ .insert(newBudget)
620
+ .select('id')
621
+ .single();
622
+ if (error)
623
+ throw new Error(`Failed to duplicate budget: ${error.message}`);
624
+ return result.id;
625
+ }
626
+ // ================ MOVE ITEM (CHANGE LEVEL) ================
627
+ /**
628
+ * Prepares moving an item to a different level
629
+ */
630
+ export function prepareMoveItem(budgetId, itemId, newLevel, itemDescription) {
631
+ return {
632
+ action: 'edit_item',
633
+ preview: `Mover "${itemDescription}" para nível ${newLevel}`,
634
+ data: {
635
+ budgetId,
636
+ itemId,
637
+ updates: { level: newLevel }
638
+ }
639
+ };
640
+ }
641
+ // ================ EDIT BUDGET (Title, BDI, Data Base) ================
642
+ /**
643
+ * Prepares editing budget metadata
644
+ */
645
+ export function prepareEditBudget(budgetId, updates) {
646
+ const changes = [];
647
+ if (updates.title !== undefined)
648
+ changes.push(`título: "${updates.title}"`);
649
+ if (updates.baseDate !== undefined)
650
+ changes.push(`data base: ${updates.baseDate}`);
651
+ if (updates.bdiPrincipal !== undefined)
652
+ changes.push(`BDI Principal: ${updates.bdiPrincipal}%`);
653
+ if (updates.bdiDiferenciado !== undefined)
654
+ changes.push(`BDI Diferenciado: ${updates.bdiDiferenciado}%`);
655
+ return {
656
+ action: 'edit_budget',
657
+ preview: `Editar orçamento: ${changes.join(', ')}`,
658
+ data: { budgetId, updates }
659
+ };
660
+ }
661
+ /**
662
+ * Executes budget metadata editing
663
+ */
664
+ export async function executeEditBudget(auth, data) {
665
+ const rawData = await getRawBudgetData(data.budgetId);
666
+ if (!rawData)
667
+ throw new Error('Budget not found');
668
+ const updates = data.updates;
669
+ const updatePayload = {
670
+ last_modified: new Date().toISOString()
671
+ };
672
+ // Handle title (it's a column, not in data JSONB)
673
+ if (updates.title !== undefined) {
674
+ updatePayload.title = updates.title;
675
+ }
676
+ // Handle data JSONB fields
677
+ const newData = { ...rawData };
678
+ if (updates.baseDate !== undefined)
679
+ newData.baseDate = updates.baseDate;
680
+ if (updates.bdiPrincipal !== undefined)
681
+ newData.bdiPrincipal = updates.bdiPrincipal;
682
+ if (updates.bdiDiferenciado !== undefined)
683
+ newData.bdiDiferenciado = updates.bdiDiferenciado;
684
+ updatePayload.data = newData;
685
+ const { error } = await supabase
686
+ .from('budgets')
687
+ .update(updatePayload)
688
+ .eq('id', data.budgetId);
689
+ if (error)
690
+ throw new Error(`Failed to edit budget: ${error.message}`);
691
+ }
692
+ // ================ SET ITEM BDI TYPE ================
693
+ /**
694
+ * Prepares setting BDI type for an item
695
+ */
696
+ export function prepareSetBdiType(budgetId, itemId, bdiType, itemDescription) {
697
+ return {
698
+ action: 'edit_item',
699
+ preview: `Definir BDI ${bdiType} para "${itemDescription}"`,
700
+ data: {
701
+ budgetId,
702
+ itemId,
703
+ updates: { bdiType }
704
+ }
705
+ };
706
+ }
707
+ // ================ SET ABC CLASSIFICATION ================
708
+ /**
709
+ * Prepares ABC classification for an item
710
+ */
711
+ export function prepareSetAbcClassification(budgetId, itemId, classification, itemDescription) {
712
+ return {
713
+ action: 'set_abc_classification',
714
+ preview: `Classificar "${itemDescription}" como ${classification}`,
715
+ data: { budgetId, itemId, classification }
716
+ };
717
+ }
718
+ /**
719
+ * Executes ABC classification setting
720
+ */
721
+ export async function executeSetAbcClassification(auth, data) {
722
+ const rawData = await getRawBudgetData(data.budgetId);
723
+ if (!rawData)
724
+ throw new Error('Budget not found');
725
+ const abcClassifications = rawData.abcClassifications || {};
726
+ abcClassifications[data.itemId] = data.classification;
727
+ const updatedData = {
728
+ ...rawData,
729
+ abcClassifications
730
+ };
731
+ const { error } = await supabase
732
+ .from('budgets')
733
+ .update({
734
+ data: updatedData,
735
+ last_modified: new Date().toISOString()
736
+ })
737
+ .eq('id', data.budgetId);
738
+ if (error)
739
+ throw new Error(`Failed to set ABC classification: ${error.message}`);
740
+ }
741
+ // ================ EDIT COMPOSITION INPUTS ================
742
+ /**
743
+ * Prepares editing inputs of a composition
744
+ */
745
+ export function prepareEditCompositionInput(budgetId, compositionId, inputId, updates, inputDescription) {
746
+ const changes = [];
747
+ if (updates.description !== undefined)
748
+ changes.push(`descrição`);
749
+ if (updates.quantity !== undefined)
750
+ changes.push(`qtd: ${updates.quantity}`);
751
+ if (updates.unitCost !== undefined)
752
+ changes.push(`custo: R$ ${updates.unitCost.toFixed(2)}`);
753
+ if (updates.unit !== undefined)
754
+ changes.push(`unidade: ${updates.unit}`);
755
+ return {
756
+ action: 'edit_composition_input',
757
+ preview: `Editar insumo "${inputDescription}": ${changes.join(', ')}`,
758
+ data: { budgetId, compositionId, inputId, updates }
759
+ };
760
+ }
761
+ /**
762
+ * Executes editing composition input
763
+ */
764
+ export async function executeEditCompositionInput(auth, data) {
765
+ const rawData = await getRawBudgetData(data.budgetId);
766
+ if (!rawData)
767
+ throw new Error('Budget not found');
768
+ const rows = [...(rawData.rows || [])];
769
+ // Find the composition
770
+ const findAndUpdateInput = (items) => {
771
+ for (let i = 0; i < items.length; i++) {
772
+ const item = items[i];
773
+ if (item.id === data.compositionId && item.composition) {
774
+ // Found the composition, now find and update the input
775
+ const inputIndex = item.composition.findIndex((inp) => inp.id === data.inputId);
776
+ if (inputIndex !== -1) {
777
+ const input = item.composition[inputIndex];
778
+ const updates = data.updates;
779
+ if (updates.code !== undefined)
780
+ input.code = updates.code;
781
+ if (updates.description !== undefined)
782
+ input.description = updates.description;
783
+ if (updates.unit !== undefined)
784
+ input.unit = updates.unit;
785
+ if (updates.quantity !== undefined)
786
+ input.quantity = updates.quantity;
787
+ if (updates.unitCost !== undefined)
788
+ input.unitCost = updates.unitCost;
789
+ if (updates.itemType !== undefined)
790
+ input.itemType = updates.itemType;
791
+ item.composition[inputIndex] = input;
792
+ // Recalculate composition unitCost
793
+ item.unitCost = item.composition.reduce((sum, inp) => sum + (inp.quantity || 0) * (inp.unitCost || 0), 0);
794
+ items[i] = item;
795
+ return true;
796
+ }
797
+ }
798
+ // Recursively check nested compositions
799
+ if (item.composition && findAndUpdateInput(item.composition)) {
800
+ return true;
801
+ }
802
+ }
803
+ return false;
804
+ };
805
+ if (!findAndUpdateInput(rows)) {
806
+ throw new Error('Input not found in composition');
807
+ }
808
+ const updatedData = { ...rawData, rows };
809
+ const { error } = await supabase
810
+ .from('budgets')
811
+ .update({
812
+ data: updatedData,
813
+ last_modified: new Date().toISOString()
814
+ })
815
+ .eq('id', data.budgetId);
816
+ if (error)
817
+ throw new Error(`Failed to edit composition input: ${error.message}`);
818
+ }
819
+ /**
820
+ * Prepares adding an input to a composition
821
+ */
822
+ export function prepareAddCompositionInput(budgetId, compositionId, input) {
823
+ const total = input.quantity * input.unitCost;
824
+ return {
825
+ action: 'add_composition_input',
826
+ preview: `Adicionar insumo "${input.description}" (${input.quantity} ${input.unit} x R$ ${input.unitCost.toFixed(2)} = R$ ${total.toFixed(2)})`,
827
+ data: { budgetId, compositionId, input }
828
+ };
829
+ }
830
+ /**
831
+ * Executes adding input to composition
832
+ */
833
+ export async function executeAddCompositionInput(auth, data) {
834
+ const rawData = await getRawBudgetData(data.budgetId);
835
+ if (!rawData)
836
+ throw new Error('Budget not found');
837
+ const rows = [...(rawData.rows || [])];
838
+ const findAndAddInput = (items) => {
839
+ for (let i = 0; i < items.length; i++) {
840
+ const item = items[i];
841
+ if (item.id === data.compositionId) {
842
+ if (!item.composition)
843
+ item.composition = [];
844
+ const newInput = {
845
+ id: crypto.randomUUID(),
846
+ code: data.input.code || '',
847
+ description: data.input.description,
848
+ unit: data.input.unit,
849
+ quantity: data.input.quantity,
850
+ unitCost: data.input.unitCost,
851
+ itemType: data.input.itemType || 'INSUMO'
852
+ };
853
+ item.composition.push(newInput);
854
+ // Recalculate unitCost
855
+ item.unitCost = item.composition.reduce((sum, inp) => sum + (inp.quantity || 0) * (inp.unitCost || 0), 0);
856
+ item.isComposition = true;
857
+ items[i] = item;
858
+ return true;
859
+ }
860
+ if (item.composition && findAndAddInput(item.composition)) {
861
+ return true;
862
+ }
863
+ }
864
+ return false;
865
+ };
866
+ if (!findAndAddInput(rows)) {
867
+ throw new Error('Composition not found');
868
+ }
869
+ const updatedData = { ...rawData, rows };
870
+ const { error } = await supabase
871
+ .from('budgets')
872
+ .update({
873
+ data: updatedData,
874
+ last_modified: new Date().toISOString()
875
+ })
876
+ .eq('id', data.budgetId);
877
+ if (error)
878
+ throw new Error(`Failed to add composition input: ${error.message}`);
879
+ }
880
+ /**
881
+ * Prepares removing an input from a composition
882
+ */
883
+ export function prepareRemoveCompositionInput(budgetId, compositionId, inputId, inputDescription) {
884
+ return {
885
+ action: 'remove_composition_input',
886
+ preview: `Remover insumo "${inputDescription}" da composição`,
887
+ data: { budgetId, compositionId, inputId }
888
+ };
889
+ }
890
+ /**
891
+ * Executes removing input from composition
892
+ */
893
+ export async function executeRemoveCompositionInput(auth, data) {
894
+ const rawData = await getRawBudgetData(data.budgetId);
895
+ if (!rawData)
896
+ throw new Error('Budget not found');
897
+ const rows = [...(rawData.rows || [])];
898
+ const findAndRemoveInput = (items) => {
899
+ for (let i = 0; i < items.length; i++) {
900
+ const item = items[i];
901
+ if (item.id === data.compositionId && item.composition) {
902
+ const inputIndex = item.composition.findIndex((inp) => inp.id === data.inputId);
903
+ if (inputIndex !== -1) {
904
+ item.composition.splice(inputIndex, 1);
905
+ // Recalculate unitCost
906
+ item.unitCost = item.composition.reduce((sum, inp) => sum + (inp.quantity || 0) * (inp.unitCost || 0), 0);
907
+ items[i] = item;
908
+ return true;
909
+ }
910
+ }
911
+ if (item.composition && findAndRemoveInput(item.composition)) {
912
+ return true;
913
+ }
914
+ }
915
+ return false;
916
+ };
917
+ if (!findAndRemoveInput(rows)) {
918
+ throw new Error('Input not found in composition');
919
+ }
920
+ const updatedData = { ...rawData, rows };
921
+ const { error } = await supabase
922
+ .from('budgets')
923
+ .update({
924
+ data: updatedData,
925
+ last_modified: new Date().toISOString()
926
+ })
927
+ .eq('id', data.budgetId);
928
+ if (error)
929
+ throw new Error(`Failed to remove composition input: ${error.message}`);
930
+ }
931
+ // ================ CONVERT COST TO COMPOSITION ================
932
+ /**
933
+ * Prepares converting a simple cost item to a composition
934
+ */
935
+ export function prepareConvertToComposition(budgetId, itemId, itemDescription) {
936
+ return {
937
+ action: 'convert_to_composition',
938
+ preview: `Converter "${itemDescription}" para composição`,
939
+ data: { budgetId, itemId }
940
+ };
941
+ }
942
+ /**
943
+ * Executes converting a cost item to a composition
944
+ */
945
+ export async function executeConvertToComposition(auth, data) {
946
+ const rawData = await getRawBudgetData(data.budgetId);
947
+ if (!rawData)
948
+ throw new Error('Budget not found');
949
+ const rows = [...(rawData.rows || [])];
950
+ const findAndConvert = (items) => {
951
+ for (let i = 0; i < items.length; i++) {
952
+ const item = items[i];
953
+ if (item.id === data.itemId) {
954
+ // Convert to composition: set isComposition flag and initialize empty composition array
955
+ item.isComposition = true;
956
+ if (!item.composition) {
957
+ // Create an initial input based on the item's current cost
958
+ if (item.unitCost > 0) {
959
+ item.composition = [{
960
+ id: crypto.randomUUID(),
961
+ code: '',
962
+ description: 'Custo base',
963
+ unit: item.unit || 'un',
964
+ quantity: 1,
965
+ unitCost: item.unitCost,
966
+ itemType: 'INSUMO'
967
+ }];
968
+ }
969
+ else {
970
+ item.composition = [];
971
+ }
972
+ }
973
+ items[i] = item;
974
+ return true;
975
+ }
976
+ if (item.composition && findAndConvert(item.composition)) {
977
+ return true;
978
+ }
979
+ }
980
+ return false;
981
+ };
982
+ if (!findAndConvert(rows)) {
983
+ throw new Error('Item not found');
984
+ }
985
+ const updatedData = { ...rawData, rows };
986
+ const { error } = await supabase
987
+ .from('budgets')
988
+ .update({
989
+ data: updatedData,
990
+ last_modified: new Date().toISOString()
991
+ })
992
+ .eq('id', data.budgetId);
993
+ if (error)
994
+ throw new Error(`Failed to convert to composition: ${error.message}`);
995
+ }
996
+ // ================ CONVERT COMPOSITION TO COST ================
997
+ /**
998
+ * Prepares converting a composition back to simple cost
999
+ */
1000
+ export function prepareConvertToCost(budgetId, itemId, itemDescription) {
1001
+ return {
1002
+ action: 'convert_to_cost',
1003
+ preview: `Converter "${itemDescription}" para custo simples (remove insumos)`,
1004
+ data: { budgetId, itemId }
1005
+ };
1006
+ }
1007
+ /**
1008
+ * Executes converting a composition back to cost
1009
+ */
1010
+ export async function executeConvertToCost(auth, data) {
1011
+ const rawData = await getRawBudgetData(data.budgetId);
1012
+ if (!rawData)
1013
+ throw new Error('Budget not found');
1014
+ const rows = [...(rawData.rows || [])];
1015
+ const findAndConvert = (items) => {
1016
+ for (let i = 0; i < items.length; i++) {
1017
+ const item = items[i];
1018
+ if (item.id === data.itemId) {
1019
+ // Convert to cost: remove composition flag and array, keep current unitCost
1020
+ item.isComposition = false;
1021
+ delete item.composition;
1022
+ items[i] = item;
1023
+ return true;
1024
+ }
1025
+ if (item.composition && findAndConvert(item.composition)) {
1026
+ return true;
1027
+ }
1028
+ }
1029
+ return false;
1030
+ };
1031
+ if (!findAndConvert(rows)) {
1032
+ throw new Error('Item not found');
1033
+ }
1034
+ const updatedData = { ...rawData, rows };
1035
+ const { error } = await supabase
1036
+ .from('budgets')
1037
+ .update({
1038
+ data: updatedData,
1039
+ last_modified: new Date().toISOString()
1040
+ })
1041
+ .eq('id', data.budgetId);
1042
+ if (error)
1043
+ throw new Error(`Failed to convert to cost: ${error.message}`);
1044
+ }
1045
+ /**
1046
+ * Lists all tags for a specific budget
1047
+ */
1048
+ export async function getTagsForBudget(budgetId) {
1049
+ const { data, error } = await supabase
1050
+ .from('budget_tags')
1051
+ .select('*')
1052
+ .eq('budget_id', budgetId)
1053
+ .order('created_at', { ascending: true });
1054
+ if (error) {
1055
+ console.error('Error fetching tags:', error);
1056
+ return [];
1057
+ }
1058
+ return data || [];
1059
+ }
1060
+ /**
1061
+ * Prepares adding a tag to a budget
1062
+ */
1063
+ export function prepareAddTag(budgetId, name) {
1064
+ return {
1065
+ action: 'add_tag',
1066
+ preview: `Adicionar rótulo "${name}" ao orçamento`,
1067
+ data: { budgetId, name }
1068
+ };
1069
+ }
1070
+ /**
1071
+ * Executes adding a tag
1072
+ */
1073
+ export async function executeAddTag(auth, data) {
1074
+ const { data: tag, error } = await supabase
1075
+ .from('budget_tags')
1076
+ .insert({ budget_id: data.budgetId, name: data.name })
1077
+ .select()
1078
+ .single();
1079
+ if (error)
1080
+ throw new Error(`Failed to add tag: ${error.message}`);
1081
+ // Update last_modified on budget
1082
+ await supabase
1083
+ .from('budgets')
1084
+ .update({ last_modified: new Date().toISOString() })
1085
+ .eq('id', data.budgetId);
1086
+ return tag;
1087
+ }
1088
+ /**
1089
+ * Prepares editing a tag
1090
+ */
1091
+ export function prepareEditTag(tagId, newName, currentName) {
1092
+ return {
1093
+ action: 'edit_tag',
1094
+ preview: `Renomear rótulo de "${currentName}" para "${newName}"`,
1095
+ data: { tagId, newName }
1096
+ };
1097
+ }
1098
+ /**
1099
+ * Executes editing a tag
1100
+ */
1101
+ export async function executeEditTag(auth, data) {
1102
+ const { error } = await supabase
1103
+ .from('budget_tags')
1104
+ .update({ name: data.newName })
1105
+ .eq('id', data.tagId);
1106
+ if (error)
1107
+ throw new Error(`Failed to edit tag: ${error.message}`);
1108
+ }
1109
+ /**
1110
+ * Prepares removing a tag
1111
+ */
1112
+ export function prepareRemoveTag(tagId, tagName) {
1113
+ return {
1114
+ action: 'remove_tag',
1115
+ preview: `Remover rótulo "${tagName}"`,
1116
+ data: { tagId }
1117
+ };
1118
+ }
1119
+ /**
1120
+ * Executes removing a tag
1121
+ */
1122
+ export async function executeRemoveTag(auth, data) {
1123
+ const { error } = await supabase
1124
+ .from('budget_tags')
1125
+ .delete()
1126
+ .eq('id', data.tagId);
1127
+ if (error)
1128
+ throw new Error(`Failed to remove tag: ${error.message}`);
1129
+ }
1130
+ /**
1131
+ * Lists all projects for the authenticated user's tenant
1132
+ */
1133
+ export async function getProjects(auth) {
1134
+ const { data, error } = await supabase
1135
+ .from('projects')
1136
+ .select('*')
1137
+ .eq('deleted', false)
1138
+ .order('created_at', { ascending: false });
1139
+ if (error) {
1140
+ console.error('Error fetching projects:', error);
1141
+ return [];
1142
+ }
1143
+ return (data || []).map(p => ({
1144
+ id: p.id,
1145
+ code: p.code,
1146
+ name: p.name,
1147
+ description: p.description,
1148
+ created_at: p.created_at,
1149
+ updated_at: p.updated_at
1150
+ }));
1151
+ }
1152
+ /**
1153
+ * Prepares creating a new project
1154
+ */
1155
+ export function prepareCreateProject(name, description, code) {
1156
+ return {
1157
+ action: 'create_project',
1158
+ preview: `Criar projeto "${name}"${code ? ` (${code})` : ''}`,
1159
+ data: { name, description, code }
1160
+ };
1161
+ }
1162
+ /**
1163
+ * Executes creating a project
1164
+ */
1165
+ export async function executeCreateProject(auth, data) {
1166
+ const now = new Date().toISOString();
1167
+ const newProject = {
1168
+ id: crypto.randomUUID(),
1169
+ name: data.name,
1170
+ description: data.description || '',
1171
+ code: data.code || null,
1172
+ user_id: auth.userId,
1173
+ created_at: now,
1174
+ updated_at: now,
1175
+ deleted: false
1176
+ };
1177
+ const { data: project, error } = await supabase
1178
+ .from('projects')
1179
+ .insert(newProject)
1180
+ .select()
1181
+ .single();
1182
+ if (error)
1183
+ throw new Error(`Failed to create project: ${error.message}`);
1184
+ return {
1185
+ id: project.id,
1186
+ code: project.code,
1187
+ name: project.name,
1188
+ description: project.description,
1189
+ created_at: project.created_at,
1190
+ updated_at: project.updated_at
1191
+ };
1192
+ }
1193
+ /**
1194
+ * Prepares editing a project
1195
+ */
1196
+ export function prepareEditProject(projectId, updates, currentName) {
1197
+ const changes = [];
1198
+ if (updates.name)
1199
+ changes.push(`nome: "${updates.name}"`);
1200
+ if (updates.description !== undefined)
1201
+ changes.push(`descrição`);
1202
+ if (updates.code !== undefined)
1203
+ changes.push(`código: "${updates.code}"`);
1204
+ return {
1205
+ action: 'edit_project',
1206
+ preview: `Editar projeto "${currentName}": ${changes.join(', ')}`,
1207
+ data: { projectId, updates }
1208
+ };
1209
+ }
1210
+ /**
1211
+ * Executes editing a project
1212
+ */
1213
+ export async function executeEditProject(auth, data) {
1214
+ const updatePayload = {
1215
+ updated_at: new Date().toISOString()
1216
+ };
1217
+ if (data.updates.name !== undefined)
1218
+ updatePayload.name = data.updates.name;
1219
+ if (data.updates.description !== undefined)
1220
+ updatePayload.description = data.updates.description;
1221
+ if (data.updates.code !== undefined)
1222
+ updatePayload.code = data.updates.code;
1223
+ const { error } = await supabase
1224
+ .from('projects')
1225
+ .update(updatePayload)
1226
+ .eq('id', data.projectId);
1227
+ if (error)
1228
+ throw new Error(`Failed to edit project: ${error.message}`);
1229
+ }
1230
+ /**
1231
+ * Prepares deleting a project (soft delete)
1232
+ */
1233
+ export function prepareDeleteProject(projectId, projectName) {
1234
+ return {
1235
+ action: 'delete_project',
1236
+ preview: `Excluir projeto "${projectName}" (orçamentos serão desvinculados)`,
1237
+ data: { projectId }
1238
+ };
1239
+ }
1240
+ /**
1241
+ * Executes deleting a project (soft delete)
1242
+ */
1243
+ export async function executeDeleteProject(auth, data) {
1244
+ // First, unlink all budgets from this project
1245
+ const { error: unlinkError } = await supabase
1246
+ .from('budgets')
1247
+ .update({ project_id: null })
1248
+ .eq('project_id', data.projectId);
1249
+ if (unlinkError) {
1250
+ console.error('Error unlinking budgets:', unlinkError);
1251
+ }
1252
+ // Soft delete the project
1253
+ const { error } = await supabase
1254
+ .from('projects')
1255
+ .update({ deleted: true })
1256
+ .eq('id', data.projectId);
1257
+ if (error)
1258
+ throw new Error(`Failed to delete project: ${error.message}`);
1259
+ }
1260
+ /**
1261
+ * Prepares linking a budget to a project
1262
+ */
1263
+ export function prepareLinkBudgetToProject(budgetId, projectId, budgetTitle, projectName) {
1264
+ const preview = projectId
1265
+ ? `Vincular orçamento "${budgetTitle}" ao projeto "${projectName}"`
1266
+ : `Desvincular orçamento "${budgetTitle}" de qualquer projeto`;
1267
+ return {
1268
+ action: 'link_budget_to_project',
1269
+ preview,
1270
+ data: { budgetId, projectId }
1271
+ };
1272
+ }
1273
+ /**
1274
+ * Executes linking a budget to a project
1275
+ */
1276
+ export async function executeLinkBudgetToProject(auth, data) {
1277
+ const { error } = await supabase
1278
+ .from('budgets')
1279
+ .update({
1280
+ project_id: data.projectId,
1281
+ last_modified: new Date().toISOString()
1282
+ })
1283
+ .eq('id', data.budgetId);
1284
+ if (error)
1285
+ throw new Error(`Failed to link budget to project: ${error.message}`);
1286
+ }