@hed-hog/finance 0.0.349 → 0.0.351

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.
Files changed (112) hide show
  1. package/dist/dto/create-financial-title.dto.d.ts +6 -0
  2. package/dist/dto/create-financial-title.dto.d.ts.map +1 -1
  3. package/dist/dto/create-financial-title.dto.js +27 -1
  4. package/dist/dto/create-financial-title.dto.js.map +1 -1
  5. package/dist/finance-data.controller.d.ts +12 -0
  6. package/dist/finance-data.controller.d.ts.map +1 -1
  7. package/dist/finance-installments.controller.d.ts +120 -0
  8. package/dist/finance-installments.controller.d.ts.map +1 -1
  9. package/dist/finance-realtime.controller.d.ts +7 -0
  10. package/dist/finance-realtime.controller.d.ts.map +1 -0
  11. package/dist/finance-realtime.controller.js +34 -0
  12. package/dist/finance-realtime.controller.js.map +1 -0
  13. package/dist/finance-realtime.service.d.ts +36 -0
  14. package/dist/finance-realtime.service.d.ts.map +1 -0
  15. package/dist/finance-realtime.service.js +59 -0
  16. package/dist/finance-realtime.service.js.map +1 -0
  17. package/dist/finance-statements.controller.d.ts +6 -0
  18. package/dist/finance-statements.controller.d.ts.map +1 -1
  19. package/dist/finance.module.d.ts.map +1 -1
  20. package/dist/finance.module.js +28 -0
  21. package/dist/finance.module.js.map +1 -1
  22. package/dist/finance.service.d.ts +142 -1
  23. package/dist/finance.service.d.ts.map +1 -1
  24. package/dist/finance.service.js +134 -19
  25. package/dist/finance.service.js.map +1 -1
  26. package/dist/mcp-tools/finance-audit-logs.mcp-tools.d.ts +8 -0
  27. package/dist/mcp-tools/finance-audit-logs.mcp-tools.d.ts.map +1 -0
  28. package/dist/mcp-tools/finance-audit-logs.mcp-tools.js +60 -0
  29. package/dist/mcp-tools/finance-audit-logs.mcp-tools.js.map +1 -0
  30. package/dist/mcp-tools/finance-bank-accounts.mcp-tools.d.ts +16 -0
  31. package/dist/mcp-tools/finance-bank-accounts.mcp-tools.d.ts.map +1 -0
  32. package/dist/mcp-tools/finance-bank-accounts.mcp-tools.js +121 -0
  33. package/dist/mcp-tools/finance-bank-accounts.mcp-tools.js.map +1 -0
  34. package/dist/mcp-tools/finance-categories.mcp-tools.d.ts +20 -0
  35. package/dist/mcp-tools/finance-categories.mcp-tools.d.ts.map +1 -0
  36. package/dist/mcp-tools/finance-categories.mcp-tools.js +135 -0
  37. package/dist/mcp-tools/finance-categories.mcp-tools.js.map +1 -0
  38. package/dist/mcp-tools/finance-collections.mcp-tools.d.ts +16 -0
  39. package/dist/mcp-tools/finance-collections.mcp-tools.d.ts.map +1 -0
  40. package/dist/mcp-tools/finance-collections.mcp-tools.js +91 -0
  41. package/dist/mcp-tools/finance-collections.mcp-tools.js.map +1 -0
  42. package/dist/mcp-tools/finance-cost-centers.mcp-tools.d.ts +16 -0
  43. package/dist/mcp-tools/finance-cost-centers.mcp-tools.d.ts.map +1 -0
  44. package/dist/mcp-tools/finance-cost-centers.mcp-tools.js +114 -0
  45. package/dist/mcp-tools/finance-cost-centers.mcp-tools.js.map +1 -0
  46. package/dist/mcp-tools/finance-currencies.mcp-tools.d.ts +16 -0
  47. package/dist/mcp-tools/finance-currencies.mcp-tools.d.ts.map +1 -0
  48. package/dist/mcp-tools/finance-currencies.mcp-tools.js +120 -0
  49. package/dist/mcp-tools/finance-currencies.mcp-tools.js.map +1 -0
  50. package/dist/mcp-tools/finance-data.mcp-tools.d.ts +15 -0
  51. package/dist/mcp-tools/finance-data.mcp-tools.d.ts.map +1 -0
  52. package/dist/mcp-tools/finance-data.mcp-tools.js +80 -0
  53. package/dist/mcp-tools/finance-data.mcp-tools.js.map +1 -0
  54. package/dist/mcp-tools/finance-installments.mcp-tools.d.ts +93 -0
  55. package/dist/mcp-tools/finance-installments.mcp-tools.d.ts.map +1 -0
  56. package/dist/mcp-tools/finance-installments.mcp-tools.js +646 -0
  57. package/dist/mcp-tools/finance-installments.mcp-tools.js.map +1 -0
  58. package/dist/mcp-tools/finance-period-close.mcp-tools.d.ts +9 -0
  59. package/dist/mcp-tools/finance-period-close.mcp-tools.d.ts.map +1 -0
  60. package/dist/mcp-tools/finance-period-close.mcp-tools.js +79 -0
  61. package/dist/mcp-tools/finance-period-close.mcp-tools.js.map +1 -0
  62. package/dist/mcp-tools/finance-reports.mcp-tools.d.ts +10 -0
  63. package/dist/mcp-tools/finance-reports.mcp-tools.d.ts.map +1 -0
  64. package/dist/mcp-tools/finance-reports.mcp-tools.js +89 -0
  65. package/dist/mcp-tools/finance-reports.mcp-tools.js.map +1 -0
  66. package/dist/mcp-tools/finance-statements.mcp-tools.d.ts +34 -0
  67. package/dist/mcp-tools/finance-statements.mcp-tools.d.ts.map +1 -0
  68. package/dist/mcp-tools/finance-statements.mcp-tools.js +253 -0
  69. package/dist/mcp-tools/finance-statements.mcp-tools.js.map +1 -0
  70. package/dist/mcp-tools/finance-transfers.mcp-tools.d.ts +9 -0
  71. package/dist/mcp-tools/finance-transfers.mcp-tools.d.ts.map +1 -0
  72. package/dist/mcp-tools/finance-transfers.mcp-tools.js +79 -0
  73. package/dist/mcp-tools/finance-transfers.mcp-tools.js.map +1 -0
  74. package/hedhog/data/route.yaml +659 -1
  75. package/hedhog/frontend/app/_components/finance-title-actions-menu.tsx.ejs +9 -3
  76. package/hedhog/frontend/app/_lib/http-error.ts.ejs +105 -0
  77. package/hedhog/frontend/app/_lib/use-finance-realtime-refresh.ts.ejs +62 -0
  78. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +3 -0
  79. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +5 -5
  80. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +776 -344
  81. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +7 -13
  82. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +5 -5
  83. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +802 -355
  84. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +9 -3
  85. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +9 -3
  86. package/hedhog/frontend/app/administration/currencies/page.tsx.ejs +9 -3
  87. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +9 -3
  88. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +7 -3
  89. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +3 -3
  90. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +28 -7
  91. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +12 -3
  92. package/hedhog/frontend/messages/en.json +63 -3
  93. package/hedhog/frontend/messages/pt.json +63 -3
  94. package/hedhog/table/financial_title.yaml +10 -0
  95. package/package.json +6 -6
  96. package/src/dto/create-financial-title.dto.ts +23 -0
  97. package/src/finance-realtime.controller.ts +12 -0
  98. package/src/finance-realtime.service.ts +106 -0
  99. package/src/finance.module.ts +28 -0
  100. package/src/finance.service.ts +184 -45
  101. package/src/mcp-tools/finance-audit-logs.mcp-tools.ts +38 -0
  102. package/src/mcp-tools/finance-bank-accounts.mcp-tools.ts +76 -0
  103. package/src/mcp-tools/finance-categories.mcp-tools.ts +86 -0
  104. package/src/mcp-tools/finance-collections.mcp-tools.ts +50 -0
  105. package/src/mcp-tools/finance-cost-centers.mcp-tools.ts +69 -0
  106. package/src/mcp-tools/finance-currencies.mcp-tools.ts +75 -0
  107. package/src/mcp-tools/finance-data.mcp-tools.ts +43 -0
  108. package/src/mcp-tools/finance-installments.mcp-tools.ts +560 -0
  109. package/src/mcp-tools/finance-period-close.mcp-tools.ts +53 -0
  110. package/src/mcp-tools/finance-reports.mcp-tools.ts +59 -0
  111. package/src/mcp-tools/finance-statements.mcp-tools.ts +202 -0
  112. package/src/mcp-tools/finance-transfers.mcp-tools.ts +53 -0
