@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.
- package/dist/bin/quanty-mcp-server.exe +0 -0
- package/dist/core/analysisService.d.ts +28 -0
- package/dist/core/analysisService.d.ts.map +1 -1
- package/dist/core/analysisService.js +86 -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 +356 -0
- package/dist/core/budgetService.d.ts.map +1 -1
- package/dist/core/budgetService.js +1020 -2
- 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 +1608 -190
- 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,
|
|
@@ -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
|
+
}
|