@hed-hog/finance 0.0.300 → 0.0.301

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 (39) hide show
  1. package/dist/finance.contract-activated.subscriber.d.ts +24 -0
  2. package/dist/finance.contract-activated.subscriber.d.ts.map +1 -0
  3. package/dist/finance.contract-activated.subscriber.js +519 -0
  4. package/dist/finance.contract-activated.subscriber.js.map +1 -0
  5. package/dist/finance.contract-activated.subscriber.spec.d.ts +2 -0
  6. package/dist/finance.contract-activated.subscriber.spec.d.ts.map +1 -0
  7. package/dist/finance.contract-activated.subscriber.spec.js +302 -0
  8. package/dist/finance.contract-activated.subscriber.spec.js.map +1 -0
  9. package/dist/finance.module.d.ts.map +1 -1
  10. package/dist/finance.module.js +6 -1
  11. package/dist/finance.module.js.map +1 -1
  12. package/hedhog/frontend/app/_components/finance-layout.tsx.ejs +108 -0
  13. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +91 -106
  14. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1306 -1145
  15. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +288 -268
  16. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +491 -351
  17. package/hedhog/frontend/app/administration/audit-logs/page.tsx.ejs +157 -173
  18. package/hedhog/frontend/app/administration/categories/page.tsx.ejs +44 -62
  19. package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +62 -80
  20. package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +151 -170
  21. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +332 -286
  22. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +204 -226
  23. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +122 -140
  24. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +32 -49
  25. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +84 -108
  26. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +53 -70
  27. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +98 -95
  28. package/hedhog/frontend/app/reports/actual-vs-forecast/page.tsx.ejs +100 -125
  29. package/hedhog/frontend/app/reports/aging-default/page.tsx.ejs +77 -105
  30. package/hedhog/frontend/app/reports/cash-position/page.tsx.ejs +99 -134
  31. package/hedhog/frontend/app/reports/overview-results/page.tsx.ejs +147 -182
  32. package/hedhog/frontend/app/reports/top-customers/page.tsx.ejs +49 -61
  33. package/hedhog/frontend/app/reports/top-operational-expenses/page.tsx.ejs +49 -67
  34. package/hedhog/frontend/messages/en.json +176 -68
  35. package/hedhog/frontend/messages/pt.json +176 -68
  36. package/package.json +7 -6
  37. package/src/finance.contract-activated.subscriber.spec.ts +392 -0
  38. package/src/finance.contract-activated.subscriber.ts +780 -0
  39. package/src/finance.module.ts +6 -1
@@ -1,28 +1,39 @@
1
1
  'use client';
2
2
 