@@ -1,6 +1,6 @@
1
1
  'use client';
2
2
 
3
- import { PersonPickerField } from '@/app/(app)/(libraries)/contact/_components/person-picker';
3
+ import { PersonPickerField } from '@/app/(app)/(libraries)/crm/_components/person-picker';
4
4
  import { CategoryPickerField } from '@/app/(app)/(libraries)/finance/_components/category-picker-field';
5
5
  import { CostCenterPickerField } from '@/app/(app)/(libraries)/finance/_components/cost-center-picker-field';
6
6
  import {
@@ -33,6 +33,7 @@ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
33
33
  import { Label } from '@/components/ui/label';
34
34
  import { Money } from '@/components/ui/money';
35
35
  import { Progress } from '@/components/ui/progress';
36
+ import { ResizableSheetContent } from '@/components/ui/resizable-sheet-content';
36
37
  import {
37
38
  Select,
38
39
  SelectContent,
@@ -42,7 +43,6 @@ import {
42
43
  } from '@/components/ui/select';
43
44
  import {
44
45
  Sheet,
45
- SheetContent,
46
46
  SheetDescription,
47
47
  SheetHeader,
48
48
  SheetTitle,
@@ -86,6 +86,10 @@ import { useEffect, useMemo, useRef, useState } from 'react';
86
86
  import { useFieldArray, useForm, useWatch } from 'react-hook-form';
87
87
  import { z } from 'zod';
88
88
  import { formatarData } from '../../_lib/formatters';
89
+ import {
90
+ getApiErrorMessage,
91
+ getFirstFormErrorMessage,
92
+ } from '../../_lib/http-error';
89
93
  import {
90
94
  canApproveTitle,
91
95
  canCancelTitle,
@@ -95,6 +99,7 @@ import {
95
99
  getFirstActiveSettlementId,
96
100
  } from '../../_lib/title-action-rules';
97
101
  import { useFinanceData } from '../../_lib/use-finance-data';
102
+ import { useFinanceRealtimeRefresh } from '../../_lib/use-finance-realtime-refresh';
98
103
 
99
104
  const INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS = 300;
100
105
 
@@ -196,6 +201,7 @@ const redistributeRemainingInstallments = (
196
201
  const getNewTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
197
202
  z
198
203
  .object({
204
+ tipoTitulo: z.enum(['parcelado', 'recorrente']).default('parcelado'),
199
205
  documento: z.string().trim().min(1, t('validation.documentRequired')),
200
206
  fornecedorId: z.string().min(1, t('validation.supplierRequired')),
201
207
  competencia: z.string().optional(),
@@ -218,12 +224,33 @@ const getNewTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
218
224
  })
219
225
  )
220
226
  .min(1, t('validation.installmentsRequired')),
227
+ recorrencia: z
228
+ .object({
229
+ frequencia: z.string().default('monthly'),
230
+ dataFim: z.string().optional(),
231
+ numOcorrencias: z.coerce.number().int().min(1).max(600).optional(),
232
+ })
233
+ .optional(),
221
234
  categoriaId: z.string().optional(),
222
235
  centroCustoId: z.string().optional(),
223
236
  metodo: z.string().optional(),
224
237
  descricao: z.string().optional(),
225
238
  })
226
239
  .superRefine((values, ctx) => {
240
+ if (values.tipoTitulo === 'recorrente') {
241
+ if (
242
+ !values.recorrencia?.dataFim &&
243
+ !values.recorrencia?.numOcorrencias
244
+ ) {
245
+ ctx.addIssue({
246
+ code: z.ZodIssueCode.custom,
247
+ path: ['recorrencia'],
248
+ message: t('validation.recurrenceEndOrCount'),
249
+ });
250
+ }
251
+ return;
252
+ }
253
+
227
254
  if (values.installments.length !== values.installmentsCount) {
228
255
  ctx.addIssue({
229
256
  code: z.ZodIssueCode.custom,
@@ -344,6 +371,7 @@ function NovoTituloSheet({
344
371
  const form = useForm<NewTitleFormValues>({
345
372
  resolver: zodResolver(newTitleFormSchema),
346
373
  defaultValues: {
374
+ tipoTitulo: 'parcelado',
347
375
  documento: '',
348
376
  fornecedorId: '',
349
377
  competencia: '',
@@ -351,6 +379,11 @@ function NovoTituloSheet({
351
379
  valor: 0,
352
380
  installmentsCount: 1,
353
381
  installments: [{ dueDate: '', amount: 0 }],
382
+ recorrencia: {
383
+ frequencia: 'monthly',
384
+ dataFim: '',
385
+ numOcorrencias: undefined,
386
+ },
354
387
  categoriaId: '',
355
388
  centroCustoId: '',
356
389
  metodo: '',
@@ -379,6 +412,7 @@ function NovoTituloSheet({
379
412
  mode: 'create',
380
413
  titleId: null,
381
414
  values: {
415
+ tipoTitulo: watchedFormValues.tipoTitulo ?? 'parcelado',
382
416
  documento: watchedFormValues.documento ?? '',
383
417
  fornecedorId: watchedFormValues.fornecedorId ?? '',
384
418
  competencia: watchedFormValues.competencia ?? '',
@@ -389,6 +423,11 @@ function NovoTituloSheet({
389
423
  dueDate: installment?.dueDate ?? '',
390
424
  amount: Number(installment?.amount ?? 0),
391
425
  })) ?? [{ dueDate: '', amount: 0 }],
426
+ recorrencia: {
427
+ frequencia: watchedFormValues.recorrencia?.frequencia ?? 'monthly',
428
+ dataFim: watchedFormValues.recorrencia?.dataFim ?? '',
429
+ numOcorrencias: watchedFormValues.recorrencia?.numOcorrencias,
430
+ },
392
431
  categoriaId: watchedFormValues.categoriaId ?? '',
393
432
  centroCustoId: watchedFormValues.centroCustoId ?? '',
394
433
  metodo: watchedFormValues.metodo ?? '',
@@ -442,9 +481,14 @@ function NovoTituloSheet({
442
481
  currentLocaleCode
443
482
  );
444
483
 
445
- return t('draftStatus', { relativeLabel, absoluteLabel });
484
+ return t.has('draftStatus')
485
+ ? t('draftStatus', { relativeLabel, absoluteLabel })
486
+ : `${relativeLabel} - ${absoluteLabel}`;
446
487
  }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft, t]);
447
488
 
489
+ const tipoTitulo = form.watch('tipoTitulo');
490
+ const isRecorrente = tipoTitulo === 'recorrente';
491
+
448
492
  const watchedInstallmentsCount = form.watch('installmentsCount');
449
493
  const watchedTotalValue = form.watch('valor');
450
494
  const watchedDueDate = form.watch('vencimento');
@@ -474,6 +518,7 @@ function NovoTituloSheet({
474
518
  }
475
519
 
476
520
  form.reset({
521
+ tipoTitulo: 'parcelado',
477
522
  documento: '',
478
523
  fornecedorId: '',
479
524
  competencia: '',
@@ -481,6 +526,11 @@ function NovoTituloSheet({
481
526
  valor: 0,
482
527
  installmentsCount: 1,
483
528
  installments: [{ dueDate: '', amount: 0 }],
529
+ recorrencia: {
530
+ frequencia: 'monthly',
531
+ dataFim: '',
532
+ numOcorrencias: undefined,
533
+ },
484
534
  categoriaId: '',
485
535
  centroCustoId: '',
486
536
  metodo: '',
@@ -582,6 +632,7 @@ function NovoTituloSheet({
582
632
 
583
633
  const handleSubmit = async (values: NewTitleFormValues) => {
584
634
  try {
635
+ const recorrente = values.tipoTitulo === 'recorrente';
585
636
  await request({
586
637
  url: '/finance/accounts-payable/installments',
587
638
  method: 'POST',
@@ -601,11 +652,22 @@ function NovoTituloSheet({
601
652
  : undefined,
602
653
  payment_channel: values.metodo || undefined,
603
654
  description: values.descricao?.trim() || undefined,
604
- installments: values.installments.map((installment, index) => ({
605
- installment_number: index + 1,
606
- due_date: installment.dueDate || values.vencimento,
607
- amount: installment.amount,
608
- })),
655
+ ...(recorrente
656
+ ? {
657
+ recurrence_rule: {
658
+ frequency: values.recorrencia?.frequencia ?? 'monthly',
659
+ end_date: values.recorrencia?.dataFim || undefined,
660
+ max_occurrences:
661
+ values.recorrencia?.numOcorrencias || undefined,
662
+ },
663
+ }
664
+ : {
665
+ installments: values.installments.map((installment, index) => ({
666
+ installment_number: index + 1,
667
+ due_date: installment.dueDate || values.vencimento,
668
+ amount: installment.amount,
669
+ })),
670
+ }),
609
671
  attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
610
672
  },
611
673
  });
@@ -621,8 +683,11 @@ function NovoTituloSheet({
621
683
  setAutoRedistributeInstallments(true);
622
684
  handleOpenChange(false);
623
685
  showToastHandler?.('success', t('messages.createSuccess'));
624
- } catch {
625
- showToastHandler?.('error', t('messages.createError'));
686
+ } catch (error) {
687
+ showToastHandler?.(
688
+ 'error',
689
+ getApiErrorMessage(error, t('messages.createError'))
690
+ );
626
691
  }
627
692
  };
628
693
 
@@ -630,6 +695,13 @@ function NovoTituloSheet({
630
695
  handleOpenChange(false);
631
696
  };
632
697
 
698
+ const handleInvalidSubmit = (errors: unknown) => {
699
+ showToastHandler?.(
700
+ 'error',
701
+ getFirstFormErrorMessage(errors, t('messages.createError'))
702
+ );
703
+ };
704
+
633
705
  const clearUploadedFile = () => {
634
706
  setUploadedFileId(null);
635
707
  setUploadedFileName('');
@@ -801,7 +873,11 @@ function NovoTituloSheet({
801
873
  {t('newTitle.action')}
802
874
  </Button>
803
875
  </SheetTrigger>
804
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl gap-0">
876
+ <ResizableSheetContent
877
+ sheetId="finance-payable-create-title"
878
+ defaultWidth={896}
879
+ className="flex h-full w-full flex-col overflow-hidden p-0 gap-0"
880
+ >
805
881
  <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
806
882
  <SheetTitle>{t('newTitle.title')}</SheetTitle>
807
883
  <SheetDescription>{t('newTitle.description')}</SheetDescription>
@@ -809,7 +885,7 @@ function NovoTituloSheet({
809
885
  <Form {...form}>
810
886
  <form
811
887
  className="flex h-full flex-col overflow-hidden"
812
- onSubmit={form.handleSubmit(handleSubmit)}
888
+ onSubmit={form.handleSubmit(handleSubmit, handleInvalidSubmit)}
813
889
  >
814
890
  <FinanceSheetBody className="pt-0">
815
891
  <FinanceSheetSection className="pt-0 mt-0">
@@ -967,6 +1043,38 @@ function NovoTituloSheet({
967
1043
  selectPlaceholder={t('common.select')}
968
1044
  />
969
1045
 
1046
+ <FormField
1047
+ control={form.control}
1048
+ name="tipoTitulo"
1049
+ render={({ field }) => (
1050
+ <FormItem>
1051
+ <FormLabel>{t('titleType.label')}</FormLabel>
1052
+ <div className="flex gap-2">
1053
+ <Button
1054
+ type="button"
1055
+ variant={
1056
+ field.value === 'parcelado' ? 'default' : 'outline'
1057
+ }
1058
+ size="sm"
1059
+ onClick={() => field.onChange('parcelado')}
1060
+ >
1061
+ {t('titleType.installment')}
1062
+ </Button>
1063
+ <Button
1064
+ type="button"
1065
+ variant={
1066
+ field.value === 'recorrente' ? 'default' : 'outline'
1067
+ }
1068
+ size="sm"
1069
+ onClick={() => field.onChange('recorrente')}
1070
+ >
1071
+ {t('titleType.recurring')}
1072
+ </Button>
1073
+ </div>
1074
+ </FormItem>
1075
+ )}
1076
+ />
1077
+
970
1078
  <div className="grid gap-4 xl:grid-cols-2">
971
1079
  <div className="grid gap-4 md:grid-cols-2">
972
1080
  <FormField
@@ -1012,7 +1120,11 @@ function NovoTituloSheet({
1012
1120
  name="valor"
1013
1121
  render={({ field }) => (
1014
1122
  <FormItem>
1015
- <FormLabel>{t('fields.totalValue')}</FormLabel>
1123
+ <FormLabel>
1124
+ {isRecorrente
1125
+ ? t('fields.totalValueRecurring')
1126
+ : t('fields.totalValue')}
1127
+ </FormLabel>
1016
1128
  <FormControl>
1017
1129
  <InputMoney
1018
1130
  ref={field.ref}
@@ -1030,34 +1142,36 @@ function NovoTituloSheet({
1030
1142
  )}
1031
1143
  />
1032
1144
 
1033
- <FormField
1034
- control={form.control}
1035
- name="installmentsCount"
1036
- render={({ field }) => (
1037
- <FormItem>
1038
- <FormLabel>
1039
- {t('installmentsEditor.countLabel')}
1040
- </FormLabel>
1041
- <FormControl>
1042
- <Input
1043
- type="number"
1044
- min={1}
1045
- max={120}
1046
- value={field.value}
1047
- onChange={(event) => {
1048
- const nextValue = Number(
1049
- event.target.value || 1
1050
- );
1051
- field.onChange(
1052
- Number.isNaN(nextValue) ? 1 : nextValue
1053
- );
1054
- }}
1055
- />
1056
- </FormControl>
1057
- <FormMessage />
1058
- </FormItem>
1059
- )}
1060
- />
1145
+ {!isRecorrente && (
1146
+ <FormField
1147
+ control={form.control}
1148
+ name="installmentsCount"
1149
+ render={({ field }) => (
1150
+ <FormItem>
1151
+ <FormLabel>
1152
+ {t('installmentsEditor.countLabel')}
1153
+ </FormLabel>
1154
+ <FormControl>
1155
+ <Input
1156
+ type="number"
1157
+ min={1}
1158
+ max={120}
1159
+ value={field.value}
1160
+ onChange={(event) => {
1161
+ const nextValue = Number(
1162
+ event.target.value || 1
1163
+ );
1164
+ field.onChange(
1165
+ Number.isNaN(nextValue) ? 1 : nextValue
1166
+ );
1167
+ }}
1168
+ />
1169
+ </FormControl>
1170
+ <FormMessage />
1171
+ </FormItem>
1172
+ )}
1173
+ />
1174
+ )}
1061
1175
  </div>
1062
1176
  </div>
1063
1177
  </FinanceSheetSection>
@@ -1066,145 +1180,256 @@ function NovoTituloSheet({
1066
1180
  title={t('sections.installments.title')}
1067
1181
  description={t('sections.installments.description')}
1068
1182
  >
1069
- <div className="space-y-3">
1070
- <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
1071
- <div className="flex items-center gap-2">
1072
- <Checkbox
1073
- id="auto-redistribute-installments-payable"
1074
- checked={autoRedistributeInstallments}
1075
- onCheckedChange={(checked) =>
1076
- setAutoRedistributeInstallments(checked === true)
1077
- }
1078
- />
1079
- <Label
1080
- htmlFor="auto-redistribute-installments-payable"
1081
- className="text-xs text-muted-foreground"
1183
+ {!isRecorrente && (
1184
+ <div className="space-y-3">
1185
+ <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
1186
+ <div className="flex items-center gap-2">
1187
+ <Checkbox
1188
+ id="auto-redistribute-installments-payable"
1189
+ checked={autoRedistributeInstallments}
1190
+ onCheckedChange={(checked) =>
1191
+ setAutoRedistributeInstallments(checked === true)
1192
+ }
1193
+ />
1194
+ <Label
1195
+ htmlFor="auto-redistribute-installments-payable"
1196
+ className="text-xs text-muted-foreground"
1197
+ >
1198
+ {t('installmentsEditor.autoRedistributeLabel')}
1199
+ </Label>
1200
+ </div>
1201
+ <Button
1202
+ type="button"
1203
+ variant="outline"
1204
+ size="sm"
1205
+ onClick={() => {
1206
+ setIsInstallmentsEdited(false);
1207
+ replaceInstallments(
1208
+ buildEqualInstallments(
1209
+ form.getValues('installmentsCount'),
1210
+ form.getValues('valor'),
1211
+ form.getValues('vencimento')
1212
+ )
1213
+ );
1214
+ }}
1082
1215
  >
1083
- {t('installmentsEditor.autoRedistributeLabel')}
1084
- </Label>
1216
+ {t('installmentsEditor.recalculate')}
1217
+ </Button>
1085
1218
  </div>
1086
- <Button
1087
- type="button"
1088
- variant="outline"
1089
- size="sm"
1090
- onClick={() => {
1091
- setIsInstallmentsEdited(false);
1092
- replaceInstallments(
1093
- buildEqualInstallments(
1094
- form.getValues('installmentsCount'),
1095
- form.getValues('valor'),
1096
- form.getValues('vencimento')
1097
- )
1098
- );
1099
- }}
1100
- >
1101
- {t('installmentsEditor.recalculate')}
1102
- </Button>
1103
- </div>
1104
1219
 
1105
- {autoRedistributeInstallments && (
1106
- <p className="text-xs text-muted-foreground">
1107
- {t('installmentsEditor.autoRedistributeHint')}
1108
- </p>
1109
- )}
1220
+ {autoRedistributeInstallments && (
1221
+ <p className="text-xs text-muted-foreground">
1222
+ {t('installmentsEditor.autoRedistributeHint')}
1223
+ </p>
1224
+ )}
1110
1225
 
1111
- <div className="space-y-2">
1112
- {installmentFields.map((installment, index) => (
1113
- <div
1114
- key={installment.id}
1115
- className="grid grid-cols-1 items-start gap-3 rounded-lg border border-border/60 bg-background p-3 sm:grid-cols-[96px_1fr_180px]"
1116
- >
1117
- <div className="flex items-center text-sm text-muted-foreground">
1118
- #{index + 1}
1226
+ <div className="space-y-2">
1227
+ {installmentFields.map((installment, index) => (
1228
+ <div
1229
+ key={installment.id}
1230
+ className="grid grid-cols-1 items-start gap-3 rounded-lg border border-border/60 bg-background p-3 sm:grid-cols-[96px_1fr_180px]"
1231
+ >
1232
+ <div className="flex items-center text-sm text-muted-foreground">
1233
+ #{index + 1}
1234
+ </div>
1235
+
1236
+ <FormField
1237
+ control={form.control}
1238
+ name={`installments.${index}.dueDate` as const}
1239
+ render={({ field }) => (
1240
+ <FormItem>
1241
+ <FormLabel className="text-xs">
1242
+ {t('installmentsEditor.dueDateLabel')}
1243
+ </FormLabel>
1244
+ <FormControl>
1245
+ <Input
1246
+ type="date"
1247
+ {...field}
1248
+ value={field.value || ''}
1249
+ onChange={(event) => {
1250
+ setIsInstallmentsEdited(true);
1251
+ field.onChange(event);
1252
+ }}
1253
+ />
1254
+ </FormControl>
1255
+ <FormMessage />
1256
+ </FormItem>
1257
+ )}
1258
+ />
1259
+
1260
+ <FormField
1261
+ control={form.control}
1262
+ name={`installments.${index}.amount` as const}
1263
+ render={({ field }) => (
1264
+ <FormItem>
1265
+ <FormLabel className="text-xs">
1266
+ {t('installmentsEditor.amountLabel')}
1267
+ </FormLabel>
1268
+ <FormControl>
1269
+ <InputMoney
1270
+ ref={field.ref}
1271
+ name={field.name}
1272
+ value={field.value}
1273
+ onBlur={() => {
1274
+ field.onBlur();
1275
+
1276
+ if (!autoRedistributeInstallments) {
1277
+ return;
1278
+ }
1279
+
1280
+ clearScheduledRedistribution(index);
1281
+ runInstallmentRedistribution(index);
1282
+ }}
1283
+ onValueChange={(value) => {
1284
+ setIsInstallmentsEdited(true);
1285
+ field.onChange(value ?? 0);
1286
+
1287
+ if (!autoRedistributeInstallments) {
1288
+ return;
1289
+ }
1290
+
1291
+ scheduleInstallmentRedistribution(index);
1292
+ }}
1293
+ placeholder="0,00"
1294
+ />
1295
+ </FormControl>
1296
+ <FormMessage />
1297
+ </FormItem>
1298
+ )}
1299
+ />
1119
1300
  </div>
1301
+ ))}
1302
+ </div>
1120
1303
 
1121
- <FormField
1122
- control={form.control}
1123
- name={`installments.${index}.dueDate` as const}
1124
- render={({ field }) => (
1125
- <FormItem>
1126
- <FormLabel className="text-xs">
1127
- {t('installmentsEditor.dueDateLabel')}
1128
- </FormLabel>
1129
- <FormControl>
1130
- <Input
1131
- type="date"
1132
- {...field}
1133
- value={field.value || ''}
1134
- onChange={(event) => {
1135
- setIsInstallmentsEdited(true);
1136
- field.onChange(event);
1137
- }}
1138
- />
1139
- </FormControl>
1140
- <FormMessage />
1141
- </FormItem>
1142
- )}
1143
- />
1304
+ <p
1305
+ className={`text-xs ${
1306
+ installmentsDiffCents === 0
1307
+ ? 'text-muted-foreground'
1308
+ : 'text-destructive'
1309
+ }`}
1310
+ >
1311
+ {t('installmentsEditor.totalPrefix', {
1312
+ total: installmentsTotal.toFixed(2),
1313
+ })}
1314
+ {installmentsDiffCents > 0 &&
1315
+ ` ${t('installmentsEditor.adjustmentNeeded')}`}
1316
+ </p>
1317
+ {form.formState.errors.installments?.message && (
1318
+ <p className="text-xs text-destructive">
1319
+ {form.formState.errors.installments.message}
1320
+ </p>
1321
+ )}
1322
+ </div>
1323
+ )}
1144
1324
 
1145
- <FormField
1146
- control={form.control}
1147
- name={`installments.${index}.amount` as const}
1148
- render={({ field }) => (
1149
- <FormItem>
1150
- <FormLabel className="text-xs">
1151
- {t('installmentsEditor.amountLabel')}
1152
- </FormLabel>
1325
+ {isRecorrente && (
1326
+ <div className="space-y-3 rounded-md border p-3">
1327
+ <p className="text-sm font-medium">
1328
+ {t('recurrenceEditor.title')}
1329
+ </p>
1330
+ <div className="grid gap-4 md:grid-cols-3">
1331
+ <FormField
1332
+ control={form.control}
1333
+ name="recorrencia.frequencia"
1334
+ render={({ field }) => (
1335
+ <FormItem>
1336
+ <FormLabel>
1337
+ {t('recurrenceEditor.frequencyLabel')}
1338
+ </FormLabel>
1339
+ <Select
1340
+ value={field.value}
1341
+ onValueChange={field.onChange}
1342
+ >
1153
1343
  <FormControl>
1154
- <InputMoney
1155
- ref={field.ref}
1156
- name={field.name}
1157
- value={field.value}
1158
- onBlur={() => {
1159
- field.onBlur();
1160
-
1161
- if (!autoRedistributeInstallments) {
1162
- return;
1163
- }
1164
-
1165
- clearScheduledRedistribution(index);
1166
- runInstallmentRedistribution(index);
1167
- }}
1168
- onValueChange={(value) => {
1169
- setIsInstallmentsEdited(true);
1170
- field.onChange(value ?? 0);
1171
-
1172
- if (!autoRedistributeInstallments) {
1173
- return;
1174
- }
1175
-
1176
- scheduleInstallmentRedistribution(index);
1177
- }}
1178
- placeholder="0,00"
1179
- />
1344
+ <SelectTrigger className="w-full">
1345
+ <SelectValue />
1346
+ </SelectTrigger>
1180
1347
  </FormControl>
1181
- <FormMessage />
1182
- </FormItem>
1183
- )}
1184
- />
1185
- </div>
1186
- ))}
1187
- </div>
1348
+ <SelectContent>
1349
+ <SelectItem value="weekly">
1350
+ {t('recurrenceEditor.frequencies.weekly')}
1351
+ </SelectItem>
1352
+ <SelectItem value="biweekly">
1353
+ {t('recurrenceEditor.frequencies.biweekly')}
1354
+ </SelectItem>
1355
+ <SelectItem value="monthly">
1356
+ {t('recurrenceEditor.frequencies.monthly')}
1357
+ </SelectItem>
1358
+ <SelectItem value="bimonthly">
1359
+ {t('recurrenceEditor.frequencies.bimonthly')}
1360
+ </SelectItem>
1361
+ <SelectItem value="quarterly">
1362
+ {t('recurrenceEditor.frequencies.quarterly')}
1363
+ </SelectItem>
1364
+ <SelectItem value="semiannual">
1365
+ {t('recurrenceEditor.frequencies.semiannual')}
1366
+ </SelectItem>
1367
+ <SelectItem value="annual">
1368
+ {t('recurrenceEditor.frequencies.annual')}
1369
+ </SelectItem>
1370
+ </SelectContent>
1371
+ </Select>
1372
+ <FormMessage />
1373
+ </FormItem>
1374
+ )}
1375
+ />
1188
1376
 
1189
- <p
1190
- className={`text-xs ${
1191
- installmentsDiffCents === 0
1192
- ? 'text-muted-foreground'
1193
- : 'text-destructive'
1194
- }`}
1195
- >
1196
- {t('installmentsEditor.totalPrefix', {
1197
- total: installmentsTotal.toFixed(2),
1198
- })}
1199
- {installmentsDiffCents > 0 &&
1200
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
1201
- </p>
1202
- {form.formState.errors.installments?.message && (
1203
- <p className="text-xs text-destructive">
1204
- {form.formState.errors.installments.message}
1205
- </p>
1206
- )}
1207
- </div>
1377
+ <FormField
1378
+ control={form.control}
1379
+ name="recorrencia.dataFim"
1380
+ render={({ field }) => (
1381
+ <FormItem>
1382
+ <FormLabel>
1383
+ {t('recurrenceEditor.endDateLabel')}
1384
+ </FormLabel>
1385
+ <FormControl>
1386
+ <Input
1387
+ type="date"
1388
+ {...field}
1389
+ value={field.value || ''}
1390
+ />
1391
+ </FormControl>
1392
+ <FormMessage />
1393
+ </FormItem>
1394
+ )}
1395
+ />
1396
+
1397
+ <FormField
1398
+ control={form.control}
1399
+ name="recorrencia.numOcorrencias"
1400
+ render={({ field }) => (
1401
+ <FormItem>
1402
+ <FormLabel>
1403
+ {t('recurrenceEditor.maxOccurrencesLabel')}
1404
+ </FormLabel>
1405
+ <FormControl>
1406
+ <Input
1407
+ type="number"
1408
+ min={1}
1409
+ max={600}
1410
+ {...field}
1411
+ value={field.value ?? ''}
1412
+ onChange={(e) =>
1413
+ field.onChange(
1414
+ e.target.value === ''
1415
+ ? undefined
1416
+ : Number(e.target.value)
1417
+ )
1418
+ }
1419
+ />
1420
+ </FormControl>
1421
+ <FormMessage />
1422
+ </FormItem>
1423
+ )}
1424
+ />
1425
+ </div>
1426
+ {form.formState.errors.recorrencia?.message && (
1427
+ <p className="text-xs text-destructive">
1428
+ {form.formState.errors.recorrencia.message}
1429
+ </p>
1430
+ )}
1431
+ </div>
1432
+ )}
1208
1433
  </FinanceSheetSection>
1209
1434
 
1210
1435
  <FinanceSheetSection
@@ -1333,7 +1558,7 @@ function NovoTituloSheet({
1333
1558
  </div>
1334
1559
  </form>
1335
1560
  </Form>
1336
- </SheetContent>
1561
+ </ResizableSheetContent>
1337
1562
  </Sheet>
1338
1563
  );
1339
1564
  }
@@ -1401,6 +1626,7 @@ function EditarTituloSheet({
1401
1626
  const form = useForm<NewTitleFormValues>({
1402
1627
  resolver: zodResolver(newTitleFormSchema),
1403
1628
  defaultValues: {
1629
+ tipoTitulo: 'parcelado',
1404
1630
  documento: '',
1405
1631
  fornecedorId: '',
1406
1632
  competencia: '',
@@ -1408,6 +1634,11 @@ function EditarTituloSheet({
1408
1634
  valor: 0,
1409
1635
  installmentsCount: 1,
1410
1636
  installments: [{ dueDate: '', amount: 0 }],
1637
+ recorrencia: {
1638
+ frequencia: 'monthly',
1639
+ dataFim: '',
1640
+ numOcorrencias: undefined,
1641
+ },
1411
1642
  categoriaId: '',
1412
1643
  centroCustoId: '',
1413
1644
  metodo: '',
@@ -1425,6 +1656,9 @@ function EditarTituloSheet({
1425
1656
  control: form.control,
1426
1657
  });
1427
1658
 
1659
+ const tipoTituloEdit = form.watch('tipoTitulo');
1660
+ const isRecorrenteEdit = tipoTituloEdit === 'recorrente';
1661
+
1428
1662
  const {
1429
1663
  clearDraft,
1430
1664
  loadDraft,
@@ -1436,6 +1670,7 @@ function EditarTituloSheet({
1436
1670
  mode: 'edit',
1437
1671
  titleId: titulo?.id ? String(titulo.id) : null,
1438
1672
  values: {
1673
+ tipoTitulo: watchedFormValues.tipoTitulo ?? 'parcelado',
1439
1674
  documento: watchedFormValues.documento ?? '',
1440
1675
  fornecedorId: watchedFormValues.fornecedorId ?? '',
1441
1676
  competencia: watchedFormValues.competencia ?? '',
@@ -1446,6 +1681,11 @@ function EditarTituloSheet({
1446
1681
  dueDate: installment?.dueDate ?? '',
1447
1682
  amount: Number(installment?.amount ?? 0),
1448
1683
  })) ?? [{ dueDate: '', amount: 0 }],
1684
+ recorrencia: {
1685
+ frequencia: watchedFormValues.recorrencia?.frequencia ?? 'monthly',
1686
+ dataFim: watchedFormValues.recorrencia?.dataFim ?? '',
1687
+ numOcorrencias: watchedFormValues.recorrencia?.numOcorrencias,
1688
+ },
1449
1689
  categoriaId: watchedFormValues.categoriaId ?? '',
1450
1690
  centroCustoId: watchedFormValues.centroCustoId ?? '',
1451
1691
  metodo: watchedFormValues.metodo ?? '',
@@ -1499,7 +1739,9 @@ function EditarTituloSheet({
1499
1739
  currentLocaleCode
1500
1740
  );
1501
1741
 
1502
- return t('draftStatus', { relativeLabel, absoluteLabel });
1742
+ return t.has('draftStatus')
1743
+ ? t('draftStatus', { relativeLabel, absoluteLabel })
1744
+ : `${relativeLabel} - ${absoluteLabel}`;
1503
1745
  }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft, t]);
1504
1746
 
1505
1747
  const watchedInstallmentsCount = form.watch('installmentsCount');
@@ -1562,15 +1804,24 @@ function EditarTituloSheet({
1562
1804
  },
1563
1805
  ];
1564
1806
 
1807
+ const isRecurringTitle = Boolean(titulo.isRecurring);
1565
1808
  form.reset({
1809
+ tipoTitulo: isRecurringTitle ? 'recorrente' : 'parcelado',
1566
1810
  documento: titulo.documento || '',
1567
1811
  fornecedorId: titulo.fornecedorId || '',
1568
1812
  competencia: titulo.competencia || '',
1569
1813
  vencimento:
1570
1814
  normalizedInstallments[0]?.dueDate || toDateInput(titulo?.vencimento),
1571
- valor: Number(titulo.valorTotal || 0),
1815
+ valor: isRecurringTitle
1816
+ ? Number(normalizedInstallments[0]?.amount || 0)
1817
+ : Number(titulo.valorTotal || 0),
1572
1818
  installmentsCount: normalizedInstallments.length,
1573
1819
  installments: normalizedInstallments,
1820
+ recorrencia: {
1821
+ frequencia: titulo.recurrenceFrequency || 'monthly',
1822
+ dataFim: titulo.recurrenceEndDate || '',
1823
+ numOcorrencias: undefined,
1824
+ },
1574
1825
  categoriaId: titulo.categoriaId || '',
1575
1826
  centroCustoId: titulo.centroCustoId || '',
1576
1827
  metodo: installments[0]?.metodoPagamento || '',
@@ -1706,6 +1957,7 @@ function EditarTituloSheet({
1706
1957
  }
1707
1958
 
1708
1959
  try {
1960
+ const recorrente = values.tipoTitulo === 'recorrente';
1709
1961
  await request({
1710
1962
  url: `/finance/accounts-payable/installments/${titulo.id}`,
1711
1963
  method: 'PATCH',
@@ -1725,11 +1977,22 @@ function EditarTituloSheet({
1725
1977
  : undefined,
1726
1978
  payment_channel: values.metodo || undefined,
1727
1979
  description: values.descricao?.trim() || undefined,
1728
- installments: values.installments.map((installment, index) => ({
1729
- installment_number: index + 1,
1730
- due_date: installment.dueDate || values.vencimento,
1731
- amount: installment.amount,
1732
- })),
1980
+ ...(recorrente
1981
+ ? {
1982
+ recurrence_rule: {
1983
+ frequency: values.recorrencia?.frequencia ?? 'monthly',
1984
+ end_date: values.recorrencia?.dataFim || undefined,
1985
+ max_occurrences:
1986
+ values.recorrencia?.numOcorrencias || undefined,
1987
+ },
1988
+ }
1989
+ : {
1990
+ installments: values.installments.map((installment, index) => ({
1991
+ installment_number: index + 1,
1992
+ due_date: installment.dueDate || values.vencimento,
1993
+ amount: installment.amount,
1994
+ })),
1995
+ }),
1733
1996
  attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
1734
1997
  },
1735
1998
  });
@@ -1738,8 +2001,11 @@ function EditarTituloSheet({
1738
2001
  await onUpdated();
1739
2002
  showToastHandler?.('success', t('messages.updateSuccess'));
1740
2003
  onOpenChange(false);
1741
- } catch {
1742
- showToastHandler?.('error', t('messages.updateError'));
2004
+ } catch (error) {
2005
+ showToastHandler?.(
2006
+ 'error',
2007
+ getApiErrorMessage(error, t('messages.updateError'))
2008
+ );
1743
2009
  }
1744
2010
  };
1745
2011
 
@@ -1747,6 +2013,13 @@ function EditarTituloSheet({
1747
2013
  onOpenChange(false);
1748
2014
  };
1749
2015
 
2016
+ const handleInvalidSubmit = (errors: unknown) => {
2017
+ showToastHandler?.(
2018
+ 'error',
2019
+ getFirstFormErrorMessage(errors, t('messages.updateError'))
2020
+ );
2021
+ };
2022
+
1750
2023
  const clearUploadedFile = () => {
1751
2024
  setUploadedFileId(null);
1752
2025
  setUploadedFileName('');
@@ -1912,7 +2185,11 @@ function EditarTituloSheet({
1912
2185
 
1913
2186
  return (
1914
2187
  <Sheet open={open} onOpenChange={onOpenChange}>
1915
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
2188
+ <ResizableSheetContent
2189
+ sheetId="finance-payable-edit-title"
2190
+ defaultWidth={896}
2191
+ className="flex h-full w-full flex-col overflow-hidden p-0"
2192
+ >
1916
2193
  <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
1917
2194
  <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1918
2195
  <SheetDescription>{t('editTitle.description')}</SheetDescription>
@@ -1920,7 +2197,7 @@ function EditarTituloSheet({
1920
2197
  <Form {...form}>
1921
2198
  <form
1922
2199
  className="flex h-full flex-col overflow-hidden"
1923
- onSubmit={form.handleSubmit(handleSubmit)}
2200
+ onSubmit={form.handleSubmit(handleSubmit, handleInvalidSubmit)}
1924
2201
  >
1925
2202
  <FinanceSheetBody>
1926
2203
  <FinanceSheetSection
@@ -2081,6 +2358,38 @@ function EditarTituloSheet({
2081
2358
  selectPlaceholder={t('common.select')}
2082
2359
  />
2083
2360
 
2361
+ <FormField
2362
+ control={form.control}
2363
+ name="tipoTitulo"
2364
+ render={({ field }) => (
2365
+ <FormItem>
2366
+ <FormLabel>{t('titleType.label')}</FormLabel>
2367
+ <div className="flex gap-2">
2368
+ <Button
2369
+ type="button"
2370
+ variant={
2371
+ field.value === 'parcelado' ? 'default' : 'outline'
2372
+ }
2373
+ size="sm"
2374
+ onClick={() => field.onChange('parcelado')}
2375
+ >
2376
+ {t('titleType.installment')}
2377
+ </Button>
2378
+ <Button
2379
+ type="button"
2380
+ variant={
2381
+ field.value === 'recorrente' ? 'default' : 'outline'
2382
+ }
2383
+ size="sm"
2384
+ onClick={() => field.onChange('recorrente')}
2385
+ >
2386
+ {t('titleType.recurring')}
2387
+ </Button>
2388
+ </div>
2389
+ </FormItem>
2390
+ )}
2391
+ />
2392
+
2084
2393
  <div className="grid gap-4 xl:grid-cols-2">
2085
2394
  <div className="grid gap-4 md:grid-cols-2">
2086
2395
  <FormField
@@ -2126,7 +2435,11 @@ function EditarTituloSheet({
2126
2435
  name="valor"
2127
2436
  render={({ field }) => (
2128
2437
  <FormItem>
2129
- <FormLabel>{t('fields.totalValue')}</FormLabel>
2438
+ <FormLabel>
2439
+ {isRecorrenteEdit
2440
+ ? t('fields.totalValueRecurring')
2441
+ : t('fields.totalValue')}
2442
+ </FormLabel>
2130
2443
  <FormControl>
2131
2444
  <InputMoney
2132
2445
  ref={field.ref}
@@ -2144,35 +2457,37 @@ function EditarTituloSheet({
2144
2457
  )}
2145
2458
  />
2146
2459
 
2147
- <FormField
2148
- control={form.control}
2149
- name="installmentsCount"
2150
- render={({ field }) => (
2151
- <FormItem>
2152
- <FormLabel>
2153
- {t('installmentsEditor.countLabel')}
2154
- </FormLabel>
2155
- <FormControl>
2156
- <Input
2157
- type="number"
2158
- min={1}
2159
- max={120}
2160
- value={field.value}
2161
- onChange={(event) => {
2162
- const nextValue = Number(
2163
- event.target.value || 1
2164
- );
2165
- field.onChange(
2166
- Number.isNaN(nextValue) ? 1 : nextValue
2167
- );
2168
- setIsInstallmentsEdited(false);
2169
- }}
2170
- />
2171
- </FormControl>
2172
- <FormMessage />
2173
- </FormItem>
2174
- )}
2175
- />
2460
+ {!isRecorrenteEdit && (
2461
+ <FormField
2462
+ control={form.control}
2463
+ name="installmentsCount"
2464
+ render={({ field }) => (
2465
+ <FormItem>
2466
+ <FormLabel>
2467
+ {t('installmentsEditor.countLabel')}
2468
+ </FormLabel>
2469
+ <FormControl>
2470
+ <Input
2471
+ type="number"
2472
+ min={1}
2473
+ max={120}
2474
+ value={field.value}
2475
+ onChange={(event) => {
2476
+ const nextValue = Number(
2477
+ event.target.value || 1
2478
+ );
2479
+ field.onChange(
2480
+ Number.isNaN(nextValue) ? 1 : nextValue
2481
+ );
2482
+ setIsInstallmentsEdited(false);
2483
+ }}
2484
+ />
2485
+ </FormControl>
2486
+ <FormMessage />
2487
+ </FormItem>
2488
+ )}
2489
+ />
2490
+ )}
2176
2491
  </div>
2177
2492
  </div>
2178
2493
  </FinanceSheetSection>
@@ -2181,145 +2496,256 @@ function EditarTituloSheet({
2181
2496
  title={t('sections.installments.title')}
2182
2497
  description={t('sections.installments.description')}
2183
2498
  >
2184
- <div className="space-y-3">
2185
- <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
2186
- <div className="flex items-center gap-2">
2187
- <Checkbox
2188
- id="auto-redistribute-installments-edit-payable"
2189
- checked={autoRedistributeInstallments}
2190
- onCheckedChange={(checked) =>
2191
- setAutoRedistributeInstallments(checked === true)
2192
- }
2193
- />
2194
- <Label
2195
- htmlFor="auto-redistribute-installments-edit-payable"
2196
- className="text-xs text-muted-foreground"
2499
+ {!isRecorrenteEdit && (
2500
+ <div className="space-y-3">
2501
+ <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
2502
+ <div className="flex items-center gap-2">
2503
+ <Checkbox
2504
+ id="auto-redistribute-installments-edit-payable"
2505
+ checked={autoRedistributeInstallments}
2506
+ onCheckedChange={(checked) =>
2507
+ setAutoRedistributeInstallments(checked === true)
2508
+ }
2509
+ />
2510
+ <Label
2511
+ htmlFor="auto-redistribute-installments-edit-payable"
2512
+ className="text-xs text-muted-foreground"
2513
+ >
2514
+ {t('installmentsEditor.autoRedistributeLabel')}
2515
+ </Label>
2516
+ </div>
2517
+ <Button
2518
+ type="button"
2519
+ variant="outline"
2520
+ size="sm"
2521
+ onClick={() => {
2522
+ setIsInstallmentsEdited(false);
2523
+ replaceInstallments(
2524
+ buildEqualInstallments(
2525
+ form.getValues('installmentsCount'),
2526
+ form.getValues('valor'),
2527
+ form.getValues('vencimento')
2528
+ )
2529
+ );
2530
+ }}
2197
2531
  >
2198
- {t('installmentsEditor.autoRedistributeLabel')}
2199
- </Label>
2532
+ {t('installmentsEditor.recalculate')}
2533
+ </Button>
2200
2534
  </div>
2201
- <Button
2202
- type="button"
2203
- variant="outline"
2204
- size="sm"
2205
- onClick={() => {
2206
- setIsInstallmentsEdited(false);
2207
- replaceInstallments(
2208
- buildEqualInstallments(
2209
- form.getValues('installmentsCount'),
2210
- form.getValues('valor'),
2211
- form.getValues('vencimento')
2212
- )
2213
- );
2214
- }}
2215
- >
2216
- {t('installmentsEditor.recalculate')}
2217
- </Button>
2218
- </div>
2219
2535
 
2220
- {autoRedistributeInstallments && (
2221
- <p className="text-xs text-muted-foreground">
2222
- {t('installmentsEditor.autoRedistributeHint')}
2223
- </p>
2224
- )}
2536
+ {autoRedistributeInstallments && (
2537
+ <p className="text-xs text-muted-foreground">
2538
+ {t('installmentsEditor.autoRedistributeHint')}
2539
+ </p>
2540
+ )}
2225
2541
 
2226
- <div className="space-y-2">
2227
- {installmentFields.map((installment, index) => (
2228
- <div
2229
- key={installment.id}
2230
- className="grid grid-cols-1 items-start gap-3 rounded-lg border border-border/60 bg-background p-3 sm:grid-cols-[96px_1fr_180px]"
2231
- >
2232
- <div className="flex items-center text-sm text-muted-foreground">
2233
- #{index + 1}
2542
+ <div className="space-y-2">
2543
+ {installmentFields.map((installment, index) => (
2544
+ <div
2545
+ key={installment.id}
2546
+ className="grid grid-cols-1 items-start gap-3 rounded-lg border border-border/60 bg-background p-3 sm:grid-cols-[96px_1fr_180px]"
2547
+ >
2548
+ <div className="flex items-center text-sm text-muted-foreground">
2549
+ #{index + 1}
2550
+ </div>
2551
+
2552
+ <FormField
2553
+ control={form.control}
2554
+ name={`installments.${index}.dueDate` as const}
2555
+ render={({ field }) => (
2556
+ <FormItem>
2557
+ <FormLabel className="text-xs">
2558
+ {t('installmentsEditor.dueDateLabel')}
2559
+ </FormLabel>
2560
+ <FormControl>
2561
+ <Input
2562
+ type="date"
2563
+ {...field}
2564
+ value={field.value || ''}
2565
+ onChange={(event) => {
2566
+ setIsInstallmentsEdited(true);
2567
+ field.onChange(event);
2568
+ }}
2569
+ />
2570
+ </FormControl>
2571
+ <FormMessage />
2572
+ </FormItem>
2573
+ )}
2574
+ />
2575
+
2576
+ <FormField
2577
+ control={form.control}
2578
+ name={`installments.${index}.amount` as const}
2579
+ render={({ field }) => (
2580
+ <FormItem>
2581
+ <FormLabel className="text-xs">
2582
+ {t('installmentsEditor.amountLabel')}
2583
+ </FormLabel>
2584
+ <FormControl>
2585
+ <InputMoney
2586
+ ref={field.ref}
2587
+ name={field.name}
2588
+ value={field.value}
2589
+ onBlur={() => {
2590
+ field.onBlur();
2591
+
2592
+ if (!autoRedistributeInstallments) {
2593
+ return;
2594
+ }
2595
+
2596
+ clearScheduledRedistribution(index);
2597
+ runInstallmentRedistribution(index);
2598
+ }}
2599
+ onValueChange={(value) => {
2600
+ setIsInstallmentsEdited(true);
2601
+ field.onChange(value ?? 0);
2602
+
2603
+ if (!autoRedistributeInstallments) {
2604
+ return;
2605
+ }
2606
+
2607
+ scheduleInstallmentRedistribution(index);
2608
+ }}
2609
+ placeholder="0,00"
2610
+ />
2611
+ </FormControl>
2612
+ <FormMessage />
2613
+ </FormItem>
2614
+ )}
2615
+ />
2234
2616
  </div>
2617
+ ))}
2618
+ </div>
2235
2619
 
2236
- <FormField
2237
- control={form.control}
2238
- name={`installments.${index}.dueDate` as const}
2239
- render={({ field }) => (
2240
- <FormItem>
2241
- <FormLabel className="text-xs">
2242
- {t('installmentsEditor.dueDateLabel')}
2243
- </FormLabel>
2244
- <FormControl>
2245
- <Input
2246
- type="date"
2247
- {...field}
2248
- value={field.value || ''}
2249
- onChange={(event) => {
2250
- setIsInstallmentsEdited(true);
2251
- field.onChange(event);
2252
- }}
2253
- />
2254
- </FormControl>
2255
- <FormMessage />
2256
- </FormItem>
2257
- )}
2258
- />
2620
+ <p
2621
+ className={`text-xs ${
2622
+ installmentsDiffCents === 0
2623
+ ? 'text-muted-foreground'
2624
+ : 'text-destructive'
2625
+ }`}
2626
+ >
2627
+ {t('installmentsEditor.totalPrefix', {
2628
+ total: installmentsTotal.toFixed(2),
2629
+ })}
2630
+ {installmentsDiffCents > 0 &&
2631
+ ` ${t('installmentsEditor.adjustmentNeeded')}`}
2632
+ </p>
2633
+ {form.formState.errors.installments?.message && (
2634
+ <p className="text-xs text-destructive">
2635
+ {form.formState.errors.installments.message}
2636
+ </p>
2637
+ )}
2638
+ </div>
2639
+ )}
2259
2640
 
2260
- <FormField
2261
- control={form.control}
2262
- name={`installments.${index}.amount` as const}
2263
- render={({ field }) => (
2264
- <FormItem>
2265
- <FormLabel className="text-xs">
2266
- {t('installmentsEditor.amountLabel')}
2267
- </FormLabel>
2641
+ {isRecorrenteEdit && (
2642
+ <div className="space-y-3 rounded-md border p-3">
2643
+ <p className="text-sm font-medium">
2644
+ {t('recurrenceEditor.title')}
2645
+ </p>
2646
+ <div className="grid gap-4 md:grid-cols-3">
2647
+ <FormField
2648
+ control={form.control}
2649
+ name="recorrencia.frequencia"
2650
+ render={({ field }) => (
2651
+ <FormItem>
2652
+ <FormLabel>
2653
+ {t('recurrenceEditor.frequencyLabel')}
2654
+ </FormLabel>
2655
+ <Select
2656
+ value={field.value}
2657
+ onValueChange={field.onChange}
2658
+ >
2268
2659
  <FormControl>
2269
- <InputMoney
2270
- ref={field.ref}
2271
- name={field.name}
2272
- value={field.value}
2273
- onBlur={() => {
2274
- field.onBlur();
2275
-
2276
- if (!autoRedistributeInstallments) {
2277
- return;
2278
- }
2279
-
2280
- clearScheduledRedistribution(index);
2281
- runInstallmentRedistribution(index);
2282
- }}
2283
- onValueChange={(value) => {
2284
- setIsInstallmentsEdited(true);
2285
- field.onChange(value ?? 0);
2286
-
2287
- if (!autoRedistributeInstallments) {
2288
- return;
2289
- }
2290
-
2291
- scheduleInstallmentRedistribution(index);
2292
- }}
2293
- placeholder="0,00"
2294
- />
2660
+ <SelectTrigger className="w-full">
2661
+ <SelectValue />
2662
+ </SelectTrigger>
2295
2663
  </FormControl>
2296
- <FormMessage />
2297
- </FormItem>
2298
- )}
2299
- />
2300
- </div>
2301
- ))}
2302
- </div>
2664
+ <SelectContent>
2665
+ <SelectItem value="weekly">
2666
+ {t('recurrenceEditor.frequencies.weekly')}
2667
+ </SelectItem>
2668
+ <SelectItem value="biweekly">
2669
+ {t('recurrenceEditor.frequencies.biweekly')}
2670
+ </SelectItem>
2671
+ <SelectItem value="monthly">
2672
+ {t('recurrenceEditor.frequencies.monthly')}
2673
+ </SelectItem>
2674
+ <SelectItem value="bimonthly">
2675
+ {t('recurrenceEditor.frequencies.bimonthly')}
2676
+ </SelectItem>
2677
+ <SelectItem value="quarterly">
2678
+ {t('recurrenceEditor.frequencies.quarterly')}
2679
+ </SelectItem>
2680
+ <SelectItem value="semiannual">
2681
+ {t('recurrenceEditor.frequencies.semiannual')}
2682
+ </SelectItem>
2683
+ <SelectItem value="annual">
2684
+ {t('recurrenceEditor.frequencies.annual')}
2685
+ </SelectItem>
2686
+ </SelectContent>
2687
+ </Select>
2688
+ <FormMessage />
2689
+ </FormItem>
2690
+ )}
2691
+ />
2303
2692
 
2304
- <p
2305
- className={`text-xs ${
2306
- installmentsDiffCents === 0
2307
- ? 'text-muted-foreground'
2308
- : 'text-destructive'
2309
- }`}
2310
- >
2311
- {t('installmentsEditor.totalPrefix', {
2312
- total: installmentsTotal.toFixed(2),
2313
- })}
2314
- {installmentsDiffCents > 0 &&
2315
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
2316
- </p>
2317
- {form.formState.errors.installments?.message && (
2318
- <p className="text-xs text-destructive">
2319
- {form.formState.errors.installments.message}
2320
- </p>
2321
- )}
2322
- </div>
2693
+ <FormField
2694
+ control={form.control}
2695
+ name="recorrencia.dataFim"
2696
+ render={({ field }) => (
2697
+ <FormItem>
2698
+ <FormLabel>
2699
+ {t('recurrenceEditor.endDateLabel')}
2700
+ </FormLabel>
2701
+ <FormControl>
2702
+ <Input
2703
+ type="date"
2704
+ {...field}
2705
+ value={field.value || ''}
2706
+ />
2707
+ </FormControl>
2708
+ <FormMessage />
2709
+ </FormItem>
2710
+ )}
2711
+ />
2712
+
2713
+ <FormField
2714
+ control={form.control}
2715
+ name="recorrencia.numOcorrencias"
2716
+ render={({ field }) => (
2717
+ <FormItem>
2718
+ <FormLabel>
2719
+ {t('recurrenceEditor.maxOccurrencesLabel')}
2720
+ </FormLabel>
2721
+ <FormControl>
2722
+ <Input
2723
+ type="number"
2724
+ min={1}
2725
+ max={600}
2726
+ {...field}
2727
+ value={field.value ?? ''}
2728
+ onChange={(e) =>
2729
+ field.onChange(
2730
+ e.target.value === ''
2731
+ ? undefined
2732
+ : Number(e.target.value)
2733
+ )
2734
+ }
2735
+ />
2736
+ </FormControl>
2737
+ <FormMessage />
2738
+ </FormItem>
2739
+ )}
2740
+ />
2741
+ </div>
2742
+ {form.formState.errors.recorrencia?.message && (
2743
+ <p className="text-xs text-destructive">
2744
+ {form.formState.errors.recorrencia.message}
2745
+ </p>
2746
+ )}
2747
+ </div>
2748
+ )}
2323
2749
  </FinanceSheetSection>
2324
2750
 
2325
2751
  <FinanceSheetSection
@@ -2448,7 +2874,7 @@ function EditarTituloSheet({
2448
2874
  </div>
2449
2875
  </form>
2450
2876
  </Form>
2451
- </SheetContent>
2877
+ </ResizableSheetContent>
2452
2878
  </Sheet>
2453
2879
  );
2454
2880
  }
@@ -2581,6 +3007,8 @@ export default function TitulosPagarPage() {
2581
3007
  placeholderData: (old) => old,
2582
3008
  });
2583
3009
 
3010
+ useFinanceRealtimeRefresh({ request, onRefresh: refetchTitles });
3011
+
2584
3012
  const titulosPagar = paginatedTitlesResponse?.data || [];
2585
3013
  const visibleTitlesTotal = useMemo(
2586
3014
  () =>
@@ -2981,7 +3409,11 @@ export default function TitulosPagarPage() {
2981
3409
  }
2982
3410
  }}
2983
3411
  >
2984
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl">
3412
+ <ResizableSheetContent
3413
+ sheetId="finance-payable-settle"
3414
+ defaultWidth={576}
3415
+ className="flex h-full w-full flex-col overflow-hidden p-0"
3416
+ >
2985
3417
  <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
2986
3418
  <SheetTitle>{t('settleSheet.title')}</SheetTitle>
2987
3419
  <SheetDescription>
@@ -3115,7 +3547,7 @@ export default function TitulosPagarPage() {
3115
3547
  </div>
3116
3548
  </form>
3117
3549
  </Form>
3118
- </SheetContent>
3550
+ </ResizableSheetContent>
3119
3551
  </Sheet>
3120
3552
 
3121
3553
  <FinancePageSection variant="flat">