@hed-hog/finance 0.0.300 → 0.0.302

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