@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.
@@ -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
+ }