3
- import {
4
- CategoryFieldWithCreate,
5
- CostCenterFieldWithCreate,
6
- } from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
7
- import { FinanceTitleActionsMenu } from '@/app/(app)/(libraries)/finance/_components/finance-title-actions-menu';
8
- import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
9
- import { EmptyState, Page, PageHeader } from '@/components/entity-list';
10
- import { Button } from '@/components/ui/button';
11
- import { Checkbox } from '@/components/ui/checkbox';
12
- import { FilterBar } from '@/components/ui/filter-bar';
13
- import {
14
- Form,
15
- FormControl,
3
+ import {
4
+ CategoryFieldWithCreate,
5
+ CostCenterFieldWithCreate,
6
+ } from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
7
+ import {
8
+ FinancePageSection,
9
+ FinanceSheetBody,
10
+ FinanceSheetSection,
11
+ } from '@/app/(app)/(libraries)/finance/_components/finance-layout';
12
+ import { FinanceTitleActionsMenu } from '@/app/(app)/(libraries)/finance/_components/finance-title-actions-menu';
13
+ import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
14
+ import {
15
+ EmptyState,
16
+ Page,
17
+ PageHeader,
18
+ PaginationFooter,
19
+ SearchBar,
20
+ } from '@/components/entity-list';
21
+ import { Button } from '@/components/ui/button';
22
+ import { Checkbox } from '@/components/ui/checkbox';
23
+ import {
24
+ Form,
25
+ FormControl,
16
26
  FormField,
17
27
  FormItem,
18
28
  FormLabel,
19
- FormMessage,
20
- } from '@/components/ui/form';
21
- import { Input } from '@/components/ui/input';
22
- import { InputMoney } from '@/components/ui/input-money';
23
- import { Label } from '@/components/ui/label';
24
- import { Money } from '@/components/ui/money';
25
- import { Progress } from '@/components/ui/progress';
29
+ FormMessage,
30
+ } from '@/components/ui/form';
31
+ import { Input } from '@/components/ui/input';
32
+ import { InputMoney } from '@/components/ui/input-money';
33
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
34
+ import { Label } from '@/components/ui/label';
35
+ import { Money } from '@/components/ui/money';
36
+ import { Progress } from '@/components/ui/progress';
26
37
  import {
27
38
  Select,
28
39
  SelectContent,
@@ -53,16 +64,18 @@ import {
53
64
  TooltipContent,
54
65
  TooltipTrigger,
55
66
  } from '@/components/ui/tooltip';
56
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
57
- import { zodResolver } from '@hookform/resolvers/zod';
58
- import {
59
- FileText,
60
- Loader2,
61
- Paperclip,
62
- Plus,
63
- Trash2,
64
- Upload,
65
- } from 'lucide-react';
67
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
68
+ import { zodResolver } from '@hookform/resolvers/zod';
69
+ import {
70
+ AlertTriangle,
71
+ FileText,
72
+ Loader2,
73
+ Paperclip,
74
+ Plus,
75
+ Trash2,
76
+ Upload,
77
+ Wallet,
78
+ } from 'lucide-react';
66
79
  import { useTranslations } from 'next-intl';
67
80
  import Link from 'next/link';
68
81
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
@@ -649,37 +662,44 @@ function NovoTituloSheet({
649
662
  {t('newTitle.action')}
650
663
  </Button>
651
664
  </SheetTrigger>
652
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
653
- <SheetHeader>
654
- <SheetTitle>{t('newTitle.title')}</SheetTitle>
655
- <SheetDescription>{t('newTitle.description')}</SheetDescription>
656
- </SheetHeader>
657
- <Form {...form}>
658
- <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
659
- <div className="grid gap-3">
660
- <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
661
- <div className="grid gap-2">
662
- <FormLabel>{t('common.upload.label')}</FormLabel>
663
- <Input
664
- ref={fileInputRef}
665
- className="hidden"
666
- type="file"
667
- accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
668
- onChange={(event) => {
669
- const file = event.target.files?.[0];
670
- if (!file) {
671
- return;
672
- }
673
-
674
- clearUploadedFile();
675
- void uploadRelatedFile(file);
676
- }}
677
- disabled={
678
- isUploadingFile ||
679
- isExtractingFileData ||
680
- form.formState.isSubmitting
681
- }
682
- />
665
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
666
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
667
+ <SheetTitle>{t('newTitle.title')}</SheetTitle>
668
+ <SheetDescription>{t('newTitle.description')}</SheetDescription>
669
+ </SheetHeader>
670
+ <Form {...form}>
671
+ <form
672
+ className="flex h-full flex-col overflow-hidden"
673
+ onSubmit={form.handleSubmit(handleSubmit)}
674
+ >
675
+ <FinanceSheetBody>
676
+ <FinanceSheetSection
677
+ title={t('sections.main.title')}
678
+ description={t('sections.main.description')}
679
+ >
680
+ <div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
681
+ <div className="grid gap-2">
682
+ <FormLabel>{t('common.upload.label')}</FormLabel>
683
+ <Input
684
+ ref={fileInputRef}
685
+ className="hidden"
686
+ type="file"
687
+ accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
688
+ onChange={(event) => {
689
+ const file = event.target.files?.[0];
690
+ if (!file) {
691
+ return;
692
+ }
693
+
694
+ clearUploadedFile();
695
+ void uploadRelatedFile(file);
696
+ }}
697
+ disabled={
698
+ isUploadingFile ||
699
+ isExtractingFileData ||
700
+ form.formState.isSubmitting
701
+ }
702
+ />
683
703
 
684
704
  <div className="grid w-full grid-cols-2 gap-2">
685
705
  <Tooltip>
@@ -744,12 +764,12 @@ function NovoTituloSheet({
744
764
  )}
745
765
  </div>
746
766
 
747
- <div className="space-y-1">
748
- {uploadedFileId && (
749
- <p className="truncate text-xs text-muted-foreground">
750
- {t('common.upload.selectedPrefix')} {uploadedFileName}
751
- </p>
752
- )}
767
+ <div className="space-y-1 rounded-lg border border-dashed border-border/70 bg-muted/20 p-3">
768
+ {uploadedFileId && (
769
+ <p className="truncate text-xs text-muted-foreground">
770
+ {t('common.upload.selectedPrefix')} {uploadedFileName}
771
+ </p>
772
+ )}
753
773
 
754
774
  {isUploadingFile && !isExtractingFileData && (
755
775
  <div className="space-y-1">
@@ -779,364 +799,388 @@ function NovoTituloSheet({
779
799
  </p>
780
800
  )}
781
801
 
782
- {!isExtractingFileData && extractionWarnings.length > 0 && (
783
- <p className="truncate text-xs text-muted-foreground">
784
- {extractionWarnings[0]}
785
- </p>
786
- )}
787
- </div>
788
- </div>
789
-
790
- <FormField
791
- control={form.control}
792
- name="documento"
793
- render={({ field }) => (
794
- <FormItem>
795
- <FormLabel>{t('fields.document')}</FormLabel>
796
- <FormControl>
797
- <Input placeholder="NF-00000" {...field} />
798
- </FormControl>
799
- <FormMessage />
800
- </FormItem>
801
- )}
802
- />
803
- </div>
804
-
805
- <PersonFieldWithCreate
806
- form={form}
807
- name="fornecedorId"
808
- label={t('fields.supplier')}
809
- entityLabel="fornecedor"
810
- selectPlaceholder={t('common.select')}
811
- />
812
-
813
- <div className="grid grid-cols-2 items-start gap-3">
814
- <FormField
815
- control={form.control}
816
- name="competencia"
817
- render={({ field }) => (
818
- <FormItem>
819
- <FormLabel>{t('fields.competency')}</FormLabel>
820
- <FormControl>
821
- <Input
822
- type="month"
823
- {...field}
824
- value={field.value || ''}
825
- />
826
- </FormControl>
827
- <FormMessage />
828
- </FormItem>
829
- )}
830
- />
831
-
832
- <FormField
833
- control={form.control}
834
- name="vencimento"
835
- render={({ field }) => (
836
- <FormItem>
837
- <FormLabel>{t('fields.dueDate')}</FormLabel>
838
- <FormControl>
839
- <Input
840
- type="date"
841
- {...field}
842
- value={field.value || ''}
843
- />
844
- </FormControl>
845
- <FormMessage />
846
- </FormItem>
847
- )}
848
- />
849
- </div>
850
-
851
- <div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
852
- <FormField
853
- control={form.control}
854
- name="valor"
855
- render={({ field }) => (
856
- <FormItem>
857
- <FormLabel>{t('fields.totalValue')}</FormLabel>
858
- <FormControl>
859
- <InputMoney
860
- ref={field.ref}
861
- name={field.name}
862
- value={field.value}
863
- onBlur={field.onBlur}
864
- onValueChange={(value) => field.onChange(value ?? 0)}
865
- placeholder="0,00"
866
- />
867
- </FormControl>
868
- <FormMessage />
869
- </FormItem>
870
- )}
871
- />
872
-
873
- <FormField
874
- control={form.control}
875
- name="installmentsCount"
876
- render={({ field }) => (
877
- <FormItem>
878
- <FormLabel>
879
- {t('installmentsEditor.countLabel')}
880
- </FormLabel>
881
- <FormControl>
882
- <Input
883
- type="number"
884
- min={1}
885
- max={120}
886
- value={field.value}
887
- onChange={(event) => {
888
- const nextValue = Number(event.target.value || 1);
889
- field.onChange(
890
- Number.isNaN(nextValue) ? 1 : nextValue
891
- );
892
- }}
893
- />
894
- </FormControl>
895
- <FormMessage />
896
- </FormItem>
897
- )}
898
- />
899
- </div>
900
-
901
- <div className="space-y-3 rounded-md border p-3">
902
- <div className="flex items-center justify-between gap-2">
903
- <p className="text-sm font-medium">
904
- {t('installmentsEditor.title')}
905
- </p>
906
- <Button
907
- type="button"
908
- variant="outline"
909
- size="sm"
910
- onClick={() => {
911
- setIsInstallmentsEdited(false);
912
- replaceInstallments(
913
- buildEqualInstallments(
914
- form.getValues('installmentsCount'),
915
- form.getValues('valor'),
916
- form.getValues('vencimento')
917
- )
918
- );
919
- }}
920
- >
921
- {t('installmentsEditor.recalculate')}
922
- </Button>
923
- </div>
924
-
925
- <div className="flex items-center gap-2">
926
- <Checkbox
927
- id="auto-redistribute-installments-payable"
928
- checked={autoRedistributeInstallments}
929
- onCheckedChange={(checked) =>
930
- setAutoRedistributeInstallments(checked === true)
931
- }
932
- />
933
- <Label
934
- htmlFor="auto-redistribute-installments-payable"
935
- className="text-xs text-muted-foreground"
936
- >
937
- {t('installmentsEditor.autoRedistributeLabel')}
938
- </Label>
939
- </div>
940
- {autoRedistributeInstallments && (
941
- <p className="text-xs text-muted-foreground">
942
- {t('installmentsEditor.autoRedistributeHint')}
943
- </p>
944
- )}
945
-
946
- <div className="space-y-2">
947
- {installmentFields.map((installment, index) => (
948
- <div
949
- key={installment.id}
950
- className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
951
- >
952
- <div className="flex items-center text-sm text-muted-foreground">
953
- #{index + 1}
954
- </div>
955
-
956
- <FormField
957
- control={form.control}
958
- name={`installments.${index}.dueDate` as const}
959
- render={({ field }) => (
960
- <FormItem>
961
- <FormLabel className="text-xs">
962
- {t('installmentsEditor.dueDateLabel')}
963
- </FormLabel>
964
- <FormControl>
965
- <Input
966
- type="date"
967
- {...field}
968
- value={field.value || ''}
969
- onChange={(event) => {
970
- setIsInstallmentsEdited(true);
971
- field.onChange(event);
972
- }}
973
- />
974
- </FormControl>
975
- <FormMessage />
976
- </FormItem>
977
- )}
978
- />
979
-
980
- <FormField
981
- control={form.control}
982
- name={`installments.${index}.amount` as const}
983
- render={({ field }) => (
984
- <FormItem>
985
- <FormLabel className="text-xs">
986
- {t('installmentsEditor.amountLabel')}
987
- </FormLabel>
988
- <FormControl>
989
- <InputMoney
990
- ref={field.ref}
991
- name={field.name}
992
- value={field.value}
993
- onBlur={() => {
994
- field.onBlur();
995
-
996
- if (!autoRedistributeInstallments) {
997
- return;
998
- }
999
-
1000
- clearScheduledRedistribution(index);
1001
- runInstallmentRedistribution(index);
1002
- }}
1003
- onValueChange={(value) => {
1004
- setIsInstallmentsEdited(true);
1005
- field.onChange(value ?? 0);
1006
-
1007
- if (!autoRedistributeInstallments) {
1008
- return;
1009
- }
1010
-
1011
- scheduleInstallmentRedistribution(index);
1012
- }}
1013
- placeholder="0,00"
1014
- />
1015
- </FormControl>
1016
- <FormMessage />
1017
- </FormItem>
1018
- )}
1019
- />
1020
- </div>
1021
- ))}
1022
- </div>
1023
-
1024
- <p
1025
- className={`text-xs ${
1026
- installmentsDiffCents === 0
1027
- ? 'text-muted-foreground'
1028
- : 'text-destructive'
1029
- }`}
1030
- >
1031
- {t('installmentsEditor.totalPrefix', {
1032
- total: installmentsTotal.toFixed(2),
1033
- })}
1034
- {installmentsDiffCents > 0 &&
1035
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
1036
- </p>
1037
- {form.formState.errors.installments?.message && (
1038
- <p className="text-xs text-destructive">
1039
- {form.formState.errors.installments.message}
1040
- </p>
1041
- )}
1042
- </div>
1043
-
1044
- <CategoryFieldWithCreate
1045
- form={form}
1046
- name="categoriaId"
1047
- label={t('fields.category')}
1048
- selectPlaceholder={t('common.select')}
1049
- categories={categorias}
1050
- categoryKind="despesa"
1051
- onCreated={onCategoriesUpdated}
1052
- />
1053
-
1054
- <CostCenterFieldWithCreate
1055
- form={form}
1056
- name="centroCustoId"
1057
- label={t('fields.costCenter')}
1058
- selectPlaceholder={t('common.select')}
1059
- costCenters={centrosCusto}
1060
- onCreated={onCostCentersUpdated}
1061
- />
1062
-
1063
- <FormField
1064
- control={form.control}
1065
- name="metodo"
1066
- render={({ field }) => (
1067
- <FormItem>
1068
- <FormLabel>{t('fields.paymentMethod')}</FormLabel>
1069
- <Select value={field.value} onValueChange={field.onChange}>
1070
- <FormControl>
1071
- <SelectTrigger className="w-full">
1072
- <SelectValue placeholder={t('common.select')} />
1073
- </SelectTrigger>
1074
- </FormControl>
1075
- <SelectContent>
1076
- <SelectItem value="boleto">
1077
- {t('paymentMethods.boleto')}
1078
- </SelectItem>
1079
- <SelectItem value="pix">PIX</SelectItem>
1080
- <SelectItem value="transferencia">
1081
- {t('paymentMethods.transfer')}
1082
- </SelectItem>
1083
- <SelectItem value="cartao">
1084
- {t('paymentMethods.card')}
1085
- </SelectItem>
1086
- <SelectItem value="dinheiro">
1087
- {t('paymentMethods.cash')}
1088
- </SelectItem>
1089
- <SelectItem value="cheque">
1090
- {t('paymentMethods.check')}
1091
- </SelectItem>
1092
- </SelectContent>
1093
- </Select>
1094
- <FormMessage />
1095
- </FormItem>
1096
- )}
1097
- />
1098
-
1099
- <FormField
1100
- control={form.control}
1101
- name="descricao"
1102
- render={({ field }) => (
1103
- <FormItem>
1104
- <FormLabel>{t('fields.description')}</FormLabel>
1105
- <FormControl>
1106
- <Textarea
1107
- placeholder={t('newTitle.descriptionPlaceholder')}
1108
- {...field}
1109
- value={field.value || ''}
1110
- />
1111
- </FormControl>
1112
- <FormMessage />
1113
- </FormItem>
1114
- )}
1115
- />
1116
- </div>
1117
-
1118
- <div className="flex flex-col gap-2 py-4">
1119
- <Button
1120
- type="submit"
1121
- disabled={
1122
- form.formState.isSubmitting ||
1123
- isUploadingFile ||
1124
- isExtractingFileData
1125
- }
1126
- >
1127
- {(isUploadingFile || isExtractingFileData) && (
1128
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1129
- )}
1130
- {isExtractingFileData
1131
- ? t('common.upload.fillingWithAi')
1132
- : isUploadingFile
1133
- ? t('common.upload.uploadingFile')
1134
- : t('common.save')}
1135
- </Button>
1136
- </div>
1137
- </form>
1138
- </Form>
1139
- </SheetContent>
802
+ {!isExtractingFileData &&
803
+ extractionWarnings.length > 0 && (
804
+ <p className="truncate text-xs text-muted-foreground">
805
+ {extractionWarnings[0]}
806
+ </p>
807
+ )}
808
+ </div>
809
+ </div>
810
+
811
+ <FormField
812
+ control={form.control}
813
+ name="documento"
814
+ render={({ field }) => (
815
+ <FormItem>
816
+ <FormLabel>{t('fields.document')}</FormLabel>
817
+ <FormControl>
818
+ <Input placeholder="NF-00000" {...field} />
819
+ </FormControl>
820
+ <FormMessage />
821
+ </FormItem>
822
+ )}
823
+ />
824
+ </div>
825
+
826
+ <PersonFieldWithCreate
827
+ form={form}
828
+ name="fornecedorId"
829
+ label={t('fields.supplier')}
830
+ entityLabel="fornecedor"
831
+ selectPlaceholder={t('common.select')}
832
+ />
833
+
834
+ <div className="grid gap-4 md:grid-cols-2">
835
+ <FormField
836
+ control={form.control}
837
+ name="competencia"
838
+ render={({ field }) => (
839
+ <FormItem>
840
+ <FormLabel>{t('fields.competency')}</FormLabel>
841
+ <FormControl>
842
+ <Input
843
+ type="month"
844
+ {...field}
845
+ value={field.value || ''}
846
+ />
847
+ </FormControl>
848
+ <FormMessage />
849
+ </FormItem>
850
+ )}
851
+ />
852
+
853
+ <FormField
854
+ control={form.control}
855
+ name="vencimento"
856
+ render={({ field }) => (
857
+ <FormItem>
858
+ <FormLabel>{t('fields.dueDate')}</FormLabel>
859
+ <FormControl>
860
+ <Input
861
+ type="date"
862
+ {...field}
863
+ value={field.value || ''}
864
+ />
865
+ </FormControl>
866
+ <FormMessage />
867
+ </FormItem>
868
+ )}
869
+ />
870
+ </div>
871
+
872
+ <div className="grid gap-4 md:grid-cols-2">
873
+ <FormField
874
+ control={form.control}
875
+ name="valor"
876
+ render={({ field }) => (
877
+ <FormItem>
878
+ <FormLabel>{t('fields.totalValue')}</FormLabel>
879
+ <FormControl>
880
+ <InputMoney
881
+ ref={field.ref}
882
+ name={field.name}
883
+ value={field.value}
884
+ onBlur={field.onBlur}
885
+ onValueChange={(value) => field.onChange(value ?? 0)}
886
+ placeholder="0,00"
887
+ />
888
+ </FormControl>
889
+ <FormMessage />
890
+ </FormItem>
891
+ )}
892
+ />
893
+
894
+ <FormField
895
+ control={form.control}
896
+ name="installmentsCount"
897
+ render={({ field }) => (
898
+ <FormItem>
899
+ <FormLabel>
900
+ {t('installmentsEditor.countLabel')}
901
+ </FormLabel>
902
+ <FormControl>
903
+ <Input
904
+ type="number"
905
+ min={1}
906
+ max={120}
907
+ value={field.value}
908
+ onChange={(event) => {
909
+ const nextValue = Number(event.target.value || 1);
910
+ field.onChange(
911
+ Number.isNaN(nextValue) ? 1 : nextValue
912
+ );
913
+ }}
914
+ />
915
+ </FormControl>
916
+ <FormMessage />
917
+ </FormItem>
918
+ )}
919
+ />
920
+ </div>
921
+ </FinanceSheetSection>
922
+
923
+ <FinanceSheetSection
924
+ title={t('sections.installments.title')}
925
+ description={t('sections.installments.description')}
926
+ >
927
+ <div className="space-y-3">
928
+ <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
929
+ <div className="flex items-center gap-2">
930
+ <Checkbox
931
+ id="auto-redistribute-installments-payable"
932
+ checked={autoRedistributeInstallments}
933
+ onCheckedChange={(checked) =>
934
+ setAutoRedistributeInstallments(checked === true)
935
+ }
936
+ />
937
+ <Label
938
+ htmlFor="auto-redistribute-installments-payable"
939
+ className="text-xs text-muted-foreground"
940
+ >
941
+ {t('installmentsEditor.autoRedistributeLabel')}
942
+ </Label>
943
+ </div>
944
+ <Button
945
+ type="button"
946
+ variant="outline"
947
+ size="sm"
948
+ onClick={() => {
949
+ setIsInstallmentsEdited(false);
950
+ replaceInstallments(
951
+ buildEqualInstallments(
952
+ form.getValues('installmentsCount'),
953
+ form.getValues('valor'),
954
+ form.getValues('vencimento')
955
+ )
956
+ );
957
+ }}
958
+ >
959
+ {t('installmentsEditor.recalculate')}
960
+ </Button>
961
+ </div>
962
+
963
+ {autoRedistributeInstallments && (
964
+ <p className="text-xs text-muted-foreground">
965
+ {t('installmentsEditor.autoRedistributeHint')}
966
+ </p>
967
+ )}
968
+
969
+ <div className="space-y-2">
970
+ {installmentFields.map((installment, index) => (
971
+ <div
972
+ key={installment.id}
973
+ 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]"
974
+ >
975
+ <div className="flex items-center text-sm text-muted-foreground">
976
+ #{index + 1}
977
+ </div>
978
+
979
+ <FormField
980
+ control={form.control}
981
+ name={`installments.${index}.dueDate` as const}
982
+ render={({ field }) => (
983
+ <FormItem>
984
+ <FormLabel className="text-xs">
985
+ {t('installmentsEditor.dueDateLabel')}
986
+ </FormLabel>
987
+ <FormControl>
988
+ <Input
989
+ type="date"
990
+ {...field}
991
+ value={field.value || ''}
992
+ onChange={(event) => {
993
+ setIsInstallmentsEdited(true);
994
+ field.onChange(event);
995
+ }}
996
+ />
997
+ </FormControl>
998
+ <FormMessage />
999
+ </FormItem>
1000
+ )}
1001
+ />
1002
+
1003
+ <FormField
1004
+ control={form.control}
1005
+ name={`installments.${index}.amount` as const}
1006
+ render={({ field }) => (
1007
+ <FormItem>
1008
+ <FormLabel className="text-xs">
1009
+ {t('installmentsEditor.amountLabel')}
1010
+ </FormLabel>
1011
+ <FormControl>
1012
+ <InputMoney
1013
+ ref={field.ref}
1014
+ name={field.name}
1015
+ value={field.value}
1016
+ onBlur={() => {
1017
+ field.onBlur();
1018
+
1019
+ if (!autoRedistributeInstallments) {
1020
+ return;
1021
+ }
1022
+
1023
+ clearScheduledRedistribution(index);
1024
+ runInstallmentRedistribution(index);
1025
+ }}
1026
+ onValueChange={(value) => {
1027
+ setIsInstallmentsEdited(true);
1028
+ field.onChange(value ?? 0);
1029
+
1030
+ if (!autoRedistributeInstallments) {
1031
+ return;
1032
+ }
1033
+
1034
+ scheduleInstallmentRedistribution(index);
1035
+ }}
1036
+ placeholder="0,00"
1037
+ />
1038
+ </FormControl>
1039
+ <FormMessage />
1040
+ </FormItem>
1041
+ )}
1042
+ />
1043
+ </div>
1044
+ ))}
1045
+ </div>
1046
+
1047
+ <p
1048
+ className={`text-xs ${
1049
+ installmentsDiffCents === 0
1050
+ ? 'text-muted-foreground'
1051
+ : 'text-destructive'
1052
+ }`}
1053
+ >
1054
+ {t('installmentsEditor.totalPrefix', {
1055
+ total: installmentsTotal.toFixed(2),
1056
+ })}
1057
+ {installmentsDiffCents > 0 &&
1058
+ ` ${t('installmentsEditor.adjustmentNeeded')}`}
1059
+ </p>
1060
+ {form.formState.errors.installments?.message && (
1061
+ <p className="text-xs text-destructive">
1062
+ {form.formState.errors.installments.message}
1063
+ </p>
1064
+ )}
1065
+ </div>
1066
+ </FinanceSheetSection>
1067
+
1068
+ <FinanceSheetSection
1069
+ title={t('sections.classification.title')}
1070
+ description={t('sections.classification.description')}
1071
+ >
1072
+ <div className="grid gap-4 xl:grid-cols-3">
1073
+ <CategoryFieldWithCreate
1074
+ form={form}
1075
+ name="categoriaId"
1076
+ label={t('fields.category')}
1077
+ selectPlaceholder={t('common.select')}
1078
+ categories={categorias}
1079
+ categoryKind="despesa"
1080
+ onCreated={onCategoriesUpdated}
1081
+ />
1082
+
1083
+ <CostCenterFieldWithCreate
1084
+ form={form}
1085
+ name="centroCustoId"
1086
+ label={t('fields.costCenter')}
1087
+ selectPlaceholder={t('common.select')}
1088
+ costCenters={centrosCusto}
1089
+ onCreated={onCostCentersUpdated}
1090
+ />
1091
+
1092
+ <FormField
1093
+ control={form.control}
1094
+ name="metodo"
1095
+ render={({ field }) => (
1096
+ <FormItem>
1097
+ <FormLabel>{t('fields.paymentMethod')}</FormLabel>
1098
+ <Select
1099
+ value={field.value}
1100
+ onValueChange={field.onChange}
1101
+ >
1102
+ <FormControl>
1103
+ <SelectTrigger className="w-full">
1104
+ <SelectValue placeholder={t('common.select')} />
1105
+ </SelectTrigger>
1106
+ </FormControl>
1107
+ <SelectContent>
1108
+ <SelectItem value="boleto">
1109
+ {t('paymentMethods.boleto')}
1110
+ </SelectItem>
1111
+ <SelectItem value="pix">PIX</SelectItem>
1112
+ <SelectItem value="transferencia">
1113
+ {t('paymentMethods.transfer')}
1114
+ </SelectItem>
1115
+ <SelectItem value="cartao">
1116
+ {t('paymentMethods.card')}
1117
+ </SelectItem>
1118
+ <SelectItem value="dinheiro">
1119
+ {t('paymentMethods.cash')}
1120
+ </SelectItem>
1121
+ <SelectItem value="cheque">
1122
+ {t('paymentMethods.check')}
1123
+ </SelectItem>
1124
+ </SelectContent>
1125
+ </Select>
1126
+ <FormMessage />
1127
+ </FormItem>
1128
+ )}
1129
+ />
1130
+ </div>
1131
+ </FinanceSheetSection>
1132
+
1133
+ <FinanceSheetSection
1134
+ title={t('sections.notes.title')}
1135
+ description={t('sections.notes.description')}
1136
+ >
1137
+ <FormField
1138
+ control={form.control}
1139
+ name="descricao"
1140
+ render={({ field }) => (
1141
+ <FormItem>
1142
+ <FormLabel>{t('fields.description')}</FormLabel>
1143
+ <FormControl>
1144
+ <Textarea
1145
+ placeholder={t('newTitle.descriptionPlaceholder')}
1146
+ {...field}
1147
+ value={field.value || ''}
1148
+ />
1149
+ </FormControl>
1150
+ <FormMessage />
1151
+ </FormItem>
1152
+ )}
1153
+ />
1154
+ </FinanceSheetSection>
1155
+ </FinanceSheetBody>
1156
+
1157
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
1158
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
1159
+ <Button type="button" variant="outline" onClick={handleCancel}>
1160
+ {t('common.cancel')}
1161
+ </Button>
1162
+ <Button
1163
+ type="submit"
1164
+ disabled={
1165
+ form.formState.isSubmitting ||
1166
+ isUploadingFile ||
1167
+ isExtractingFileData
1168
+ }
1169
+ >
1170
+ {(isUploadingFile || isExtractingFileData) && (
1171
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1172
+ )}
1173
+ {isExtractingFileData
1174
+ ? t('common.upload.fillingWithAi')
1175
+ : isUploadingFile
1176
+ ? t('common.upload.uploadingFile')
1177
+ : t('common.save')}
1178
+ </Button>
1179
+ </div>
1180
+ </div>
1181
+ </form>
1182
+ </Form>
1183
+ </SheetContent>
1140
1184
  </Sheet>
1141
1185
  );
1142
1186
  }
@@ -1401,9 +1445,9 @@ function EditarTituloSheet({
1401
1445
  };
1402
1446
  }, []);
1403
1447
 
1404
- const handleSubmit = async (values: NewTitleFormValues) => {
1405
- if (!titulo?.id) {
1406
- showToastHandler?.('error', t('messages.invalidTitleForEdit'));
1448
+ const handleSubmit = async (values: NewTitleFormValues) => {
1449
+ if (!titulo?.id) {
1450
+ showToastHandler?.('error', t('messages.invalidTitleForEdit'));
1407
1451
  return;
1408
1452
  }
1409
1453
 
@@ -1440,9 +1484,13 @@ function EditarTituloSheet({
1440
1484
  showToastHandler?.('success', t('messages.updateSuccess'));
1441
1485
  onOpenChange(false);
1442
1486
  } catch {
1443
- showToastHandler?.('error', t('messages.updateError'));
1444
- }
1445
- };
1487
+ showToastHandler?.('error', t('messages.updateError'));
1488
+ }
1489
+ };
1490
+
1491
+ const handleCancel = () => {
1492
+ onOpenChange(false);
1493
+ };
1446
1494
 
1447
1495
  const clearUploadedFile = () => {
1448
1496
  setUploadedFileId(null);
@@ -1609,37 +1657,44 @@ function EditarTituloSheet({
1609
1657
 
1610
1658
  return (
1611
1659
  <Sheet open={open} onOpenChange={onOpenChange}>
1612
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
1613
- <SheetHeader>
1614
- <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1615
- <SheetDescription>{t('editTitle.description')}</SheetDescription>
1616
- </SheetHeader>
1617
- <Form {...form}>
1618
- <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
1619
- <div className="grid gap-3">
1620
- <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
1621
- <div className="grid gap-2">
1622
- <FormLabel>{t('common.upload.label')}</FormLabel>
1623
- <Input
1624
- ref={fileInputRef}
1625
- className="hidden"
1626
- type="file"
1627
- accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
1628
- onChange={(event) => {
1629
- const file = event.target.files?.[0];
1630
- if (!file) {
1631
- return;
1632
- }
1633
-
1634
- clearUploadedFile();
1635
- void uploadRelatedFile(file);
1636
- }}
1637
- disabled={
1638
- isUploadingFile ||
1639
- isExtractingFileData ||
1640
- form.formState.isSubmitting
1641
- }
1642
- />
1660
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
1661
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
1662
+ <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1663
+ <SheetDescription>{t('editTitle.description')}</SheetDescription>
1664
+ </SheetHeader>
1665
+ <Form {...form}>
1666
+ <form
1667
+ className="flex h-full flex-col overflow-hidden"
1668
+ onSubmit={form.handleSubmit(handleSubmit)}
1669
+ >
1670
+ <FinanceSheetBody>
1671
+ <FinanceSheetSection
1672
+ title={t('sections.main.title')}
1673
+ description={t('sections.main.description')}
1674
+ >
1675
+ <div className="grid grid-cols-1 items-start gap-4 xl:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
1676
+ <div className="grid gap-2">
1677
+ <FormLabel>{t('common.upload.label')}</FormLabel>
1678
+ <Input
1679
+ ref={fileInputRef}
1680
+ className="hidden"
1681
+ type="file"
1682
+ accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
1683
+ onChange={(event) => {
1684
+ const file = event.target.files?.[0];
1685
+ if (!file) {
1686
+ return;
1687
+ }
1688
+
1689
+ clearUploadedFile();
1690
+ void uploadRelatedFile(file);
1691
+ }}
1692
+ disabled={
1693
+ isUploadingFile ||
1694
+ isExtractingFileData ||
1695
+ form.formState.isSubmitting
1696
+ }
1697
+ />
1643
1698
 
1644
1699
  <div className="grid w-full grid-cols-2 gap-2">
1645
1700
  <Tooltip>
@@ -1704,12 +1759,12 @@ function EditarTituloSheet({
1704
1759
  )}
1705
1760
  </div>
1706
1761
 
1707
- <div className="space-y-1">
1708
- {(uploadedFileId || uploadedFileName) && (
1709
- <p className="truncate text-xs text-muted-foreground">
1710
- {t('common.upload.selectedPrefix')} {uploadedFileName}
1711
- </p>
1712
- )}
1762
+ <div className="space-y-1 rounded-lg border border-dashed border-border/70 bg-muted/20 p-3">
1763
+ {(uploadedFileId || uploadedFileName) && (
1764
+ <p className="truncate text-xs text-muted-foreground">
1765
+ {t('common.upload.selectedPrefix')} {uploadedFileName}
1766
+ </p>
1767
+ )}
1713
1768
 
1714
1769
  {isUploadingFile && !isExtractingFileData && (
1715
1770
  <div className="space-y-1">
@@ -1739,358 +1794,387 @@ function EditarTituloSheet({
1739
1794
  </p>
1740
1795
  )}
1741
1796
 
1742
- {!isExtractingFileData && extractionWarnings.length > 0 && (
1743
- <p className="truncate text-xs text-muted-foreground">
1744
- {extractionWarnings[0]}
1745
- </p>
1746
- )}
1747
- </div>
1748
- </div>
1749
-
1750
- <FormField
1751
- control={form.control}
1752
- name="documento"
1753
- render={({ field }) => (
1754
- <FormItem>
1755
- <FormLabel>{t('fields.document')}</FormLabel>
1756
- <FormControl>
1757
- <Input placeholder="NF-00000" {...field} />
1758
- </FormControl>
1759
- <FormMessage />
1760
- </FormItem>
1761
- )}
1762
- />
1763
- </div>
1764
-
1765
- <PersonFieldWithCreate
1766
- form={form}
1767
- name="fornecedorId"
1768
- label={t('fields.supplier')}
1769
- entityLabel="fornecedor"
1770
- selectPlaceholder={t('common.select')}
1771
- />
1772
-
1773
- <div className="grid grid-cols-2 items-start gap-3">
1774
- <FormField
1775
- control={form.control}
1776
- name="competencia"
1777
- render={({ field }) => (
1778
- <FormItem>
1779
- <FormLabel>{t('fields.competency')}</FormLabel>
1780
- <FormControl>
1781
- <Input
1782
- type="month"
1783
- {...field}
1784
- value={field.value || ''}
1785
- />
1786
- </FormControl>
1787
- <FormMessage />
1788
- </FormItem>
1789
- )}
1790
- />
1791
-
1792
- <FormField
1793
- control={form.control}
1794
- name="vencimento"
1795
- render={({ field }) => (
1796
- <FormItem>
1797
- <FormLabel>{t('fields.dueDate')}</FormLabel>
1798
- <FormControl>
1799
- <Input
1800
- type="date"
1801
- {...field}
1802
- value={field.value || ''}
1803
- />
1804
- </FormControl>
1805
- <FormMessage />
1806
- </FormItem>
1807
- )}
1808
- />
1809
- </div>
1810
-
1811
- <div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
1812
- <FormField
1813
- control={form.control}
1814
- name="valor"
1815
- render={({ field }) => (
1816
- <FormItem>
1817
- <FormLabel>{t('fields.totalValue')}</FormLabel>
1818
- <FormControl>
1819
- <InputMoney
1820
- ref={field.ref}
1821
- name={field.name}
1822
- value={field.value}
1823
- onBlur={field.onBlur}
1824
- onValueChange={(value) => field.onChange(value ?? 0)}
1825
- placeholder="0,00"
1826
- />
1827
- </FormControl>
1828
- <FormMessage />
1829
- </FormItem>
1830
- )}
1831
- />
1832
-
1833
- <FormField
1834
- control={form.control}
1835
- name="installmentsCount"
1836
- render={({ field }) => (
1837
- <FormItem>
1838
- <FormLabel>
1839
- {t('installmentsEditor.countLabel')}
1840
- </FormLabel>
1841
- <FormControl>
1842
- <Input
1843
- type="number"
1844
- min={1}
1845
- max={120}
1846
- value={field.value}
1847
- onChange={(event) => {
1848
- const nextValue = Number(event.target.value || 1);
1849
- field.onChange(
1850
- Number.isNaN(nextValue) ? 1 : nextValue
1851
- );
1852
- setIsInstallmentsEdited(false);
1853
- }}
1854
- />
1855
- </FormControl>
1856
- <FormMessage />
1857
- </FormItem>
1858
- )}
1859
- />
1860
- </div>
1861
-
1862
- <div className="space-y-3 rounded-md border p-3">
1863
- <div className="flex items-center justify-between gap-2">
1864
- <p className="text-sm font-medium">
1865
- {t('installmentsEditor.title')}
1866
- </p>
1867
- <Button
1868
- type="button"
1869
- variant="outline"
1870
- size="sm"
1871
- onClick={() => {
1872
- setIsInstallmentsEdited(false);
1873
- replaceInstallments(
1874
- buildEqualInstallments(
1875
- form.getValues('installmentsCount'),
1876
- form.getValues('valor'),
1877
- form.getValues('vencimento')
1878
- )
1879
- );
1880
- }}
1881
- >
1882
- {t('installmentsEditor.recalculate')}
1883
- </Button>
1884
- </div>
1885
-
1886
- <div className="flex items-center gap-2">
1887
- <Checkbox
1888
- id="auto-redistribute-installments-edit-payable"
1889
- checked={autoRedistributeInstallments}
1890
- onCheckedChange={(checked) =>
1891
- setAutoRedistributeInstallments(checked === true)
1892
- }
1893
- />
1894
- <Label
1895
- htmlFor="auto-redistribute-installments-edit-payable"
1896
- className="text-xs text-muted-foreground"
1897
- >
1898
- {t('installmentsEditor.autoRedistributeLabel')}
1899
- </Label>
1900
- </div>
1901
-
1902
- <div className="space-y-2">
1903
- {installmentFields.map((installment, index) => (
1904
- <div
1905
- key={installment.id}
1906
- className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
1907
- >
1908
- <div className="flex items-center text-sm text-muted-foreground">
1909
- #{index + 1}
1910
- </div>
1911
-
1912
- <FormField
1913
- control={form.control}
1914
- name={`installments.${index}.dueDate` as const}
1915
- render={({ field }) => (
1916
- <FormItem>
1917
- <FormLabel className="text-xs">
1918
- {t('installmentsEditor.dueDateLabel')}
1919
- </FormLabel>
1920
- <FormControl>
1921
- <Input
1922
- type="date"
1923
- {...field}
1924
- value={field.value || ''}
1925
- onChange={(event) => {
1926
- setIsInstallmentsEdited(true);
1927
- field.onChange(event);
1928
- }}
1929
- />
1930
- </FormControl>
1931
- <FormMessage />
1932
- </FormItem>
1933
- )}
1934
- />
1935
-
1936
- <FormField
1937
- control={form.control}
1938
- name={`installments.${index}.amount` as const}
1939
- render={({ field }) => (
1940
- <FormItem>
1941
- <FormLabel className="text-xs">
1942
- {t('installmentsEditor.amountLabel')}
1943
- </FormLabel>
1944
- <FormControl>
1945
- <InputMoney
1946
- ref={field.ref}
1947
- name={field.name}
1948
- value={field.value}
1949
- onBlur={() => {
1950
- field.onBlur();
1951
-
1952
- if (!autoRedistributeInstallments) {
1953
- return;
1954
- }
1955
-
1956
- clearScheduledRedistribution(index);
1957
- runInstallmentRedistribution(index);
1958
- }}
1959
- onValueChange={(value) => {
1960
- setIsInstallmentsEdited(true);
1961
- field.onChange(value ?? 0);
1962
-
1963
- if (!autoRedistributeInstallments) {
1964
- return;
1965
- }
1966
-
1967
- scheduleInstallmentRedistribution(index);
1968
- }}
1969
- placeholder="0,00"
1970
- />
1971
- </FormControl>
1972
- <FormMessage />
1973
- </FormItem>
1974
- )}
1975
- />
1976
- </div>
1977
- ))}
1978
- </div>
1979
-
1980
- <p
1981
- className={`text-xs ${
1982
- installmentsDiffCents === 0
1983
- ? 'text-muted-foreground'
1984
- : 'text-destructive'
1985
- }`}
1986
- >
1987
- {t('installmentsEditor.totalPrefix', {
1988
- total: installmentsTotal.toFixed(2),
1989
- })}
1990
- {installmentsDiffCents > 0 &&
1991
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
1992
- </p>
1993
- {form.formState.errors.installments?.message && (
1994
- <p className="text-xs text-destructive">
1995
- {form.formState.errors.installments.message}
1996
- </p>
1997
- )}
1998
- </div>
1999
-
2000
- <CategoryFieldWithCreate
2001
- form={form}
2002
- name="categoriaId"
2003
- label={t('fields.category')}
2004
- selectPlaceholder={t('common.select')}
2005
- categories={categorias}
2006
- categoryKind="despesa"
2007
- onCreated={onCategoriesUpdated}
2008
- />
2009
-
2010
- <CostCenterFieldWithCreate
2011
- form={form}
2012
- name="centroCustoId"
2013
- label={t('fields.costCenter')}
2014
- selectPlaceholder={t('common.select')}
2015
- costCenters={centrosCusto}
2016
- onCreated={onCostCentersUpdated}
2017
- />
2018
-
2019
- <FormField
2020
- control={form.control}
2021
- name="metodo"
2022
- render={({ field }) => (
2023
- <FormItem>
2024
- <FormLabel>{t('fields.paymentMethod')}</FormLabel>
2025
- <Select value={field.value} onValueChange={field.onChange}>
2026
- <FormControl>
2027
- <SelectTrigger className="w-full">
2028
- <SelectValue placeholder={t('common.select')} />
2029
- </SelectTrigger>
2030
- </FormControl>
2031
- <SelectContent>
2032
- <SelectItem value="boleto">
2033
- {t('paymentMethods.boleto')}
2034
- </SelectItem>
2035
- <SelectItem value="pix">PIX</SelectItem>
2036
- <SelectItem value="transferencia">
2037
- {t('paymentMethods.transfer')}
2038
- </SelectItem>
2039
- <SelectItem value="cartao">
2040
- {t('paymentMethods.card')}
2041
- </SelectItem>
2042
- <SelectItem value="dinheiro">
2043
- {t('paymentMethods.cash')}
2044
- </SelectItem>
2045
- <SelectItem value="cheque">
2046
- {t('paymentMethods.check')}
2047
- </SelectItem>
2048
- </SelectContent>
2049
- </Select>
2050
- <FormMessage />
2051
- </FormItem>
2052
- )}
2053
- />
2054
-
2055
- <FormField
2056
- control={form.control}
2057
- name="descricao"
2058
- render={({ field }) => (
2059
- <FormItem>
2060
- <FormLabel>{t('fields.description')}</FormLabel>
2061
- <FormControl>
2062
- <Textarea
2063
- placeholder={t('newTitle.descriptionPlaceholder')}
2064
- {...field}
2065
- value={field.value || ''}
2066
- />
2067
- </FormControl>
2068
- <FormMessage />
2069
- </FormItem>
2070
- )}
2071
- />
2072
- </div>
2073
-
2074
- <div className="flex flex-col gap-2 py-4">
2075
- <Button
2076
- type="submit"
2077
- disabled={
2078
- form.formState.isSubmitting ||
2079
- isUploadingFile ||
2080
- isExtractingFileData
2081
- }
2082
- >
2083
- {(isUploadingFile || isExtractingFileData) && (
2084
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2085
- )}
2086
- {isExtractingFileData
2087
- ? t('common.upload.fillingWithAi')
2088
- : isUploadingFile
2089
- ? t('common.upload.uploadingFile')
2090
- : t('common.save')}
2091
- </Button>
2092
- </div>
2093
- </form>
1797
+ {!isExtractingFileData &&
1798
+ extractionWarnings.length > 0 && (
1799
+ <p className="truncate text-xs text-muted-foreground">
1800
+ {extractionWarnings[0]}
1801
+ </p>
1802
+ )}
1803
+ </div>
1804
+ </div>
1805
+
1806
+ <FormField
1807
+ control={form.control}
1808
+ name="documento"
1809
+ render={({ field }) => (
1810
+ <FormItem>
1811
+ <FormLabel>{t('fields.document')}</FormLabel>
1812
+ <FormControl>
1813
+ <Input placeholder="NF-00000" {...field} />
1814
+ </FormControl>
1815
+ <FormMessage />
1816
+ </FormItem>
1817
+ )}
1818
+ />
1819
+ </div>
1820
+
1821
+ <PersonFieldWithCreate
1822
+ form={form}
1823
+ name="fornecedorId"
1824
+ label={t('fields.supplier')}
1825
+ entityLabel="fornecedor"
1826
+ selectPlaceholder={t('common.select')}
1827
+ />
1828
+
1829
+ <div className="grid gap-4 md:grid-cols-2">
1830
+ <FormField
1831
+ control={form.control}
1832
+ name="competencia"
1833
+ render={({ field }) => (
1834
+ <FormItem>
1835
+ <FormLabel>{t('fields.competency')}</FormLabel>
1836
+ <FormControl>
1837
+ <Input
1838
+ type="month"
1839
+ {...field}
1840
+ value={field.value || ''}
1841
+ />
1842
+ </FormControl>
1843
+ <FormMessage />
1844
+ </FormItem>
1845
+ )}
1846
+ />
1847
+
1848
+ <FormField
1849
+ control={form.control}
1850
+ name="vencimento"
1851
+ render={({ field }) => (
1852
+ <FormItem>
1853
+ <FormLabel>{t('fields.dueDate')}</FormLabel>
1854
+ <FormControl>
1855
+ <Input
1856
+ type="date"
1857
+ {...field}
1858
+ value={field.value || ''}
1859
+ />
1860
+ </FormControl>
1861
+ <FormMessage />
1862
+ </FormItem>
1863
+ )}
1864
+ />
1865
+ </div>
1866
+
1867
+ <div className="grid gap-4 md:grid-cols-2">
1868
+ <FormField
1869
+ control={form.control}
1870
+ name="valor"
1871
+ render={({ field }) => (
1872
+ <FormItem>
1873
+ <FormLabel>{t('fields.totalValue')}</FormLabel>
1874
+ <FormControl>
1875
+ <InputMoney
1876
+ ref={field.ref}
1877
+ name={field.name}
1878
+ value={field.value}
1879
+ onBlur={field.onBlur}
1880
+ onValueChange={(value) => field.onChange(value ?? 0)}
1881
+ placeholder="0,00"
1882
+ />
1883
+ </FormControl>
1884
+ <FormMessage />
1885
+ </FormItem>
1886
+ )}
1887
+ />
1888
+
1889
+ <FormField
1890
+ control={form.control}
1891
+ name="installmentsCount"
1892
+ render={({ field }) => (
1893
+ <FormItem>
1894
+ <FormLabel>
1895
+ {t('installmentsEditor.countLabel')}
1896
+ </FormLabel>
1897
+ <FormControl>
1898
+ <Input
1899
+ type="number"
1900
+ min={1}
1901
+ max={120}
1902
+ value={field.value}
1903
+ onChange={(event) => {
1904
+ const nextValue = Number(event.target.value || 1);
1905
+ field.onChange(
1906
+ Number.isNaN(nextValue) ? 1 : nextValue
1907
+ );
1908
+ setIsInstallmentsEdited(false);
1909
+ }}
1910
+ />
1911
+ </FormControl>
1912
+ <FormMessage />
1913
+ </FormItem>
1914
+ )}
1915
+ />
1916
+ </div>
1917
+ </FinanceSheetSection>
1918
+
1919
+ <FinanceSheetSection
1920
+ title={t('sections.installments.title')}
1921
+ description={t('sections.installments.description')}
1922
+ >
1923
+ <div className="space-y-3">
1924
+ <div className="flex flex-col gap-3 xl:flex-row xl:items-center xl:justify-between">
1925
+ <div className="flex items-center gap-2">
1926
+ <Checkbox
1927
+ id="auto-redistribute-installments-edit-payable"
1928
+ checked={autoRedistributeInstallments}
1929
+ onCheckedChange={(checked) =>
1930
+ setAutoRedistributeInstallments(checked === true)
1931
+ }
1932
+ />
1933
+ <Label
1934
+ htmlFor="auto-redistribute-installments-edit-payable"
1935
+ className="text-xs text-muted-foreground"
1936
+ >
1937
+ {t('installmentsEditor.autoRedistributeLabel')}
1938
+ </Label>
1939
+ </div>
1940
+ <Button
1941
+ type="button"
1942
+ variant="outline"
1943
+ size="sm"
1944
+ onClick={() => {
1945
+ setIsInstallmentsEdited(false);
1946
+ replaceInstallments(
1947
+ buildEqualInstallments(
1948
+ form.getValues('installmentsCount'),
1949
+ form.getValues('valor'),
1950
+ form.getValues('vencimento')
1951
+ )
1952
+ );
1953
+ }}
1954
+ >
1955
+ {t('installmentsEditor.recalculate')}
1956
+ </Button>
1957
+ </div>
1958
+
1959
+ {autoRedistributeInstallments && (
1960
+ <p className="text-xs text-muted-foreground">
1961
+ {t('installmentsEditor.autoRedistributeHint')}
1962
+ </p>
1963
+ )}
1964
+
1965
+ <div className="space-y-2">
1966
+ {installmentFields.map((installment, index) => (
1967
+ <div
1968
+ key={installment.id}
1969
+ 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]"
1970
+ >
1971
+ <div className="flex items-center text-sm text-muted-foreground">
1972
+ #{index + 1}
1973
+ </div>
1974
+
1975
+ <FormField
1976
+ control={form.control}
1977
+ name={`installments.${index}.dueDate` as const}
1978
+ render={({ field }) => (
1979
+ <FormItem>
1980
+ <FormLabel className="text-xs">
1981
+ {t('installmentsEditor.dueDateLabel')}
1982
+ </FormLabel>
1983
+ <FormControl>
1984
+ <Input
1985
+ type="date"
1986
+ {...field}
1987
+ value={field.value || ''}
1988
+ onChange={(event) => {
1989
+ setIsInstallmentsEdited(true);
1990
+ field.onChange(event);
1991
+ }}
1992
+ />
1993
+ </FormControl>
1994
+ <FormMessage />
1995
+ </FormItem>
1996
+ )}
1997
+ />
1998
+
1999
+ <FormField
2000
+ control={form.control}
2001
+ name={`installments.${index}.amount` as const}
2002
+ render={({ field }) => (
2003
+ <FormItem>
2004
+ <FormLabel className="text-xs">
2005
+ {t('installmentsEditor.amountLabel')}
2006
+ </FormLabel>
2007
+ <FormControl>
2008
+ <InputMoney
2009
+ ref={field.ref}
2010
+ name={field.name}
2011
+ value={field.value}
2012
+ onBlur={() => {
2013
+ field.onBlur();
2014
+
2015
+ if (!autoRedistributeInstallments) {
2016
+ return;
2017
+ }
2018
+
2019
+ clearScheduledRedistribution(index);
2020
+ runInstallmentRedistribution(index);
2021
+ }}
2022
+ onValueChange={(value) => {
2023
+ setIsInstallmentsEdited(true);
2024
+ field.onChange(value ?? 0);
2025
+
2026
+ if (!autoRedistributeInstallments) {
2027
+ return;
2028
+ }
2029
+
2030
+ scheduleInstallmentRedistribution(index);
2031
+ }}
2032
+ placeholder="0,00"
2033
+ />
2034
+ </FormControl>
2035
+ <FormMessage />
2036
+ </FormItem>
2037
+ )}
2038
+ />
2039
+ </div>
2040
+ ))}
2041
+ </div>
2042
+
2043
+ <p
2044
+ className={`text-xs ${
2045
+ installmentsDiffCents === 0
2046
+ ? 'text-muted-foreground'
2047
+ : 'text-destructive'
2048
+ }`}
2049
+ >
2050
+ {t('installmentsEditor.totalPrefix', {
2051
+ total: installmentsTotal.toFixed(2),
2052
+ })}
2053
+ {installmentsDiffCents > 0 &&
2054
+ ` ${t('installmentsEditor.adjustmentNeeded')}`}
2055
+ </p>
2056
+ {form.formState.errors.installments?.message && (
2057
+ <p className="text-xs text-destructive">
2058
+ {form.formState.errors.installments.message}
2059
+ </p>
2060
+ )}
2061
+ </div>
2062
+ </FinanceSheetSection>
2063
+
2064
+ <FinanceSheetSection
2065
+ title={t('sections.classification.title')}
2066
+ description={t('sections.classification.description')}
2067
+ >
2068
+ <div className="grid gap-4 xl:grid-cols-3">
2069
+ <CategoryFieldWithCreate
2070
+ form={form}
2071
+ name="categoriaId"
2072
+ label={t('fields.category')}
2073
+ selectPlaceholder={t('common.select')}
2074
+ categories={categorias}
2075
+ categoryKind="despesa"
2076
+ onCreated={onCategoriesUpdated}
2077
+ />
2078
+
2079
+ <CostCenterFieldWithCreate
2080
+ form={form}
2081
+ name="centroCustoId"
2082
+ label={t('fields.costCenter')}
2083
+ selectPlaceholder={t('common.select')}
2084
+ costCenters={centrosCusto}
2085
+ onCreated={onCostCentersUpdated}
2086
+ />
2087
+
2088
+ <FormField
2089
+ control={form.control}
2090
+ name="metodo"
2091
+ render={({ field }) => (
2092
+ <FormItem>
2093
+ <FormLabel>{t('fields.paymentMethod')}</FormLabel>
2094
+ <Select
2095
+ value={field.value}
2096
+ onValueChange={field.onChange}
2097
+ >
2098
+ <FormControl>
2099
+ <SelectTrigger className="w-full">
2100
+ <SelectValue placeholder={t('common.select')} />
2101
+ </SelectTrigger>
2102
+ </FormControl>
2103
+ <SelectContent>
2104
+ <SelectItem value="boleto">
2105
+ {t('paymentMethods.boleto')}
2106
+ </SelectItem>
2107
+ <SelectItem value="pix">PIX</SelectItem>
2108
+ <SelectItem value="transferencia">
2109
+ {t('paymentMethods.transfer')}
2110
+ </SelectItem>
2111
+ <SelectItem value="cartao">
2112
+ {t('paymentMethods.card')}
2113
+ </SelectItem>
2114
+ <SelectItem value="dinheiro">
2115
+ {t('paymentMethods.cash')}
2116
+ </SelectItem>
2117
+ <SelectItem value="cheque">
2118
+ {t('paymentMethods.check')}
2119
+ </SelectItem>
2120
+ </SelectContent>
2121
+ </Select>
2122
+ <FormMessage />
2123
+ </FormItem>
2124
+ )}
2125
+ />
2126
+ </div>
2127
+ </FinanceSheetSection>
2128
+
2129
+ <FinanceSheetSection
2130
+ title={t('sections.notes.title')}
2131
+ description={t('sections.notes.description')}
2132
+ >
2133
+ <FormField
2134
+ control={form.control}
2135
+ name="descricao"
2136
+ render={({ field }) => (
2137
+ <FormItem>
2138
+ <FormLabel>{t('fields.description')}</FormLabel>
2139
+ <FormControl>
2140
+ <Textarea
2141
+ placeholder={t('newTitle.descriptionPlaceholder')}
2142
+ {...field}
2143
+ value={field.value || ''}
2144
+ />
2145
+ </FormControl>
2146
+ <FormMessage />
2147
+ </FormItem>
2148
+ )}
2149
+ />
2150
+ </FinanceSheetSection>
2151
+ </FinanceSheetBody>
2152
+
2153
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2154
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2155
+ <Button type="button" variant="outline" onClick={handleCancel}>
2156
+ {t('common.cancel')}
2157
+ </Button>
2158
+ <Button
2159
+ type="submit"
2160
+ disabled={
2161
+ form.formState.isSubmitting ||
2162
+ isUploadingFile ||
2163
+ isExtractingFileData
2164
+ }
2165
+ >
2166
+ {(isUploadingFile || isExtractingFileData) && (
2167
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2168
+ )}
2169
+ {isExtractingFileData
2170
+ ? t('common.upload.fillingWithAi')
2171
+ : isUploadingFile
2172
+ ? t('common.upload.uploadingFile')
2173
+ : t('common.save')}
2174
+ </Button>
2175
+ </div>
2176
+ </div>
2177
+ </form>
2094
2178
  </Form>
2095
2179
  </SheetContent>
2096
2180
  </Sheet>
@@ -2138,12 +2222,12 @@ export default function TitulosPagarPage() {
2138
2222
  const centrosCusto = centrosCustoData || [];
2139
2223
 
2140
2224
  const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
2141
- const getCategoriaById = (id?: string) => categorias.find((c) => c.id === id);
2142
-
2143
- const [search, setSearch] = useState('');
2144
- const [statusFilter, setStatusFilter] = useState<string>('');
2145
- const [page, setPage] = useState(1);
2146
- const pageSize = 10;
2225
+ const getCategoriaById = (id?: string) => categorias.find((c) => c.id === id);
2226
+
2227
+ const [search, setSearch] = useState('');
2228
+ const [statusFilter, setStatusFilter] = useState<string>('all');
2229
+ const [page, setPage] = useState(1);
2230
+ const pageSize = 10;
2147
2231
  const [isNewTitleSheetOpen, setIsNewTitleSheetOpen] = useState(false);
2148
2232
  const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
2149
2233
  const [approvingTitleId, setApprovingTitleId] = useState<string | null>(null);
@@ -2178,11 +2262,10 @@ export default function TitulosPagarPage() {
2178
2262
  const normalizedStatusFilter =
2179
2263
  statusFilter && statusFilter !== 'all' ? statusFilter : undefined;
2180
2264
 
2181
- const {
2182
- data: paginatedTitlesResponse,
2183
- refetch: refetchTitles,
2184
- isFetching: isFetchingTitles,
2185
- } = useQuery<{
2265
+ const {
2266
+ data: paginatedTitlesResponse,
2267
+ refetch: refetchTitles,
2268
+ } = useQuery<{
2186
2269
  data: any[];
2187
2270
  total: number;
2188
2271
  page: number;
@@ -2222,11 +2305,76 @@ export default function TitulosPagarPage() {
2222
2305
  },
2223
2306
  placeholderData: (old) => old,
2224
2307
  });
2225
-
2226
- const titulosPagar = paginatedTitlesResponse?.data || [];
2227
-
2228
- useEffect(() => {
2229
- const firstCandidate = settleCandidates[0];
2308
+
2309
+ const titulosPagar = paginatedTitlesResponse?.data || [];
2310
+ const visibleTitlesTotal = useMemo(
2311
+ () =>
2312
+ titulosPagar.reduce(
2313
+ (acc, title) => acc + Number(title?.valorTotal || 0),
2314
+ 0
2315
+ ),
2316
+ [titulosPagar]
2317
+ );
2318
+ const visiblePendingTitles = useMemo(
2319
+ () =>
2320
+ titulosPagar.filter((title) =>
2321
+ ['aberto', 'parcial', 'vencido'].includes(String(title?.status || ''))
2322
+ ).length,
2323
+ [titulosPagar]
2324
+ );
2325
+ const visibleOverdueTitles = useMemo(
2326
+ () =>
2327
+ titulosPagar.filter(
2328
+ (title) =>
2329
+ title?.status === 'vencido' ||
2330
+ (Array.isArray(title?.parcelas) &&
2331
+ title.parcelas.some((installment: any) => installment.status === 'vencido'))
2332
+ ).length,
2333
+ [titulosPagar]
2334
+ );
2335
+ const summaryCards = useMemo(
2336
+ () => [
2337
+ {
2338
+ key: 'visible',
2339
+ title: t('summary.cards.visible.title'),
2340
+ value: titulosPagar.length,
2341
+ description: t('summary.cards.visible.description', {
2342
+ total: paginatedTitlesResponse?.total || 0,
2343
+ }),
2344
+ icon: FileText,
2345
+ layout: 'compact' as const,
2346
+ },
2347
+ {
2348
+ key: 'value',
2349
+ title: t('summary.cards.value.title'),
2350
+ value: <Money value={visibleTitlesTotal} />,
2351
+ description: t('summary.cards.value.description'),
2352
+ icon: Wallet,
2353
+ layout: 'compact' as const,
2354
+ },
2355
+ {
2356
+ key: 'attention',
2357
+ title: t('summary.cards.attention.title'),
2358
+ value: visiblePendingTitles,
2359
+ description: t('summary.cards.attention.description', {
2360
+ overdue: visibleOverdueTitles,
2361
+ }),
2362
+ icon: AlertTriangle,
2363
+ layout: 'compact' as const,
2364
+ },
2365
+ ],
2366
+ [
2367
+ paginatedTitlesResponse?.total,
2368
+ t,
2369
+ titulosPagar.length,
2370
+ visibleOverdueTitles,
2371
+ visiblePendingTitles,
2372
+ visibleTitlesTotal,
2373
+ ]
2374
+ );
2375
+
2376
+ useEffect(() => {
2377
+ const firstCandidate = settleCandidates[0];
2230
2378
 
2231
2379
  settleTitleForm.reset({
2232
2380
  installmentId: firstCandidate?.id || '',
@@ -2469,9 +2617,9 @@ export default function TitulosPagarPage() {
2469
2617
 
2470
2618
  return (
2471
2619
  <Page>
2472
- <PageHeader
2473
- title={t('header.title')}
2474
- description={t('header.description')}
2620
+ <PageHeader
2621
+ title={t('header.title')}
2622
+ description={t('header.description')}
2475
2623
  breadcrumbs={[
2476
2624
  { label: t('breadcrumbs.home'), href: '/' },
2477
2625
  { label: t('breadcrumbs.finance'), href: '/finance' },
@@ -2504,319 +2652,332 @@ export default function TitulosPagarPage() {
2504
2652
  onCategoriesUpdated={refetchCategorias}
2505
2653
  onCostCentersUpdated={refetchCentrosCusto}
2506
2654
  />
2507
- </>
2508
- }
2509
- />
2510
-
2511
- <FilterBar
2512
- searchPlaceholder={t('filters.searchPlaceholder')}
2513
- searchValue={search}
2514
- onSearchChange={setSearch}
2515
- filters={[
2516
- {
2517
- id: 'status',
2518
- label: t('filters.status'),
2519
- value: statusFilter,
2520
- onChange: setStatusFilter,
2521
- options: [
2522
- { value: 'all', label: t('statuses.all') },
2523
- { value: 'rascunho', label: t('statuses.rascunho') },
2524
- { value: 'aberto', label: t('statuses.aberto') },
2525
- { value: 'parcial', label: t('statuses.parcial') },
2526
- { value: 'liquidado', label: t('statuses.liquidado') },
2527
- { value: 'vencido', label: t('statuses.vencido') },
2528
- { value: 'cancelado', label: t('statuses.cancelado') },
2529
- ],
2530
- },
2531
- ]}
2532
- activeFilters={normalizedStatusFilter ? 1 : 0}
2533
- onClearFilters={() => setStatusFilter('all')}
2534
- />
2535
-
2536
- <Sheet
2537
- open={isSettleSheetOpen}
2538
- onOpenChange={(open) => {
2655
+ </>
2656
+ }
2657
+ />
2658
+
2659
+ <KpiCardsGrid items={summaryCards} columns={3} />
2660
+
2661
+ <div className="min-w-0">
2662
+ <SearchBar
2663
+ searchQuery={search}
2664
+ onSearchChange={setSearch}
2665
+ onSearch={() => undefined}
2666
+ placeholder={t('filters.searchPlaceholder')}
2667
+ controls={[
2668
+ {
2669
+ id: 'status',
2670
+ type: 'select',
2671
+ value: statusFilter,
2672
+ onChange: setStatusFilter,
2673
+ placeholder: t('filters.status'),
2674
+ options: [
2675
+ { value: 'all', label: t('statuses.all') },
2676
+ { value: 'rascunho', label: t('statuses.rascunho') },
2677
+ { value: 'aberto', label: t('statuses.aberto') },
2678
+ { value: 'parcial', label: t('statuses.parcial') },
2679
+ { value: 'liquidado', label: t('statuses.liquidado') },
2680
+ { value: 'vencido', label: t('statuses.vencido') },
2681
+ { value: 'cancelado', label: t('statuses.cancelado') },
2682
+ ],
2683
+ },
2684
+ ]}
2685
+ />
2686
+ </div>
2687
+
2688
+ <Sheet
2689
+ open={isSettleSheetOpen}
2690
+ onOpenChange={(open) => {
2539
2691
  setIsSettleSheetOpen(open);
2540
2692
 
2541
2693
  if (!open) {
2542
2694
  setTitleToSettle(null);
2543
- }
2544
- }}
2545
- >
2546
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
2547
- <SheetHeader>
2548
- <SheetTitle>{t('settleSheet.title')}</SheetTitle>
2549
- <SheetDescription>
2550
- {t('settleSheet.description', {
2695
+ }
2696
+ }}
2697
+ >
2698
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl">
2699
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
2700
+ <SheetTitle>{t('settleSheet.title')}</SheetTitle>
2701
+ <SheetDescription>
2702
+ {t('settleSheet.description', {
2551
2703
  document: titleToSettle?.documento || '-',
2552
2704
  })}
2553
2705
  </SheetDescription>
2554
2706
  </SheetHeader>
2555
-
2556
- <Form {...settleTitleForm}>
2557
- <form
2558
- className="space-y-4 px-4"
2559
- onSubmit={settleTitleForm.handleSubmit(handleSubmitSettleTitle)}
2560
- >
2561
- <FormField
2562
- control={settleTitleForm.control}
2563
- name="installmentId"
2564
- render={({ field }) => (
2565
- <FormItem>
2566
- <FormLabel>{t('settleSheet.installmentLabel')}</FormLabel>
2567
- <Select
2568
- value={field.value}
2569
- onValueChange={(value) => {
2570
- field.onChange(value);
2571
-
2572
- const selected = settleCandidates.find(
2573
- (installment: any) => installment.id === value
2574
- );
2575
-
2576
- if (selected) {
2577
- settleTitleForm.setValue(
2578
- 'amount',
2579
- Number(selected.valorAberto || 0),
2580
- { shouldValidate: true }
2581
- );
2582
- }
2583
- }}
2584
- >
2585
- <FormControl>
2586
- <SelectTrigger className="w-full">
2587
- <SelectValue
2588
- placeholder={t(
2589
- 'settleSheet.installmentPlaceholder'
2590
- )}
2591
- />
2592
- </SelectTrigger>
2593
- </FormControl>
2594
- <SelectContent>
2595
- {settleCandidates.map((installment: any) => (
2596
- <SelectItem
2597
- key={installment.id}
2598
- value={installment.id}
2599
- >
2600
- {t('settleSheet.installmentOption', {
2601
- number: installment.numero,
2602
- amount: new Intl.NumberFormat('pt-BR', {
2603
- style: 'currency',
2604
- currency: 'BRL',
2605
- }).format(Number(installment.valorAberto || 0)),
2606
- })}
2607
- </SelectItem>
2608
- ))}
2609
- </SelectContent>
2610
- </Select>
2611
- <FormMessage />
2612
- </FormItem>
2613
- )}
2614
- />
2615
-
2616
- <FormField
2617
- control={settleTitleForm.control}
2618
- name="amount"
2619
- render={({ field }) => (
2620
- <FormItem>
2621
- <FormLabel>{t('settleSheet.amountLabel')}</FormLabel>
2622
- <FormControl>
2623
- <InputMoney
2624
- value={Number(field.value || 0)}
2625
- onValueChange={(value) => {
2626
- field.onChange(Number(value || 0));
2627
- }}
2628
- />
2629
- </FormControl>
2630
- <FormMessage />
2631
- </FormItem>
2632
- )}
2633
- />
2634
-
2635
- <FormField
2636
- control={settleTitleForm.control}
2637
- name="description"
2638
- render={({ field }) => (
2639
- <FormItem>
2640
- <FormLabel>{t('settleSheet.descriptionLabel')}</FormLabel>
2641
- <FormControl>
2642
- <Input {...field} value={field.value || ''} />
2643
- </FormControl>
2644
- <FormMessage />
2645
- </FormItem>
2646
- )}
2647
- />
2648
-
2649
- <Button
2650
- className="w-full"
2651
- type="submit"
2652
- disabled={isSettlingTitle}
2653
- >
2654
- {t('settleSheet.confirm')}
2655
- </Button>
2656
- </form>
2657
- </Form>
2658
- </SheetContent>
2707
+
2708
+ <Form {...settleTitleForm}>
2709
+ <form
2710
+ className="flex h-full flex-col overflow-hidden"
2711
+ onSubmit={settleTitleForm.handleSubmit(handleSubmitSettleTitle)}
2712
+ >
2713
+ <FinanceSheetBody>
2714
+ <FinanceSheetSection
2715
+ title={t('settleSheet.sections.payment.title')}
2716
+ description={t('settleSheet.sections.payment.description')}
2717
+ >
2718
+ <FormField
2719
+ control={settleTitleForm.control}
2720
+ name="installmentId"
2721
+ render={({ field }) => (
2722
+ <FormItem>
2723
+ <FormLabel>{t('settleSheet.installmentLabel')}</FormLabel>
2724
+ <Select
2725
+ value={field.value}
2726
+ onValueChange={(value) => {
2727
+ field.onChange(value);
2728
+
2729
+ const selected = settleCandidates.find(
2730
+ (installment: any) => installment.id === value
2731
+ );
2732
+
2733
+ if (selected) {
2734
+ settleTitleForm.setValue(
2735
+ 'amount',
2736
+ Number(selected.valorAberto || 0),
2737
+ { shouldValidate: true }
2738
+ );
2739
+ }
2740
+ }}
2741
+ >
2742
+ <FormControl>
2743
+ <SelectTrigger className="w-full">
2744
+ <SelectValue
2745
+ placeholder={t(
2746
+ 'settleSheet.installmentPlaceholder'
2747
+ )}
2748
+ />
2749
+ </SelectTrigger>
2750
+ </FormControl>
2751
+ <SelectContent>
2752
+ {settleCandidates.map((installment: any) => (
2753
+ <SelectItem
2754
+ key={installment.id}
2755
+ value={installment.id}
2756
+ >
2757
+ {t('settleSheet.installmentOption', {
2758
+ number: installment.numero,
2759
+ amount: new Intl.NumberFormat('pt-BR', {
2760
+ style: 'currency',
2761
+ currency: 'BRL',
2762
+ }).format(Number(installment.valorAberto || 0)),
2763
+ })}
2764
+ </SelectItem>
2765
+ ))}
2766
+ </SelectContent>
2767
+ </Select>
2768
+ <FormMessage />
2769
+ </FormItem>
2770
+ )}
2771
+ />
2772
+
2773
+ <div className="grid gap-4 sm:grid-cols-2">
2774
+ <FormField
2775
+ control={settleTitleForm.control}
2776
+ name="amount"
2777
+ render={({ field }) => (
2778
+ <FormItem>
2779
+ <FormLabel>{t('settleSheet.amountLabel')}</FormLabel>
2780
+ <FormControl>
2781
+ <InputMoney
2782
+ value={Number(field.value || 0)}
2783
+ onValueChange={(value) => {
2784
+ field.onChange(Number(value || 0));
2785
+ }}
2786
+ />
2787
+ </FormControl>
2788
+ <FormMessage />
2789
+ </FormItem>
2790
+ )}
2791
+ />
2792
+
2793
+ <FormField
2794
+ control={settleTitleForm.control}
2795
+ name="description"
2796
+ render={({ field }) => (
2797
+ <FormItem>
2798
+ <FormLabel>
2799
+ {t('settleSheet.descriptionLabel')}
2800
+ </FormLabel>
2801
+ <FormControl>
2802
+ <Input {...field} value={field.value || ''} />
2803
+ </FormControl>
2804
+ <FormMessage />
2805
+ </FormItem>
2806
+ )}
2807
+ />
2808
+ </div>
2809
+ </FinanceSheetSection>
2810
+ </FinanceSheetBody>
2811
+
2812
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2813
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2814
+ <Button
2815
+ type="button"
2816
+ variant="outline"
2817
+ onClick={() => setIsSettleSheetOpen(false)}
2818
+ >
2819
+ {t('common.cancel')}
2820
+ </Button>
2821
+ <Button type="submit" disabled={isSettlingTitle}>
2822
+ {t('settleSheet.confirm')}
2823
+ </Button>
2824
+ </div>
2825
+ </div>
2826
+ </form>
2827
+ </Form>
2828
+ </SheetContent>
2659
2829
  </Sheet>
2660
2830
 
2661
- {titulosPagar.length > 0 ? (
2662
- <div className="rounded-md border">
2663
- <Table>
2664
- <TableHeader>
2665
- <TableRow>
2666
- <TableHead>{t('table.headers.document')}</TableHead>
2667
- <TableHead>{t('table.headers.supplier')}</TableHead>
2668
- <TableHead>{t('table.headers.competency')}</TableHead>
2669
- <TableHead>{t('table.headers.dueDate')}</TableHead>
2670
- <TableHead className="text-right">
2671
- {t('table.headers.value')}
2672
- </TableHead>
2673
- <TableHead>{t('table.headers.category')}</TableHead>
2674
- <TableHead>{t('table.headers.status')}</TableHead>
2675
- <TableHead className="w-[50px]" />
2676
- </TableRow>
2677
- </TableHeader>
2678
- <TableBody>
2679
- {titulosPagar.map((titulo) => {
2680
- const fornecedor = getPessoaById(titulo.fornecedorId);
2681
- const categoria = getCategoriaById(titulo.categoriaId);
2682
- const proximaParcela = titulo.parcelas.find(
2683
- (p: any) => p.status === 'aberto' || p.status === 'vencido'
2684
- );
2685
-
2686
- return (
2687
- <TableRow key={titulo.id}>
2688
- <TableCell className="font-medium">
2689
- <Link
2690
- href={`/finance/accounts-payable/installments/${titulo.id}`}
2691
- className="hover:underline"
2692
- >
2693
- {titulo.documento}
2694
- </Link>
2695
- {titulo.anexos.length > 0 && (
2696
- <Button
2697
- type="button"
2698
- variant="ghost"
2699
- size="icon"
2700
- className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2701
- onClick={(event) => {
2702
- event.preventDefault();
2703
- event.stopPropagation();
2704
- const firstAttachmentId =
2705
- titulo.anexosDetalhes?.[0]?.id;
2706
- void handleOpenAttachment(firstAttachmentId);
2707
- }}
2708
- aria-label={t('table.actions.openAttachment')}
2709
- >
2710
- <Paperclip className="h-3 w-3" />
2711
- </Button>
2712
- )}
2713
- </TableCell>
2714
- <TableCell>{fornecedor?.nome}</TableCell>
2715
- <TableCell>{titulo.competencia}</TableCell>
2716
- <TableCell>
2717
- {proximaParcela
2718
- ? formatarData(proximaParcela.vencimento)
2719
- : '-'}
2720
- </TableCell>
2721
- <TableCell className="text-right">
2722
- <Money value={titulo.valorTotal} />
2723
- </TableCell>
2724
- <TableCell>{categoria?.nome}</TableCell>
2725
- <TableCell>
2726
- <StatusBadge status={titulo.status} />
2727
- </TableCell>
2728
- <TableCell>
2729
- <FinanceTitleActionsMenu
2730
- triggerVariant="ghost"
2731
- detailHref={`/finance/accounts-payable/installments/${titulo.id}`}
2732
- canEdit={canEditTitle(titulo.status)}
2733
- canApprove={canApproveTitle(titulo.status)}
2734
- canSettle={canSettleTitle(titulo.status)}
2735
- canReverse={
2736
- canReverseTitle(titulo.status) &&
2737
- !!getFirstActiveSettlementId(titulo)
2738
- }
2739
- canCancel={canCancelTitle(titulo.status)}
2740
- isApproving={approvingTitleId === titulo.id}
2741
- isReversing={reversingTitleId === titulo.id}
2742
- isCanceling={cancelingTitleId === titulo.id}
2743
- labels={{
2744
- menu: t.has('actions.title')
2745
- ? t('actions.title')
2746
- : t('table.actions.srActions'),
2747
- srActions: t('table.actions.srActions'),
2748
- viewDetails: t('table.actions.viewDetails'),
2749
- edit: t('table.actions.edit'),
2750
- approve: t('table.actions.approve'),
2751
- settle: t('table.actions.settle'),
2752
- reverse: t('table.actions.reverse'),
2753
- cancel: t('table.actions.cancel'),
2754
- }}
2755
- dialogs={{
2756
- cancelTitle: t('dialogs.cancel.title'),
2757
- cancelDescription: t('dialogs.cancel.description'),
2758
- cancelButton: t('dialogs.cancel.cancel'),
2759
- confirmCancelButton: t('dialogs.cancel.confirm'),
2760
- reverseTitle: t('dialogs.reverse.title'),
2761
- reverseDescription: t('dialogs.reverse.description'),
2762
- reverseReasonLabel: t('dialogs.reverse.reasonLabel'),
2763
- reverseReasonPlaceholder: t(
2764
- 'dialogs.reverse.reasonPlaceholder'
2765
- ),
2766
- reverseButton: t('dialogs.reverse.cancel'),
2767
- confirmReverseButton: t('dialogs.reverse.confirm'),
2768
- }}
2769
- onEdit={() => setEditingTitleId(titulo.id)}
2770
- onApprove={() => void handleApproveTitle(titulo.id)}
2771
- onSettle={() => handleSettleTitle(titulo)}
2772
- onReverse={(reason) =>
2773
- handleReverseTitle(titulo, reason)
2774
- }
2775
- onCancel={() => handleCancelTitle(titulo.id)}
2776
- />
2777
- </TableCell>
2778
- </TableRow>
2779
- );
2780
- })}
2781
- </TableBody>
2782
- </Table>
2783
- </div>
2784
- ) : (
2785
- <EmptyState
2786
- icon={<FileText className="h-12 w-12" />}
2787
- title={t('empty.title')}
2788
- description={t('empty.description')}
2789
- actionLabel={t('newTitle.action')}
2790
- onAction={() => setIsNewTitleSheetOpen(true)}
2791
- />
2792
- )}
2793
-
2794
- <div className="flex items-center justify-between">
2795
- <p className="text-sm text-muted-foreground">
2796
- {t('footer.showing', {
2797
- filtered: titulosPagar.length,
2798
- total: paginatedTitlesResponse?.total || 0,
2799
- })}
2800
- </p>
2801
- <div className="flex items-center gap-2">
2802
- <Button
2803
- variant="outline"
2804
- size="sm"
2805
- disabled={!paginatedTitlesResponse?.prev || isFetchingTitles}
2806
- onClick={() => setPage((current) => Math.max(1, current - 1))}
2807
- >
2808
- {t('footer.previous')}
2809
- </Button>
2810
- <Button
2811
- variant="outline"
2812
- size="sm"
2813
- disabled={!paginatedTitlesResponse?.next || isFetchingTitles}
2814
- onClick={() => setPage((current) => current + 1)}
2815
- >
2816
- {t('footer.next')}
2817
- </Button>
2818
- </div>
2819
- </div>
2820
- </Page>
2821
- );
2822
- }
2831
+ <FinancePageSection
2832
+ title={t('list.title')}
2833
+ description={t('list.description')}
2834
+ >
2835
+ {titulosPagar.length > 0 ? (
2836
+ <div className="overflow-x-auto">
2837
+ <Table className="min-w-[760px]">
2838
+ <TableHeader>
2839
+ <TableRow>
2840
+ <TableHead>{t('table.headers.document')}</TableHead>
2841
+ <TableHead>{t('table.headers.supplier')}</TableHead>
2842
+ <TableHead>{t('table.headers.competency')}</TableHead>
2843
+ <TableHead>{t('table.headers.dueDate')}</TableHead>
2844
+ <TableHead className="text-right">
2845
+ {t('table.headers.value')}
2846
+ </TableHead>
2847
+ <TableHead>{t('table.headers.category')}</TableHead>
2848
+ <TableHead>{t('table.headers.status')}</TableHead>
2849
+ <TableHead className="w-[50px]" />
2850
+ </TableRow>
2851
+ </TableHeader>
2852
+ <TableBody>
2853
+ {titulosPagar.map((titulo) => {
2854
+ const fornecedor = getPessoaById(titulo.fornecedorId);
2855
+ const categoria = getCategoriaById(titulo.categoriaId);
2856
+ const proximaParcela = titulo.parcelas.find(
2857
+ (p: any) => p.status === 'aberto' || p.status === 'vencido'
2858
+ );
2859
+
2860
+ return (
2861
+ <TableRow key={titulo.id} className="hover:bg-muted/30">
2862
+ <TableCell className="font-medium">
2863
+ <Link
2864
+ href={`/finance/accounts-payable/installments/${titulo.id}`}
2865
+ className="cursor-pointer hover:underline"
2866
+ >
2867
+ {titulo.documento}
2868
+ </Link>
2869
+ {titulo.anexos.length > 0 && (
2870
+ <Button
2871
+ type="button"
2872
+ variant="ghost"
2873
+ size="icon"
2874
+ className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2875
+ onClick={(event) => {
2876
+ event.preventDefault();
2877
+ event.stopPropagation();
2878
+ const firstAttachmentId =
2879
+ titulo.anexosDetalhes?.[0]?.id;
2880
+ void handleOpenAttachment(firstAttachmentId);
2881
+ }}
2882
+ aria-label={t('table.actions.openAttachment')}
2883
+ >
2884
+ <Paperclip className="h-3 w-3" />
2885
+ </Button>
2886
+ )}
2887
+ </TableCell>
2888
+ <TableCell>{fornecedor?.nome}</TableCell>
2889
+ <TableCell>{titulo.competencia}</TableCell>
2890
+ <TableCell>
2891
+ {proximaParcela
2892
+ ? formatarData(proximaParcela.vencimento)
2893
+ : '-'}
2894
+ </TableCell>
2895
+ <TableCell className="text-right">
2896
+ <Money value={titulo.valorTotal} />
2897
+ </TableCell>
2898
+ <TableCell>{categoria?.nome}</TableCell>
2899
+ <TableCell>
2900
+ <StatusBadge status={titulo.status} />
2901
+ </TableCell>
2902
+ <TableCell>
2903
+ <FinanceTitleActionsMenu
2904
+ triggerVariant="ghost"
2905
+ detailHref={`/finance/accounts-payable/installments/${titulo.id}`}
2906
+ canEdit={canEditTitle(titulo.status)}
2907
+ canApprove={canApproveTitle(titulo.status)}
2908
+ canSettle={canSettleTitle(titulo.status)}
2909
+ canReverse={
2910
+ canReverseTitle(titulo.status) &&
2911
+ !!getFirstActiveSettlementId(titulo)
2912
+ }
2913
+ canCancel={canCancelTitle(titulo.status)}
2914
+ isApproving={approvingTitleId === titulo.id}
2915
+ isReversing={reversingTitleId === titulo.id}
2916
+ isCanceling={cancelingTitleId === titulo.id}
2917
+ labels={{
2918
+ menu: t.has('actions.title')
2919
+ ? t('actions.title')
2920
+ : t('table.actions.srActions'),
2921
+ srActions: t('table.actions.srActions'),
2922
+ viewDetails: t('table.actions.viewDetails'),
2923
+ edit: t('table.actions.edit'),
2924
+ approve: t('table.actions.approve'),
2925
+ settle: t('table.actions.settle'),
2926
+ reverse: t('table.actions.reverse'),
2927
+ cancel: t('table.actions.cancel'),
2928
+ }}
2929
+ dialogs={{
2930
+ cancelTitle: t('dialogs.cancel.title'),
2931
+ cancelDescription: t('dialogs.cancel.description'),
2932
+ cancelButton: t('dialogs.cancel.cancel'),
2933
+ confirmCancelButton: t('dialogs.cancel.confirm'),
2934
+ reverseTitle: t('dialogs.reverse.title'),
2935
+ reverseDescription: t('dialogs.reverse.description'),
2936
+ reverseReasonLabel: t('dialogs.reverse.reasonLabel'),
2937
+ reverseReasonPlaceholder: t(
2938
+ 'dialogs.reverse.reasonPlaceholder'
2939
+ ),
2940
+ reverseButton: t('dialogs.reverse.cancel'),
2941
+ confirmReverseButton: t('dialogs.reverse.confirm'),
2942
+ }}
2943
+ onEdit={() => setEditingTitleId(titulo.id)}
2944
+ onApprove={() => void handleApproveTitle(titulo.id)}
2945
+ onSettle={() => handleSettleTitle(titulo)}
2946
+ onReverse={(reason) =>
2947
+ handleReverseTitle(titulo, reason)
2948
+ }
2949
+ onCancel={() => handleCancelTitle(titulo.id)}
2950
+ />
2951
+ </TableCell>
2952
+ </TableRow>
2953
+ );
2954
+ })}
2955
+ </TableBody>
2956
+ </Table>
2957
+ </div>
2958
+ ) : (
2959
+ <div className="px-4 py-8 sm:px-6">
2960
+ <EmptyState
2961
+ icon={<FileText className="h-12 w-12" />}
2962
+ title={t('empty.title')}
2963
+ description={t('empty.description')}
2964
+ actionLabel={t('newTitle.action')}
2965
+ onAction={() => setIsNewTitleSheetOpen(true)}
2966
+ />
2967
+ </div>
2968
+ )}
2969
+
2970
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2971
+ <PaginationFooter
2972
+ currentPage={page}
2973
+ pageSize={pageSize}
2974
+ totalItems={paginatedTitlesResponse?.total || 0}
2975
+ onPageChange={setPage}
2976
+ onPageSizeChange={() => undefined}
2977
+ pageSizeOptions={[10]}
2978
+ />
2979
+ </div>
2980
+ </FinancePageSection>
2981
+ </Page>
2982
+ );
2983
+ }