@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,8 +4,19 @@ 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 { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
8
- import { Page, PageHeader } from '@/components/entity-list';
13
+ import {
14
+ EmptyState,
15
+ Page,
16
+ PageHeader,
17
+ PaginationFooter,
18
+ SearchBar,
19
+ } from '@/components/entity-list';
9
20
  import { Badge } from '@/components/ui/badge';
10
21
  import { Button } from '@/components/ui/button';
11
22
  import { Checkbox } from '@/components/ui/checkbox';
@@ -16,7 +27,6 @@ import {
16
27
  DropdownMenuSeparator,
17
28
  DropdownMenuTrigger,
18
29
  } from '@/components/ui/dropdown-menu';
19
- import { FilterBar } from '@/components/ui/filter-bar';
20
30
  import {
21
31
  Form,
22
32
  FormControl,
@@ -27,6 +37,7 @@ import {
27
37
  } from '@/components/ui/form';
28
38
  import { Input } from '@/components/ui/input';
29
39
  import { InputMoney } from '@/components/ui/input-money';
40
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
30
41
  import { Label } from '@/components/ui/label';
31
42
  import { Money } from '@/components/ui/money';
32
43
  import { Progress } from '@/components/ui/progress';
@@ -63,9 +74,11 @@ import {
63
74
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
64
75
  import { zodResolver } from '@hookform/resolvers/zod';
65
76
  import {
77
+ AlertTriangle,
66
78
  Download,
67
79
  Edit,
68
80
  Eye,
81
+ FileText,
69
82
  Loader2,
70
83
  MoreHorizontal,
71
84
  Paperclip,
@@ -73,6 +86,7 @@ import {
73
86
  Send,
74
87
  Trash2,
75
88
  Upload,
89
+ Wallet,
76
90
  } from 'lucide-react';
77
91
  import { useTranslations } from 'next-intl';
78
92
  import Link from 'next/link';
@@ -628,485 +642,520 @@ function NovoTituloSheet({
628
642
  {t('newTitle.action')}
629
643
  </Button>
630
644
  </SheetTrigger>
631
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
632
- <SheetHeader>
645
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl gap-0">
646
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
633
647
  <SheetTitle>{t('newTitle.title')}</SheetTitle>
634
648
  <SheetDescription>{t('newTitle.description')}</SheetDescription>
635
649
  </SheetHeader>
636
650
  <Form {...form}>
637
- <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
638
- <div className="grid gap-3">
639
- <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
640
- <div className="grid gap-2">
641
- <FormLabel>{t('common.upload.label')}</FormLabel>
642
- <Input
643
- ref={fileInputRef}
644
- className="hidden"
645
- type="file"
646
- accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
647
- onChange={(event) => {
648
- const file = event.target.files?.[0];
649
- if (!file) {
650
- return;
651
+ <form
652
+ className="flex h-full flex-col overflow-hidden"
653
+ onSubmit={form.handleSubmit(handleSubmit)}
654
+ >
655
+ <FinanceSheetBody className="pt-0">
656
+ <FinanceSheetSection className="pt-0 mt-0">
657
+ <div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
658
+ <div className="grid gap-2">
659
+ <FormLabel>{t('common.upload.label')}</FormLabel>
660
+ <Input
661
+ ref={fileInputRef}
662
+ className="hidden"
663
+ type="file"
664
+ accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
665
+ onChange={(event) => {
666
+ const file = event.target.files?.[0];
667
+ if (!file) {
668
+ return;
669
+ }
670
+
671
+ clearUploadedFile();
672
+ void uploadRelatedFile(file);
673
+ }}
674
+ disabled={
675
+ isUploadingFile ||
676
+ isExtractingFileData ||
677
+ form.formState.isSubmitting
651
678
  }
679
+ />
652
680
 
653
- clearUploadedFile();
654
- void uploadRelatedFile(file);
655
- }}
656
- disabled={
657
- isUploadingFile ||
658
- isExtractingFileData ||
659
- form.formState.isSubmitting
660
- }
661
- />
662
-
663
- <div className="grid w-full grid-cols-2 gap-2">
664
- <Tooltip>
665
- <TooltipTrigger asChild>
666
- <Button
667
- type="button"
668
- variant="outline"
669
- className={
670
- uploadedFileId ? 'w-full' : 'col-span-2 w-full'
671
- }
672
- onClick={handleSelectFile}
673
- aria-label={
674
- uploadedFileId
675
- ? t('common.upload.change')
676
- : t('common.upload.upload')
677
- }
678
- disabled={
679
- isUploadingFile ||
680
- isExtractingFileData ||
681
- form.formState.isSubmitting
682
- }
683
- >
684
- {uploadedFileId ? (
685
- <Upload className="h-4 w-4" />
686
- ) : (
687
- <>
688
- <Upload className="mr-2 h-4 w-4" />
689
- {t('common.upload.upload')}
690
- </>
691
- )}
692
- </Button>
693
- </TooltipTrigger>
694
- <TooltipContent>
695
- {uploadedFileId
696
- ? t('common.upload.change')
697
- : t('common.upload.upload')}
698
- </TooltipContent>
699
- </Tooltip>
700
-
701
- {uploadedFileId && (
681
+ <div className="grid w-full grid-cols-2 gap-2">
702
682
  <Tooltip>
703
683
  <TooltipTrigger asChild>
704
684
  <Button
705
685
  type="button"
706
686
  variant="outline"
707
- className="w-full"
708
- onClick={clearUploadedFile}
709
- aria-label={t('common.upload.remove')}
687
+ className={
688
+ uploadedFileId ? 'w-full' : 'col-span-2 w-full'
689
+ }
690
+ onClick={handleSelectFile}
691
+ aria-label={
692
+ uploadedFileId
693
+ ? t('common.upload.change')
694
+ : t('common.upload.upload')
695
+ }
710
696
  disabled={
711
697
  isUploadingFile ||
712
698
  isExtractingFileData ||
713
699
  form.formState.isSubmitting
714
700
  }
715
701
  >
716
- <Trash2 className="h-4 w-4" />
702
+ {uploadedFileId ? (
703
+ <Upload className="h-4 w-4" />
704
+ ) : (
705
+ <>
706
+ <Upload className="mr-2 h-4 w-4" />
707
+ {t('common.upload.upload')}
708
+ </>
709
+ )}
717
710
  </Button>
718
711
  </TooltipTrigger>
719
712
  <TooltipContent>
720
- {t('common.upload.remove')}
713
+ {uploadedFileId
714
+ ? t('common.upload.change')
715
+ : t('common.upload.upload')}
721
716
  </TooltipContent>
722
717
  </Tooltip>
723
- )}
724
- </div>
725
718
 
726
- <div className="space-y-1">
727
- {uploadedFileId && (
728
- <p className="truncate text-xs text-muted-foreground">
729
- {t('common.upload.selectedPrefix')} {uploadedFileName}
730
- </p>
731
- )}
719
+ {uploadedFileId && (
720
+ <Tooltip>
721
+ <TooltipTrigger asChild>
722
+ <Button
723
+ type="button"
724
+ variant="outline"
725
+ className="w-full"
726
+ onClick={clearUploadedFile}
727
+ aria-label={t('common.upload.remove')}
728
+ disabled={
729
+ isUploadingFile ||
730
+ isExtractingFileData ||
731
+ form.formState.isSubmitting
732
+ }
733
+ >
734
+ <Trash2 className="h-4 w-4" />
735
+ </Button>
736
+ </TooltipTrigger>
737
+ <TooltipContent>
738
+ {t('common.upload.remove')}
739
+ </TooltipContent>
740
+ </Tooltip>
741
+ )}
742
+ </div>
732
743
 
733
- {isUploadingFile && !isExtractingFileData && (
734
- <div className="space-y-1">
735
- <Progress value={uploadProgress} className="h-2" />
736
- <p className="text-xs text-muted-foreground">
737
- {t('common.upload.uploadingProgress', {
738
- progress: uploadProgress,
739
- })}
744
+ <div className="space-y-1 rounded-lg border border-dashed border-border/70 bg-muted/20 p-3">
745
+ {uploadedFileId && (
746
+ <p className="truncate text-xs text-muted-foreground">
747
+ {t('common.upload.selectedPrefix')} {uploadedFileName}
740
748
  </p>
741
- </div>
742
- )}
749
+ )}
743
750
 
744
- {isExtractingFileData && (
745
- <p className="flex items-center gap-2 text-xs text-primary">
746
- <Loader2 className="h-4 w-4 animate-spin" />
747
- {t('common.upload.processingAi')}
748
- </p>
749
- )}
751
+ {isUploadingFile && !isExtractingFileData && (
752
+ <div className="space-y-1">
753
+ <Progress value={uploadProgress} className="h-2" />
754
+ <p className="text-xs text-muted-foreground">
755
+ {t('common.upload.uploadingProgress', {
756
+ progress: uploadProgress,
757
+ })}
758
+ </p>
759
+ </div>
760
+ )}
750
761
 
751
- {!isExtractingFileData &&
752
- extractionConfidence !== null &&
753
- extractionConfidence < 70 && (
754
- <p className="text-xs text-destructive">
755
- {t('common.upload.lowConfidence', {
756
- confidence: Math.round(extractionConfidence),
757
- })}
762
+ {isExtractingFileData && (
763
+ <p className="flex items-center gap-2 text-xs text-primary">
764
+ <Loader2 className="h-4 w-4 animate-spin" />
765
+ {t('common.upload.processingAi')}
758
766
  </p>
759
767
  )}
760
768
 
761
- {!isExtractingFileData && extractionWarnings.length > 0 && (
762
- <p className="truncate text-xs text-muted-foreground">
763
- {extractionWarnings[0]}
764
- </p>
765
- )}
769
+ {!isExtractingFileData &&
770
+ extractionConfidence !== null &&
771
+ extractionConfidence < 70 && (
772
+ <p className="text-xs text-destructive">
773
+ {t('common.upload.lowConfidence', {
774
+ confidence: Math.round(extractionConfidence),
775
+ })}
776
+ </p>
777
+ )}
778
+
779
+ {!isExtractingFileData &&
780
+ extractionWarnings.length > 0 && (
781
+ <p className="truncate text-xs text-muted-foreground">
782
+ {extractionWarnings[0]}
783
+ </p>
784
+ )}
785
+ </div>
766
786
  </div>
787
+
788
+ <FormField
789
+ control={form.control}
790
+ name="documento"
791
+ render={({ field }) => (
792
+ <FormItem>
793
+ <FormLabel>{t('fields.document')}</FormLabel>
794
+ <FormControl>
795
+ <Input placeholder="FAT-00000" {...field} />
796
+ </FormControl>
797
+ <FormMessage />
798
+ </FormItem>
799
+ )}
800
+ />
767
801
  </div>
768
802
 
769
- <FormField
770
- control={form.control}
771
- name="documento"
772
- render={({ field }) => (
773
- <FormItem>
774
- <FormLabel>{t('fields.document')}</FormLabel>
775
- <FormControl>
776
- <Input placeholder="FAT-00000" {...field} />
777
- </FormControl>
778
- <FormMessage />
779
- </FormItem>
780
- )}
803
+ <PersonFieldWithCreate
804
+ form={form}
805
+ name="clienteId"
806
+ label={t('fields.client')}
807
+ entityLabel="cliente"
808
+ selectPlaceholder={t('common.select')}
781
809
  />
782
- </div>
783
810
 
784
- <PersonFieldWithCreate
785
- form={form}
786
- name="clienteId"
787
- label={t('fields.client')}
788
- entityLabel="cliente"
789
- selectPlaceholder={t('common.select')}
790
- />
811
+ <div className="grid gap-4 xl:grid-cols-2">
812
+ <div className="grid gap-4 md:grid-cols-2">
813
+ <FormField
814
+ control={form.control}
815
+ name="competencia"
816
+ render={({ field }) => (
817
+ <FormItem>
818
+ <FormLabel>{t('fields.competency')}</FormLabel>
819
+ <FormControl>
820
+ <Input
821
+ type="month"
822
+ {...field}
823
+ value={field.value || ''}
824
+ />
825
+ </FormControl>
826
+ <FormMessage />
827
+ </FormItem>
828
+ )}
829
+ />
830
+
831
+ <FormField
832
+ control={form.control}
833
+ name="vencimento"
834
+ render={({ field }) => (
835
+ <FormItem>
836
+ <FormLabel>{t('fields.dueDate')}</FormLabel>
837
+ <FormControl>
838
+ <Input
839
+ type="date"
840
+ {...field}
841
+ value={field.value || ''}
842
+ />
843
+ </FormControl>
844
+ <FormMessage />
845
+ </FormItem>
846
+ )}
847
+ />
848
+ </div>
849
+
850
+ <div className="grid gap-4 md:grid-cols-2">
851
+ <FormField
852
+ control={form.control}
853
+ name="valor"
854
+ render={({ field }) => (
855
+ <FormItem>
856
+ <FormLabel>{t('fields.totalValue')}</FormLabel>
857
+ <FormControl>
858
+ <InputMoney
859
+ ref={field.ref}
860
+ name={field.name}
861
+ value={field.value}
862
+ onBlur={field.onBlur}
863
+ onValueChange={(value) =>
864
+ field.onChange(value ?? 0)
865
+ }
866
+ placeholder="0,00"
867
+ />
868
+ </FormControl>
869
+ <FormMessage />
870
+ </FormItem>
871
+ )}
872
+ />
873
+
874
+ <FormField
875
+ control={form.control}
876
+ name="installmentsCount"
877
+ render={({ field }) => (
878
+ <FormItem>
879
+ <FormLabel>
880
+ {t('installmentsEditor.countLabel')}
881
+ </FormLabel>
882
+ <FormControl>
883
+ <Input
884
+ type="number"
885
+ min={1}
886
+ max={120}
887
+ value={field.value}
888
+ onChange={(event) => {
889
+ const nextValue = Number(
890
+ event.target.value || 1
891
+ );
892
+ field.onChange(
893
+ Number.isNaN(nextValue) ? 1 : nextValue
894
+ );
895
+ }}
896
+ />
897
+ </FormControl>
898
+ <FormMessage />
899
+ </FormItem>
900
+ )}
901
+ />
902
+ </div>
903
+ </div>
791
904
 
792
- <div className="grid grid-cols-2 items-start gap-3">
793
- <FormField
794
- control={form.control}
795
- name="competencia"
796
- render={({ field }) => (
797
- <FormItem>
798
- <FormLabel>{t('fields.competency')}</FormLabel>
799
- <FormControl>
800
- <Input
801
- type="month"
802
- {...field}
803
- value={field.value || ''}
804
- />
805
- </FormControl>
806
- <FormMessage />
807
- </FormItem>
905
+ <div className="space-y-3 rounded-md border p-3">
906
+ <div className="flex items-center justify-between gap-2">
907
+ <p className="text-sm font-medium">
908
+ {t('installmentsEditor.title')}
909
+ </p>
910
+ <Button
911
+ type="button"
912
+ variant="outline"
913
+ size="sm"
914
+ onClick={() => {
915
+ setIsInstallmentsEdited(false);
916
+ replaceInstallments(
917
+ buildEqualInstallments(
918
+ form.getValues('installmentsCount'),
919
+ form.getValues('valor'),
920
+ form.getValues('vencimento')
921
+ )
922
+ );
923
+ }}
924
+ >
925
+ {t('installmentsEditor.recalculate')}
926
+ </Button>
927
+ </div>
928
+
929
+ <div className="flex items-center gap-2">
930
+ <Checkbox
931
+ id="auto-redistribute-installments-receivable"
932
+ checked={autoRedistributeInstallments}
933
+ onCheckedChange={(checked) =>
934
+ setAutoRedistributeInstallments(checked === true)
935
+ }
936
+ />
937
+ <Label
938
+ htmlFor="auto-redistribute-installments-receivable"
939
+ className="text-xs text-muted-foreground"
940
+ >
941
+ {t('installmentsEditor.autoRedistributeLabel')}
942
+ </Label>
943
+ </div>
944
+ {autoRedistributeInstallments && (
945
+ <p className="text-xs text-muted-foreground">
946
+ {t('installmentsEditor.autoRedistributeHint')}
947
+ </p>
808
948
  )}
809
- />
810
949
 
811
- <FormField
812
- control={form.control}
813
- name="vencimento"
814
- render={({ field }) => (
815
- <FormItem>
816
- <FormLabel>{t('fields.dueDate')}</FormLabel>
817
- <FormControl>
818
- <Input
819
- type="date"
820
- {...field}
821
- value={field.value || ''}
950
+ <div className="space-y-2">
951
+ {installmentFields.map((installment, index) => (
952
+ <div
953
+ key={installment.id}
954
+ className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
955
+ >
956
+ <div className="flex items-center text-sm text-muted-foreground">
957
+ #{index + 1}
958
+ </div>
959
+
960
+ <FormField
961
+ control={form.control}
962
+ name={`installments.${index}.dueDate` as const}
963
+ render={({ field }) => (
964
+ <FormItem>
965
+ <FormLabel className="text-xs">
966
+ {t('installmentsEditor.dueDateLabel')}
967
+ </FormLabel>
968
+ <FormControl>
969
+ <Input
970
+ type="date"
971
+ {...field}
972
+ value={field.value || ''}
973
+ onChange={(event) => {
974
+ setIsInstallmentsEdited(true);
975
+ field.onChange(event);
976
+ }}
977
+ />
978
+ </FormControl>
979
+ <FormMessage />
980
+ </FormItem>
981
+ )}
822
982
  />
823
- </FormControl>
824
- <FormMessage />
825
- </FormItem>
826
- )}
827
- />
828
- </div>
829
983
 
830
- <div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
831
- <FormField
832
- control={form.control}
833
- name="valor"
834
- render={({ field }) => (
835
- <FormItem>
836
- <FormLabel>{t('fields.totalValue')}</FormLabel>
837
- <FormControl>
838
- <InputMoney
839
- ref={field.ref}
840
- name={field.name}
841
- value={field.value}
842
- onBlur={field.onBlur}
843
- onValueChange={(value) => field.onChange(value ?? 0)}
844
- placeholder="0,00"
984
+ <FormField
985
+ control={form.control}
986
+ name={`installments.${index}.amount` as const}
987
+ render={({ field }) => (
988
+ <FormItem>
989
+ <FormLabel className="text-xs">
990
+ {t('installmentsEditor.amountLabel')}
991
+ </FormLabel>
992
+ <FormControl>
993
+ <InputMoney
994
+ ref={field.ref}
995
+ name={field.name}
996
+ value={field.value}
997
+ onBlur={() => {
998
+ field.onBlur();
999
+
1000
+ if (!autoRedistributeInstallments) {
1001
+ return;
1002
+ }
1003
+
1004
+ clearScheduledRedistribution(index);
1005
+ runInstallmentRedistribution(index);
1006
+ }}
1007
+ onValueChange={(value) => {
1008
+ setIsInstallmentsEdited(true);
1009
+ field.onChange(value ?? 0);
1010
+
1011
+ if (!autoRedistributeInstallments) {
1012
+ return;
1013
+ }
1014
+
1015
+ scheduleInstallmentRedistribution(index);
1016
+ }}
1017
+ placeholder="0,00"
1018
+ />
1019
+ </FormControl>
1020
+ <FormMessage />
1021
+ </FormItem>
1022
+ )}
845
1023
  />
846
- </FormControl>
847
- <FormMessage />
848
- </FormItem>
1024
+ </div>
1025
+ ))}
1026
+ </div>
1027
+
1028
+ <p
1029
+ className={`text-xs ${
1030
+ installmentsDiffCents === 0
1031
+ ? 'text-muted-foreground'
1032
+ : 'text-destructive'
1033
+ }`}
1034
+ >
1035
+ {t('installmentsEditor.totalPrefix', {
1036
+ total: installmentsTotal.toFixed(2),
1037
+ })}
1038
+ {installmentsDiffCents > 0 &&
1039
+ ` ${t('installmentsEditor.adjustmentNeeded')}`}
1040
+ </p>
1041
+ {form.formState.errors.installments?.message && (
1042
+ <p className="text-xs text-destructive">
1043
+ {form.formState.errors.installments.message}
1044
+ </p>
849
1045
  )}
850
- />
1046
+ </div>
1047
+ </FinanceSheetSection>
1048
+
1049
+ <FinanceSheetSection
1050
+ title={t('sections.classification.title')}
1051
+ description={t('sections.classification.description')}
1052
+ >
1053
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
1054
+ <CategoryFieldWithCreate
1055
+ form={form}
1056
+ name="categoriaId"
1057
+ label={t('fields.category')}
1058
+ selectPlaceholder={t('common.select')}
1059
+ categories={categorias}
1060
+ categoryKind="receita"
1061
+ onCreated={onOptionsUpdated}
1062
+ />
851
1063
 
1064
+ <CostCenterFieldWithCreate
1065
+ form={form}
1066
+ name="centroCustoId"
1067
+ label={t('fields.costCenter')}
1068
+ selectPlaceholder={t('common.select')}
1069
+ costCenters={centrosCusto}
1070
+ onCreated={onOptionsUpdated}
1071
+ />
1072
+
1073
+ <FormField
1074
+ control={form.control}
1075
+ name="canal"
1076
+ render={({ field }) => (
1077
+ <FormItem>
1078
+ <FormLabel>{t('fields.channel')}</FormLabel>
1079
+ <Select
1080
+ value={field.value}
1081
+ onValueChange={field.onChange}
1082
+ >
1083
+ <FormControl>
1084
+ <SelectTrigger className="w-full">
1085
+ <SelectValue placeholder={t('common.select')} />
1086
+ </SelectTrigger>
1087
+ </FormControl>
1088
+ <SelectContent>
1089
+ <SelectItem value="boleto">
1090
+ {t('channels.boleto')}
1091
+ </SelectItem>
1092
+ <SelectItem value="pix">PIX</SelectItem>
1093
+ <SelectItem value="cartao">
1094
+ {t('channels.card')}
1095
+ </SelectItem>
1096
+ <SelectItem value="transferencia">
1097
+ {t('channels.transfer')}
1098
+ </SelectItem>
1099
+ </SelectContent>
1100
+ </Select>
1101
+ <FormMessage />
1102
+ </FormItem>
1103
+ )}
1104
+ />
1105
+ </div>
1106
+ </FinanceSheetSection>
1107
+
1108
+ <FinanceSheetSection
1109
+ title={t('sections.notes.title')}
1110
+ description={t('sections.notes.description')}
1111
+ >
852
1112
  <FormField
853
1113
  control={form.control}
854
- name="installmentsCount"
1114
+ name="descricao"
855
1115
  render={({ field }) => (
856
1116
  <FormItem>
857
- <FormLabel>
858
- {t('installmentsEditor.countLabel')}
859
- </FormLabel>
1117
+ <FormLabel>{t('fields.description')}</FormLabel>
860
1118
  <FormControl>
861
- <Input
862
- type="number"
863
- min={1}
864
- max={120}
865
- value={field.value}
866
- onChange={(event) => {
867
- const nextValue = Number(event.target.value || 1);
868
- field.onChange(
869
- Number.isNaN(nextValue) ? 1 : nextValue
870
- );
871
- }}
1119
+ <Textarea
1120
+ placeholder={t('newTitle.descriptionPlaceholder')}
1121
+ {...field}
1122
+ value={field.value || ''}
872
1123
  />
873
1124
  </FormControl>
874
1125
  <FormMessage />
875
1126
  </FormItem>
876
1127
  )}
877
1128
  />
878
- </div>
879
-
880
- <div className="space-y-3 rounded-md border p-3">
881
- <div className="flex items-center justify-between gap-2">
882
- <p className="text-sm font-medium">
883
- {t('installmentsEditor.title')}
884
- </p>
885
- <Button
886
- type="button"
887
- variant="outline"
888
- size="sm"
889
- onClick={() => {
890
- setIsInstallmentsEdited(false);
891
- replaceInstallments(
892
- buildEqualInstallments(
893
- form.getValues('installmentsCount'),
894
- form.getValues('valor'),
895
- form.getValues('vencimento')
896
- )
897
- );
898
- }}
899
- >
900
- {t('installmentsEditor.recalculate')}
901
- </Button>
902
- </div>
903
-
904
- <div className="flex items-center gap-2">
905
- <Checkbox
906
- id="auto-redistribute-installments-receivable"
907
- checked={autoRedistributeInstallments}
908
- onCheckedChange={(checked) =>
909
- setAutoRedistributeInstallments(checked === true)
910
- }
911
- />
912
- <Label
913
- htmlFor="auto-redistribute-installments-receivable"
914
- className="text-xs text-muted-foreground"
915
- >
916
- {t('installmentsEditor.autoRedistributeLabel')}
917
- </Label>
918
- </div>
919
- {autoRedistributeInstallments && (
920
- <p className="text-xs text-muted-foreground">
921
- A redistribuição ocorre ao parar de digitar e ao sair do
922
- campo.
923
- </p>
924
- )}
925
-
926
- <div className="space-y-2">
927
- {installmentFields.map((installment, index) => (
928
- <div
929
- key={installment.id}
930
- className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
931
- >
932
- <div className="flex items-center text-sm text-muted-foreground">
933
- #{index + 1}
934
- </div>
935
-
936
- <FormField
937
- control={form.control}
938
- name={`installments.${index}.dueDate` as const}
939
- render={({ field }) => (
940
- <FormItem>
941
- <FormLabel className="text-xs">
942
- {t('installmentsEditor.dueDateLabel')}
943
- </FormLabel>
944
- <FormControl>
945
- <Input
946
- type="date"
947
- {...field}
948
- value={field.value || ''}
949
- onChange={(event) => {
950
- setIsInstallmentsEdited(true);
951
- field.onChange(event);
952
- }}
953
- />
954
- </FormControl>
955
- <FormMessage />
956
- </FormItem>
957
- )}
958
- />
959
-
960
- <FormField
961
- control={form.control}
962
- name={`installments.${index}.amount` as const}
963
- render={({ field }) => (
964
- <FormItem>
965
- <FormLabel className="text-xs">
966
- {t('installmentsEditor.amountLabel')}
967
- </FormLabel>
968
- <FormControl>
969
- <InputMoney
970
- ref={field.ref}
971
- name={field.name}
972
- value={field.value}
973
- onBlur={() => {
974
- field.onBlur();
975
-
976
- if (!autoRedistributeInstallments) {
977
- return;
978
- }
979
-
980
- clearScheduledRedistribution(index);
981
- runInstallmentRedistribution(index);
982
- }}
983
- onValueChange={(value) => {
984
- setIsInstallmentsEdited(true);
985
- field.onChange(value ?? 0);
986
-
987
- if (!autoRedistributeInstallments) {
988
- return;
989
- }
990
-
991
- scheduleInstallmentRedistribution(index);
992
- }}
993
- placeholder="0,00"
994
- />
995
- </FormControl>
996
- <FormMessage />
997
- </FormItem>
998
- )}
999
- />
1000
- </div>
1001
- ))}
1002
- </div>
1003
-
1004
- <p
1005
- className={`text-xs ${
1006
- installmentsDiffCents === 0
1007
- ? 'text-muted-foreground'
1008
- : 'text-destructive'
1009
- }`}
1129
+ </FinanceSheetSection>
1130
+ </FinanceSheetBody>
1131
+
1132
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
1133
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
1134
+ <Button
1135
+ type="button"
1136
+ variant="outline"
1137
+ onClick={() => setOpen(false)}
1010
1138
  >
1011
- {t('installmentsEditor.totalPrefix', {
1012
- total: installmentsTotal.toFixed(2),
1013
- })}
1014
- {installmentsDiffCents > 0 &&
1015
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
1016
- </p>
1017
- {form.formState.errors.installments?.message && (
1018
- <p className="text-xs text-destructive">
1019
- {form.formState.errors.installments.message}
1020
- </p>
1021
- )}
1139
+ {t('common.cancel')}
1140
+ </Button>
1141
+ <Button
1142
+ type="submit"
1143
+ disabled={
1144
+ form.formState.isSubmitting ||
1145
+ isUploadingFile ||
1146
+ isExtractingFileData
1147
+ }
1148
+ >
1149
+ {(isUploadingFile || isExtractingFileData) && (
1150
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1151
+ )}
1152
+ {isExtractingFileData
1153
+ ? t('common.upload.fillingWithAi')
1154
+ : isUploadingFile
1155
+ ? t('common.upload.uploadingFile')
1156
+ : t('common.save')}
1157
+ </Button>
1022
1158
  </div>
1023
-
1024
- <CategoryFieldWithCreate
1025
- form={form}
1026
- name="categoriaId"
1027
- label={t('fields.category')}
1028
- selectPlaceholder={t('common.select')}
1029
- categories={categorias}
1030
- categoryKind="receita"
1031
- onCreated={onOptionsUpdated}
1032
- />
1033
-
1034
- <CostCenterFieldWithCreate
1035
- form={form}
1036
- name="centroCustoId"
1037
- label={t('fields.costCenter')}
1038
- selectPlaceholder={t('common.select')}
1039
- costCenters={centrosCusto}
1040
- onCreated={onOptionsUpdated}
1041
- />
1042
-
1043
- <FormField
1044
- control={form.control}
1045
- name="canal"
1046
- render={({ field }) => (
1047
- <FormItem>
1048
- <FormLabel>{t('fields.channel')}</FormLabel>
1049
- <Select value={field.value} onValueChange={field.onChange}>
1050
- <FormControl>
1051
- <SelectTrigger className="w-full">
1052
- <SelectValue placeholder={t('common.select')} />
1053
- </SelectTrigger>
1054
- </FormControl>
1055
- <SelectContent>
1056
- <SelectItem value="boleto">
1057
- {t('channels.boleto')}
1058
- </SelectItem>
1059
- <SelectItem value="pix">PIX</SelectItem>
1060
- <SelectItem value="cartao">
1061
- {t('channels.card')}
1062
- </SelectItem>
1063
- <SelectItem value="transferencia">
1064
- {t('channels.transfer')}
1065
- </SelectItem>
1066
- </SelectContent>
1067
- </Select>
1068
- <FormMessage />
1069
- </FormItem>
1070
- )}
1071
- />
1072
-
1073
- <FormField
1074
- control={form.control}
1075
- name="descricao"
1076
- render={({ field }) => (
1077
- <FormItem>
1078
- <FormLabel>{t('fields.description')}</FormLabel>
1079
- <FormControl>
1080
- <Textarea
1081
- placeholder={t('newTitle.descriptionPlaceholder')}
1082
- {...field}
1083
- value={field.value || ''}
1084
- />
1085
- </FormControl>
1086
- <FormMessage />
1087
- </FormItem>
1088
- )}
1089
- />
1090
- </div>
1091
-
1092
- <div className="flex flex-col gap-2 py-4">
1093
- <Button
1094
- type="submit"
1095
- disabled={
1096
- form.formState.isSubmitting ||
1097
- isUploadingFile ||
1098
- isExtractingFileData
1099
- }
1100
- >
1101
- {(isUploadingFile || isExtractingFileData) && (
1102
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1103
- )}
1104
- {isExtractingFileData
1105
- ? t('common.upload.fillingWithAi')
1106
- : isUploadingFile
1107
- ? t('common.upload.uploadingFile')
1108
- : t('common.save')}
1109
- </Button>
1110
1159
  </div>
1111
1160
  </form>
1112
1161
  </Form>
@@ -1579,482 +1628,527 @@ function EditarTituloSheet({
1579
1628
  }
1580
1629
  };
1581
1630
 
1631
+ const handleCancel = () => {
1632
+ onOpenChange(false);
1633
+ };
1634
+
1582
1635
  return (
1583
1636
  <Sheet open={open} onOpenChange={onOpenChange}>
1584
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
1585
- <SheetHeader>
1637
+ <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
1638
+ <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
1586
1639
  <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1587
1640
  <SheetDescription>{t('editTitle.description')}</SheetDescription>
1588
1641
  </SheetHeader>
1589
1642
  <Form {...form}>
1590
- <form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
1591
- <div className="grid gap-3">
1592
- <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
1593
- <div className="grid gap-2">
1594
- <FormLabel>{t('common.upload.label')}</FormLabel>
1595
- <Input
1596
- ref={fileInputRef}
1597
- className="hidden"
1598
- type="file"
1599
- accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
1600
- onChange={(event) => {
1601
- const file = event.target.files?.[0];
1602
- if (!file) {
1603
- return;
1643
+ <form
1644
+ className="flex h-full flex-col overflow-hidden"
1645
+ onSubmit={form.handleSubmit(handleSubmit)}
1646
+ >
1647
+ <FinanceSheetBody>
1648
+ <FinanceSheetSection
1649
+ title={t('sections.main.title')}
1650
+ description={t('sections.main.description')}
1651
+ >
1652
+ <div className="grid grid-cols-1 items-start gap-4 lg:grid-cols-[minmax(0,1.15fr)_minmax(0,0.85fr)]">
1653
+ <div className="grid gap-2">
1654
+ <FormLabel>{t('common.upload.label')}</FormLabel>
1655
+ <Input
1656
+ ref={fileInputRef}
1657
+ className="hidden"
1658
+ type="file"
1659
+ accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
1660
+ onChange={(event) => {
1661
+ const file = event.target.files?.[0];
1662
+ if (!file) {
1663
+ return;
1664
+ }
1665
+
1666
+ clearUploadedFile();
1667
+ void uploadRelatedFile(file);
1668
+ }}
1669
+ disabled={
1670
+ isUploadingFile ||
1671
+ isExtractingFileData ||
1672
+ form.formState.isSubmitting
1604
1673
  }
1674
+ />
1605
1675
 
1606
- clearUploadedFile();
1607
- void uploadRelatedFile(file);
1608
- }}
1609
- disabled={
1610
- isUploadingFile ||
1611
- isExtractingFileData ||
1612
- form.formState.isSubmitting
1613
- }
1614
- />
1615
-
1616
- <div className="grid w-full grid-cols-2 gap-2">
1617
- <Tooltip>
1618
- <TooltipTrigger asChild>
1619
- <Button
1620
- type="button"
1621
- variant="outline"
1622
- className={
1623
- uploadedFileId ? 'w-full' : 'col-span-2 w-full'
1624
- }
1625
- onClick={handleSelectFile}
1626
- aria-label={
1627
- uploadedFileId
1628
- ? t('common.upload.change')
1629
- : t('common.upload.upload')
1630
- }
1631
- disabled={
1632
- isUploadingFile ||
1633
- isExtractingFileData ||
1634
- form.formState.isSubmitting
1635
- }
1636
- >
1637
- {uploadedFileId ? (
1638
- <Upload className="h-4 w-4" />
1639
- ) : (
1640
- <>
1641
- <Upload className="mr-2 h-4 w-4" />
1642
- {t('common.upload.upload')}
1643
- </>
1644
- )}
1645
- </Button>
1646
- </TooltipTrigger>
1647
- <TooltipContent>
1648
- {uploadedFileId
1649
- ? t('common.upload.change')
1650
- : t('common.upload.upload')}
1651
- </TooltipContent>
1652
- </Tooltip>
1653
-
1654
- {uploadedFileId && (
1676
+ <div className="grid w-full grid-cols-2 gap-2">
1655
1677
  <Tooltip>
1656
1678
  <TooltipTrigger asChild>
1657
1679
  <Button
1658
1680
  type="button"
1659
1681
  variant="outline"
1660
- className="w-full"
1661
- onClick={clearUploadedFile}
1662
- aria-label={t('common.upload.remove')}
1682
+ className={
1683
+ uploadedFileId ? 'w-full' : 'col-span-2 w-full'
1684
+ }
1685
+ onClick={handleSelectFile}
1686
+ aria-label={
1687
+ uploadedFileId
1688
+ ? t('common.upload.change')
1689
+ : t('common.upload.upload')
1690
+ }
1663
1691
  disabled={
1664
1692
  isUploadingFile ||
1665
1693
  isExtractingFileData ||
1666
1694
  form.formState.isSubmitting
1667
1695
  }
1668
1696
  >
1669
- <Trash2 className="h-4 w-4" />
1697
+ {uploadedFileId ? (
1698
+ <Upload className="h-4 w-4" />
1699
+ ) : (
1700
+ <>
1701
+ <Upload className="mr-2 h-4 w-4" />
1702
+ {t('common.upload.upload')}
1703
+ </>
1704
+ )}
1670
1705
  </Button>
1671
1706
  </TooltipTrigger>
1672
1707
  <TooltipContent>
1673
- {t('common.upload.remove')}
1708
+ {uploadedFileId
1709
+ ? t('common.upload.change')
1710
+ : t('common.upload.upload')}
1674
1711
  </TooltipContent>
1675
1712
  </Tooltip>
1676
- )}
1677
- </div>
1678
1713
 
1679
- <div className="space-y-1">
1680
- {(uploadedFileId || uploadedFileName) && (
1681
- <p className="truncate text-xs text-muted-foreground">
1682
- {t('common.upload.selectedPrefix')} {uploadedFileName}
1683
- </p>
1684
- )}
1714
+ {uploadedFileId && (
1715
+ <Tooltip>
1716
+ <TooltipTrigger asChild>
1717
+ <Button
1718
+ type="button"
1719
+ variant="outline"
1720
+ className="w-full"
1721
+ onClick={clearUploadedFile}
1722
+ aria-label={t('common.upload.remove')}
1723
+ disabled={
1724
+ isUploadingFile ||
1725
+ isExtractingFileData ||
1726
+ form.formState.isSubmitting
1727
+ }
1728
+ >
1729
+ <Trash2 className="h-4 w-4" />
1730
+ </Button>
1731
+ </TooltipTrigger>
1732
+ <TooltipContent>
1733
+ {t('common.upload.remove')}
1734
+ </TooltipContent>
1735
+ </Tooltip>
1736
+ )}
1737
+ </div>
1685
1738
 
1686
- {isUploadingFile && !isExtractingFileData && (
1687
- <div className="space-y-1">
1688
- <Progress value={uploadProgress} className="h-2" />
1689
- <p className="text-xs text-muted-foreground">
1690
- {t('common.upload.uploadingProgress', {
1691
- progress: uploadProgress,
1692
- })}
1739
+ <div className="space-y-1 rounded-lg border border-dashed border-border/70 bg-muted/20 p-3">
1740
+ {(uploadedFileId || uploadedFileName) && (
1741
+ <p className="truncate text-xs text-muted-foreground">
1742
+ {t('common.upload.selectedPrefix')} {uploadedFileName}
1693
1743
  </p>
1694
- </div>
1695
- )}
1744
+ )}
1696
1745
 
1697
- {isExtractingFileData && (
1698
- <p className="flex items-center gap-2 text-xs text-primary">
1699
- <Loader2 className="h-4 w-4 animate-spin" />
1700
- {t('common.upload.processingAi')}
1701
- </p>
1702
- )}
1746
+ {isUploadingFile && !isExtractingFileData && (
1747
+ <div className="space-y-1">
1748
+ <Progress value={uploadProgress} className="h-2" />
1749
+ <p className="text-xs text-muted-foreground">
1750
+ {t('common.upload.uploadingProgress', {
1751
+ progress: uploadProgress,
1752
+ })}
1753
+ </p>
1754
+ </div>
1755
+ )}
1703
1756
 
1704
- {!isExtractingFileData &&
1705
- extractionConfidence !== null &&
1706
- extractionConfidence < 70 && (
1707
- <p className="text-xs text-destructive">
1708
- {t('common.upload.lowConfidence', {
1709
- confidence: Math.round(extractionConfidence),
1710
- })}
1757
+ {isExtractingFileData && (
1758
+ <p className="flex items-center gap-2 text-xs text-primary">
1759
+ <Loader2 className="h-4 w-4 animate-spin" />
1760
+ {t('common.upload.processingAi')}
1711
1761
  </p>
1712
1762
  )}
1713
1763
 
1714
- {!isExtractingFileData && extractionWarnings.length > 0 && (
1715
- <p className="truncate text-xs text-muted-foreground">
1716
- {extractionWarnings[0]}
1717
- </p>
1718
- )}
1764
+ {!isExtractingFileData &&
1765
+ extractionConfidence !== null &&
1766
+ extractionConfidence < 70 && (
1767
+ <p className="text-xs text-destructive">
1768
+ {t('common.upload.lowConfidence', {
1769
+ confidence: Math.round(extractionConfidence),
1770
+ })}
1771
+ </p>
1772
+ )}
1773
+
1774
+ {!isExtractingFileData &&
1775
+ extractionWarnings.length > 0 && (
1776
+ <p className="truncate text-xs text-muted-foreground">
1777
+ {extractionWarnings[0]}
1778
+ </p>
1779
+ )}
1780
+ </div>
1719
1781
  </div>
1782
+
1783
+ <FormField
1784
+ control={form.control}
1785
+ name="documento"
1786
+ render={({ field }) => (
1787
+ <FormItem>
1788
+ <FormLabel>{t('fields.document')}</FormLabel>
1789
+ <FormControl>
1790
+ <Input placeholder="FAT-00000" {...field} />
1791
+ </FormControl>
1792
+ <FormMessage />
1793
+ </FormItem>
1794
+ )}
1795
+ />
1720
1796
  </div>
1721
1797
 
1722
- <FormField
1723
- control={form.control}
1724
- name="documento"
1725
- render={({ field }) => (
1726
- <FormItem>
1727
- <FormLabel>{t('fields.document')}</FormLabel>
1728
- <FormControl>
1729
- <Input placeholder="FAT-00000" {...field} />
1730
- </FormControl>
1731
- <FormMessage />
1732
- </FormItem>
1733
- )}
1798
+ <PersonFieldWithCreate
1799
+ form={form}
1800
+ name="clienteId"
1801
+ label={t('fields.client')}
1802
+ entityLabel="cliente"
1803
+ selectPlaceholder={t('common.select')}
1734
1804
  />
1735
- </div>
1736
1805
 
1737
- <PersonFieldWithCreate
1738
- form={form}
1739
- name="clienteId"
1740
- label={t('fields.client')}
1741
- entityLabel="cliente"
1742
- selectPlaceholder={t('common.select')}
1743
- />
1806
+ <div className="grid gap-4 xl:grid-cols-2">
1807
+ <div className="grid gap-4 md:grid-cols-2">
1808
+ <FormField
1809
+ control={form.control}
1810
+ name="competencia"
1811
+ render={({ field }) => (
1812
+ <FormItem>
1813
+ <FormLabel>{t('fields.competency')}</FormLabel>
1814
+ <FormControl>
1815
+ <Input
1816
+ type="month"
1817
+ {...field}
1818
+ value={field.value || ''}
1819
+ />
1820
+ </FormControl>
1821
+ <FormMessage />
1822
+ </FormItem>
1823
+ )}
1824
+ />
1825
+
1826
+ <FormField
1827
+ control={form.control}
1828
+ name="vencimento"
1829
+ render={({ field }) => (
1830
+ <FormItem>
1831
+ <FormLabel>{t('fields.dueDate')}</FormLabel>
1832
+ <FormControl>
1833
+ <Input
1834
+ type="date"
1835
+ {...field}
1836
+ value={field.value || ''}
1837
+ />
1838
+ </FormControl>
1839
+ <FormMessage />
1840
+ </FormItem>
1841
+ )}
1842
+ />
1843
+ </div>
1744
1844
 
1745
- <div className="grid grid-cols-2 items-start gap-3">
1746
- <FormField
1747
- control={form.control}
1748
- name="competencia"
1749
- render={({ field }) => (
1750
- <FormItem>
1751
- <FormLabel>{t('fields.competency')}</FormLabel>
1752
- <FormControl>
1753
- <Input
1754
- type="month"
1755
- {...field}
1756
- value={field.value || ''}
1757
- />
1758
- </FormControl>
1759
- <FormMessage />
1760
- </FormItem>
1845
+ <div className="grid gap-4 md:grid-cols-2">
1846
+ <FormField
1847
+ control={form.control}
1848
+ name="valor"
1849
+ render={({ field }) => (
1850
+ <FormItem>
1851
+ <FormLabel>{t('fields.totalValue')}</FormLabel>
1852
+ <FormControl>
1853
+ <InputMoney
1854
+ ref={field.ref}
1855
+ name={field.name}
1856
+ value={field.value}
1857
+ onBlur={field.onBlur}
1858
+ onValueChange={(value) =>
1859
+ field.onChange(value ?? 0)
1860
+ }
1861
+ placeholder="0,00"
1862
+ />
1863
+ </FormControl>
1864
+ <FormMessage />
1865
+ </FormItem>
1866
+ )}
1867
+ />
1868
+
1869
+ <FormField
1870
+ control={form.control}
1871
+ name="installmentsCount"
1872
+ render={({ field }) => (
1873
+ <FormItem>
1874
+ <FormLabel>
1875
+ {t('installmentsEditor.countLabel')}
1876
+ </FormLabel>
1877
+ <FormControl>
1878
+ <Input
1879
+ type="number"
1880
+ min={1}
1881
+ max={120}
1882
+ value={field.value}
1883
+ onChange={(event) => {
1884
+ const nextValue = Number(
1885
+ event.target.value || 1
1886
+ );
1887
+ field.onChange(
1888
+ Number.isNaN(nextValue) ? 1 : nextValue
1889
+ );
1890
+ setIsInstallmentsEdited(false);
1891
+ }}
1892
+ />
1893
+ </FormControl>
1894
+ <FormMessage />
1895
+ </FormItem>
1896
+ )}
1897
+ />
1898
+ </div>
1899
+ </div>
1900
+
1901
+ <div className="space-y-3 rounded-md border p-3">
1902
+ <div className="flex items-center justify-between gap-2">
1903
+ <p className="text-sm font-medium">
1904
+ {t('installmentsEditor.title')}
1905
+ </p>
1906
+ <Button
1907
+ type="button"
1908
+ variant="outline"
1909
+ size="sm"
1910
+ onClick={() => {
1911
+ setIsInstallmentsEdited(false);
1912
+ replaceInstallments(
1913
+ buildEqualInstallments(
1914
+ form.getValues('installmentsCount'),
1915
+ form.getValues('valor'),
1916
+ form.getValues('vencimento')
1917
+ )
1918
+ );
1919
+ }}
1920
+ >
1921
+ {t('installmentsEditor.recalculate')}
1922
+ </Button>
1923
+ </div>
1924
+
1925
+ <div className="flex items-center gap-2">
1926
+ <Checkbox
1927
+ id="auto-redistribute-installments-edit-receivable"
1928
+ checked={autoRedistributeInstallments}
1929
+ onCheckedChange={(checked) =>
1930
+ setAutoRedistributeInstallments(checked === true)
1931
+ }
1932
+ />
1933
+ <Label
1934
+ htmlFor="auto-redistribute-installments-edit-receivable"
1935
+ className="text-xs text-muted-foreground"
1936
+ >
1937
+ {t('installmentsEditor.autoRedistributeLabel')}
1938
+ </Label>
1939
+ </div>
1940
+
1941
+ {autoRedistributeInstallments && (
1942
+ <p className="text-xs text-muted-foreground">
1943
+ {t('installmentsEditor.autoRedistributeHint')}
1944
+ </p>
1761
1945
  )}
1762
- />
1763
1946
 
1764
- <FormField
1765
- control={form.control}
1766
- name="vencimento"
1767
- render={({ field }) => (
1768
- <FormItem>
1769
- <FormLabel>{t('fields.dueDate')}</FormLabel>
1770
- <FormControl>
1771
- <Input
1772
- type="date"
1773
- {...field}
1774
- value={field.value || ''}
1947
+ <div className="space-y-2">
1948
+ {installmentFields.map((installment, index) => (
1949
+ <div
1950
+ key={installment.id}
1951
+ className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
1952
+ >
1953
+ <div className="flex items-center text-sm text-muted-foreground">
1954
+ #{index + 1}
1955
+ </div>
1956
+
1957
+ <FormField
1958
+ control={form.control}
1959
+ name={`installments.${index}.dueDate` as const}
1960
+ render={({ field }) => (
1961
+ <FormItem>
1962
+ <FormLabel className="text-xs">
1963
+ {t('installmentsEditor.dueDateLabel')}
1964
+ </FormLabel>
1965
+ <FormControl>
1966
+ <Input
1967
+ type="date"
1968
+ {...field}
1969
+ value={field.value || ''}
1970
+ onChange={(event) => {
1971
+ setIsInstallmentsEdited(true);
1972
+ field.onChange(event);
1973
+ }}
1974
+ />
1975
+ </FormControl>
1976
+ <FormMessage />
1977
+ </FormItem>
1978
+ )}
1775
1979
  />
1776
- </FormControl>
1777
- <FormMessage />
1778
- </FormItem>
1779
- )}
1780
- />
1781
- </div>
1782
1980
 
1783
- <div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
1784
- <FormField
1785
- control={form.control}
1786
- name="valor"
1787
- render={({ field }) => (
1788
- <FormItem>
1789
- <FormLabel>{t('fields.totalValue')}</FormLabel>
1790
- <FormControl>
1791
- <InputMoney
1792
- ref={field.ref}
1793
- name={field.name}
1794
- value={field.value}
1795
- onBlur={field.onBlur}
1796
- onValueChange={(value) => field.onChange(value ?? 0)}
1797
- placeholder="0,00"
1981
+ <FormField
1982
+ control={form.control}
1983
+ name={`installments.${index}.amount` as const}
1984
+ render={({ field }) => (
1985
+ <FormItem>
1986
+ <FormLabel className="text-xs">
1987
+ {t('installmentsEditor.amountLabel')}
1988
+ </FormLabel>
1989
+ <FormControl>
1990
+ <InputMoney
1991
+ ref={field.ref}
1992
+ name={field.name}
1993
+ value={field.value}
1994
+ onBlur={() => {
1995
+ field.onBlur();
1996
+
1997
+ if (!autoRedistributeInstallments) {
1998
+ return;
1999
+ }
2000
+
2001
+ clearScheduledRedistribution(index);
2002
+ runInstallmentRedistribution(index);
2003
+ }}
2004
+ onValueChange={(value) => {
2005
+ setIsInstallmentsEdited(true);
2006
+ field.onChange(value ?? 0);
2007
+
2008
+ if (!autoRedistributeInstallments) {
2009
+ return;
2010
+ }
2011
+
2012
+ scheduleInstallmentRedistribution(index);
2013
+ }}
2014
+ placeholder="0,00"
2015
+ />
2016
+ </FormControl>
2017
+ <FormMessage />
2018
+ </FormItem>
2019
+ )}
1798
2020
  />
1799
- </FormControl>
1800
- <FormMessage />
1801
- </FormItem>
2021
+ </div>
2022
+ ))}
2023
+ </div>
2024
+
2025
+ <p
2026
+ className={`text-xs ${
2027
+ installmentsDiffCents === 0
2028
+ ? 'text-muted-foreground'
2029
+ : 'text-destructive'
2030
+ }`}
2031
+ >
2032
+ {t('installmentsEditor.totalPrefix', {
2033
+ total: installmentsTotal.toFixed(2),
2034
+ })}
2035
+ {installmentsDiffCents > 0 &&
2036
+ ` ${t('installmentsEditor.adjustmentNeeded')}`}
2037
+ </p>
2038
+ {form.formState.errors.installments?.message && (
2039
+ <p className="text-xs text-destructive">
2040
+ {form.formState.errors.installments.message}
2041
+ </p>
1802
2042
  )}
1803
- />
2043
+ </div>
2044
+ </FinanceSheetSection>
2045
+
2046
+ <FinanceSheetSection
2047
+ title={t('sections.classification.title')}
2048
+ description={t('sections.classification.description')}
2049
+ >
2050
+ <div className="grid gap-4 md:grid-cols-2 xl:grid-cols-3">
2051
+ <CategoryFieldWithCreate
2052
+ form={form}
2053
+ name="categoriaId"
2054
+ label={t('fields.category')}
2055
+ selectPlaceholder={t('common.select')}
2056
+ categories={categorias}
2057
+ categoryKind="receita"
2058
+ onCreated={onOptionsUpdated}
2059
+ />
2060
+
2061
+ <CostCenterFieldWithCreate
2062
+ form={form}
2063
+ name="centroCustoId"
2064
+ label={t('fields.costCenter')}
2065
+ selectPlaceholder={t('common.select')}
2066
+ costCenters={centrosCusto}
2067
+ onCreated={onOptionsUpdated}
2068
+ />
2069
+
2070
+ <FormField
2071
+ control={form.control}
2072
+ name="canal"
2073
+ render={({ field }) => (
2074
+ <FormItem>
2075
+ <FormLabel>{t('fields.channel')}</FormLabel>
2076
+ <Select
2077
+ value={field.value}
2078
+ onValueChange={field.onChange}
2079
+ >
2080
+ <FormControl>
2081
+ <SelectTrigger className="w-full">
2082
+ <SelectValue placeholder={t('common.select')} />
2083
+ </SelectTrigger>
2084
+ </FormControl>
2085
+ <SelectContent>
2086
+ <SelectItem value="boleto">
2087
+ {t('channels.boleto')}
2088
+ </SelectItem>
2089
+ <SelectItem value="pix">PIX</SelectItem>
2090
+ <SelectItem value="cartao">
2091
+ {t('channels.card')}
2092
+ </SelectItem>
2093
+ <SelectItem value="transferencia">
2094
+ {t('channels.transfer')}
2095
+ </SelectItem>
2096
+ </SelectContent>
2097
+ </Select>
2098
+ <FormMessage />
2099
+ </FormItem>
2100
+ )}
2101
+ />
2102
+ </div>
2103
+ </FinanceSheetSection>
1804
2104
 
2105
+ <FinanceSheetSection
2106
+ title={t('sections.notes.title')}
2107
+ description={t('sections.notes.description')}
2108
+ >
1805
2109
  <FormField
1806
2110
  control={form.control}
1807
- name="installmentsCount"
2111
+ name="descricao"
1808
2112
  render={({ field }) => (
1809
2113
  <FormItem>
1810
- <FormLabel>
1811
- {t('installmentsEditor.countLabel')}
1812
- </FormLabel>
2114
+ <FormLabel>{t('fields.description')}</FormLabel>
1813
2115
  <FormControl>
1814
- <Input
1815
- type="number"
1816
- min={1}
1817
- max={120}
1818
- value={field.value}
1819
- onChange={(event) => {
1820
- const nextValue = Number(event.target.value || 1);
1821
- field.onChange(
1822
- Number.isNaN(nextValue) ? 1 : nextValue
1823
- );
1824
- setIsInstallmentsEdited(false);
1825
- }}
2116
+ <Textarea
2117
+ placeholder={t('newTitle.descriptionPlaceholder')}
2118
+ {...field}
2119
+ value={field.value || ''}
1826
2120
  />
1827
2121
  </FormControl>
1828
2122
  <FormMessage />
1829
2123
  </FormItem>
1830
2124
  )}
1831
2125
  />
1832
- </div>
1833
-
1834
- <div className="space-y-3 rounded-md border p-3">
1835
- <div className="flex items-center justify-between gap-2">
1836
- <p className="text-sm font-medium">
1837
- {t('installmentsEditor.title')}
1838
- </p>
1839
- <Button
1840
- type="button"
1841
- variant="outline"
1842
- size="sm"
1843
- onClick={() => {
1844
- setIsInstallmentsEdited(false);
1845
- replaceInstallments(
1846
- buildEqualInstallments(
1847
- form.getValues('installmentsCount'),
1848
- form.getValues('valor'),
1849
- form.getValues('vencimento')
1850
- )
1851
- );
1852
- }}
1853
- >
1854
- {t('installmentsEditor.recalculate')}
1855
- </Button>
1856
- </div>
1857
-
1858
- <div className="flex items-center gap-2">
1859
- <Checkbox
1860
- id="auto-redistribute-installments-edit-receivable"
1861
- checked={autoRedistributeInstallments}
1862
- onCheckedChange={(checked) =>
1863
- setAutoRedistributeInstallments(checked === true)
1864
- }
1865
- />
1866
- <Label
1867
- htmlFor="auto-redistribute-installments-edit-receivable"
1868
- className="text-xs text-muted-foreground"
1869
- >
1870
- {t('installmentsEditor.autoRedistributeLabel')}
1871
- </Label>
1872
- </div>
1873
-
1874
- <div className="space-y-2">
1875
- {installmentFields.map((installment, index) => (
1876
- <div
1877
- key={installment.id}
1878
- className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
1879
- >
1880
- <div className="flex items-center text-sm text-muted-foreground">
1881
- #{index + 1}
1882
- </div>
1883
-
1884
- <FormField
1885
- control={form.control}
1886
- name={`installments.${index}.dueDate` as const}
1887
- render={({ field }) => (
1888
- <FormItem>
1889
- <FormLabel className="text-xs">
1890
- {t('installmentsEditor.dueDateLabel')}
1891
- </FormLabel>
1892
- <FormControl>
1893
- <Input
1894
- type="date"
1895
- {...field}
1896
- value={field.value || ''}
1897
- onChange={(event) => {
1898
- setIsInstallmentsEdited(true);
1899
- field.onChange(event);
1900
- }}
1901
- />
1902
- </FormControl>
1903
- <FormMessage />
1904
- </FormItem>
1905
- )}
1906
- />
1907
-
1908
- <FormField
1909
- control={form.control}
1910
- name={`installments.${index}.amount` as const}
1911
- render={({ field }) => (
1912
- <FormItem>
1913
- <FormLabel className="text-xs">
1914
- {t('installmentsEditor.amountLabel')}
1915
- </FormLabel>
1916
- <FormControl>
1917
- <InputMoney
1918
- ref={field.ref}
1919
- name={field.name}
1920
- value={field.value}
1921
- onBlur={() => {
1922
- field.onBlur();
1923
-
1924
- if (!autoRedistributeInstallments) {
1925
- return;
1926
- }
1927
-
1928
- clearScheduledRedistribution(index);
1929
- runInstallmentRedistribution(index);
1930
- }}
1931
- onValueChange={(value) => {
1932
- setIsInstallmentsEdited(true);
1933
- field.onChange(value ?? 0);
1934
-
1935
- if (!autoRedistributeInstallments) {
1936
- return;
1937
- }
1938
-
1939
- scheduleInstallmentRedistribution(index);
1940
- }}
1941
- placeholder="0,00"
1942
- />
1943
- </FormControl>
1944
- <FormMessage />
1945
- </FormItem>
1946
- )}
1947
- />
1948
- </div>
1949
- ))}
1950
- </div>
1951
-
1952
- <p
1953
- className={`text-xs ${
1954
- installmentsDiffCents === 0
1955
- ? 'text-muted-foreground'
1956
- : 'text-destructive'
1957
- }`}
2126
+ </FinanceSheetSection>
2127
+ </FinanceSheetBody>
2128
+
2129
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2130
+ <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2131
+ <Button type="button" variant="outline" onClick={handleCancel}>
2132
+ {t('common.cancel')}
2133
+ </Button>
2134
+ <Button
2135
+ type="submit"
2136
+ disabled={
2137
+ form.formState.isSubmitting ||
2138
+ isUploadingFile ||
2139
+ isExtractingFileData
2140
+ }
1958
2141
  >
1959
- {t('installmentsEditor.totalPrefix', {
1960
- total: installmentsTotal.toFixed(2),
1961
- })}
1962
- {installmentsDiffCents > 0 &&
1963
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
1964
- </p>
1965
- {form.formState.errors.installments?.message && (
1966
- <p className="text-xs text-destructive">
1967
- {form.formState.errors.installments.message}
1968
- </p>
1969
- )}
2142
+ {(isUploadingFile || isExtractingFileData) && (
2143
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2144
+ )}
2145
+ {isExtractingFileData
2146
+ ? t('common.upload.fillingWithAi')
2147
+ : isUploadingFile
2148
+ ? t('common.upload.uploadingFile')
2149
+ : t('common.save')}
2150
+ </Button>
1970
2151
  </div>
1971
-
1972
- <CategoryFieldWithCreate
1973
- form={form}
1974
- name="categoriaId"
1975
- label={t('fields.category')}
1976
- selectPlaceholder={t('common.select')}
1977
- categories={categorias}
1978
- categoryKind="receita"
1979
- onCreated={onOptionsUpdated}
1980
- />
1981
-
1982
- <CostCenterFieldWithCreate
1983
- form={form}
1984
- name="centroCustoId"
1985
- label={t('fields.costCenter')}
1986
- selectPlaceholder={t('common.select')}
1987
- costCenters={centrosCusto}
1988
- onCreated={onOptionsUpdated}
1989
- />
1990
-
1991
- <FormField
1992
- control={form.control}
1993
- name="canal"
1994
- render={({ field }) => (
1995
- <FormItem>
1996
- <FormLabel>{t('fields.channel')}</FormLabel>
1997
- <Select value={field.value} onValueChange={field.onChange}>
1998
- <FormControl>
1999
- <SelectTrigger className="w-full">
2000
- <SelectValue placeholder={t('common.select')} />
2001
- </SelectTrigger>
2002
- </FormControl>
2003
- <SelectContent>
2004
- <SelectItem value="boleto">
2005
- {t('channels.boleto')}
2006
- </SelectItem>
2007
- <SelectItem value="pix">PIX</SelectItem>
2008
- <SelectItem value="cartao">
2009
- {t('channels.card')}
2010
- </SelectItem>
2011
- <SelectItem value="transferencia">
2012
- {t('channels.transfer')}
2013
- </SelectItem>
2014
- </SelectContent>
2015
- </Select>
2016
- <FormMessage />
2017
- </FormItem>
2018
- )}
2019
- />
2020
-
2021
- <FormField
2022
- control={form.control}
2023
- name="descricao"
2024
- render={({ field }) => (
2025
- <FormItem>
2026
- <FormLabel>{t('fields.description')}</FormLabel>
2027
- <FormControl>
2028
- <Textarea
2029
- placeholder={t('newTitle.descriptionPlaceholder')}
2030
- {...field}
2031
- value={field.value || ''}
2032
- />
2033
- </FormControl>
2034
- <FormMessage />
2035
- </FormItem>
2036
- )}
2037
- />
2038
- </div>
2039
-
2040
- <div className="flex flex-col gap-2 py-4">
2041
- <Button
2042
- type="submit"
2043
- disabled={
2044
- form.formState.isSubmitting ||
2045
- isUploadingFile ||
2046
- isExtractingFileData
2047
- }
2048
- >
2049
- {(isUploadingFile || isExtractingFileData) && (
2050
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2051
- )}
2052
- {isExtractingFileData
2053
- ? t('common.upload.fillingWithAi')
2054
- : isUploadingFile
2055
- ? t('common.upload.uploadingFile')
2056
- : t('common.save')}
2057
- </Button>
2058
2152
  </div>
2059
2153
  </form>
2060
2154
  </Form>
@@ -2080,18 +2174,14 @@ export default function TitulosReceberPage() {
2080
2174
  const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
2081
2175
 
2082
2176
  const [search, setSearch] = useState('');
2083
- const [statusFilter, setStatusFilter] = useState<string>('');
2177
+ const [statusFilter, setStatusFilter] = useState<string>('all');
2084
2178
  const [page, setPage] = useState(1);
2085
2179
  const pageSize = 10;
2086
2180
 
2087
2181
  const normalizedStatusFilter =
2088
2182
  statusFilter && statusFilter !== 'all' ? statusFilter : undefined;
2089
2183
 
2090
- const {
2091
- data: paginatedTitlesResponse,
2092
- refetch: refetchTitles,
2093
- isFetching: isFetchingTitles,
2094
- } = useQuery<{
2184
+ const { data: paginatedTitlesResponse, refetch: refetchTitles } = useQuery<{
2095
2185
  data: any[];
2096
2186
  total: number;
2097
2187
  page: number;
@@ -2133,6 +2223,73 @@ export default function TitulosReceberPage() {
2133
2223
  });
2134
2224
 
2135
2225
  const titulosReceber = paginatedTitlesResponse?.data || [];
2226
+ const visibleTitlesTotal = useMemo(
2227
+ () =>
2228
+ titulosReceber.reduce(
2229
+ (acc, title) => acc + Number(title?.valorTotal || 0),
2230
+ 0
2231
+ ),
2232
+ [titulosReceber]
2233
+ );
2234
+ const visiblePendingTitles = useMemo(
2235
+ () =>
2236
+ titulosReceber.filter((title) =>
2237
+ ['aberto', 'parcial', 'vencido'].includes(String(title?.status || ''))
2238
+ ).length,
2239
+ [titulosReceber]
2240
+ );
2241
+ const visibleOverdueTitles = useMemo(
2242
+ () =>
2243
+ titulosReceber.filter(
2244
+ (title) =>
2245
+ title?.status === 'vencido' ||
2246
+ (Array.isArray(title?.parcelas) &&
2247
+ title.parcelas.some(
2248
+ (installment: any) => installment.status === 'vencido'
2249
+ ))
2250
+ ).length,
2251
+ [titulosReceber]
2252
+ );
2253
+ const summaryCards = useMemo(
2254
+ () => [
2255
+ {
2256
+ key: 'visible',
2257
+ title: t('summary.cards.visible.title'),
2258
+ value: titulosReceber.length,
2259
+ description: t('summary.cards.visible.description', {
2260
+ total: paginatedTitlesResponse?.total || 0,
2261
+ }),
2262
+ icon: FileText,
2263
+ layout: 'compact' as const,
2264
+ },
2265
+ {
2266
+ key: 'value',
2267
+ title: t('summary.cards.value.title'),
2268
+ value: <Money value={visibleTitlesTotal} />,
2269
+ description: t('summary.cards.value.description'),
2270
+ icon: Wallet,
2271
+ layout: 'compact' as const,
2272
+ },
2273
+ {
2274
+ key: 'attention',
2275
+ title: t('summary.cards.attention.title'),
2276
+ value: visiblePendingTitles,
2277
+ description: t('summary.cards.attention.description', {
2278
+ overdue: visibleOverdueTitles,
2279
+ }),
2280
+ icon: AlertTriangle,
2281
+ layout: 'compact' as const,
2282
+ },
2283
+ ],
2284
+ [
2285
+ paginatedTitlesResponse?.total,
2286
+ t,
2287
+ titulosReceber.length,
2288
+ visibleOverdueTitles,
2289
+ visiblePendingTitles,
2290
+ visibleTitlesTotal,
2291
+ ]
2292
+ );
2136
2293
  const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
2137
2294
 
2138
2295
  const editingTitle = useMemo(
@@ -2264,177 +2421,179 @@ export default function TitulosReceberPage() {
2264
2421
  }
2265
2422
  />
2266
2423
 
2267
- <FilterBar
2268
- searchPlaceholder={t('filters.searchPlaceholder')}
2269
- searchValue={search}
2270
- onSearchChange={setSearch}
2271
- filters={[
2272
- {
2273
- id: 'status',
2274
- label: t('filters.status'),
2275
- value: statusFilter,
2276
- onChange: setStatusFilter,
2277
- options: [
2278
- { value: 'all', label: t('statuses.all') },
2279
- { value: 'aberto', label: t('statuses.aberto') },
2280
- { value: 'parcial', label: t('statuses.parcial') },
2281
- { value: 'liquidado', label: t('statuses.liquidado') },
2282
- { value: 'vencido', label: t('statuses.vencido') },
2283
- { value: 'cancelado', label: t('statuses.cancelado') },
2284
- ],
2285
- },
2286
- ]}
2287
- activeFilters={normalizedStatusFilter ? 1 : 0}
2288
- onClearFilters={() => setStatusFilter('all')}
2289
- />
2424
+ <KpiCardsGrid items={summaryCards} columns={3} />
2290
2425
 
2291
- <div className="rounded-md border">
2292
- <Table>
2293
- <TableHeader>
2294
- <TableRow>
2295
- <TableHead>{t('table.headers.document')}</TableHead>
2296
- <TableHead>{t('table.headers.client')}</TableHead>
2297
- <TableHead>{t('table.headers.competency')}</TableHead>
2298
- <TableHead>{t('table.headers.dueDate')}</TableHead>
2299
- <TableHead className="text-right">
2300
- {t('table.headers.value')}
2301
- </TableHead>
2302
- <TableHead>{t('table.headers.channel')}</TableHead>
2303
- <TableHead>{t('table.headers.status')}</TableHead>
2304
- <TableHead className="w-[50px]" />
2305
- </TableRow>
2306
- </TableHeader>
2307
- <TableBody>
2308
- {titulosReceber.map((titulo) => {
2309
- const cliente = getPessoaById(titulo.clienteId);
2310
- const canal =
2311
- canalBadge[titulo.canal as keyof typeof canalBadge] ||
2312
- canalBadge.transferencia;
2313
- const proximaParcela = titulo.parcelas.find(
2314
- (p: any) => p.status === 'aberto' || p.status === 'vencido'
2315
- );
2316
-
2317
- return (
2318
- <TableRow key={titulo.id}>
2319
- <TableCell className="font-medium">
2320
- <Link
2321
- href={`/finance/accounts-receivable/installments/${titulo.id}`}
2322
- className="hover:underline"
2323
- >
2324
- {titulo.documento}
2325
- </Link>
2326
- {titulo.anexos.length > 0 && (
2327
- <Button
2328
- type="button"
2329
- variant="ghost"
2330
- size="icon"
2331
- className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2332
- onClick={(event) => {
2333
- event.preventDefault();
2334
- event.stopPropagation();
2335
- const firstAttachmentId =
2336
- titulo.anexosDetalhes?.[0]?.id;
2337
- void handleOpenAttachment(firstAttachmentId);
2338
- }}
2339
- aria-label={t('table.actions.openAttachment')}
2340
- >
2341
- <Paperclip className="h-3 w-3" />
2342
- </Button>
2343
- )}
2344
- </TableCell>
2345
- <TableCell>{cliente?.nome}</TableCell>
2346
- <TableCell>{titulo.competencia}</TableCell>
2347
- <TableCell>
2348
- {proximaParcela
2349
- ? formatarData(proximaParcela.vencimento)
2350
- : '-'}
2351
- </TableCell>
2352
- <TableCell className="text-right">
2353
- <Money value={titulo.valorTotal} />
2354
- </TableCell>
2355
- <TableCell>
2356
- <Badge className={canal.className} variant="outline">
2357
- {canal.label}
2358
- </Badge>
2359
- </TableCell>
2360
- <TableCell>
2361
- <StatusBadge status={titulo.status} />
2362
- </TableCell>
2363
- <TableCell>
2364
- <DropdownMenu>
2365
- <DropdownMenuTrigger asChild>
2366
- <Button variant="ghost" size="icon">
2367
- <MoreHorizontal className="h-4 w-4" />
2368
- <span className="sr-only">
2369
- {t('table.actions.srActions')}
2370
- </span>
2371
- </Button>
2372
- </DropdownMenuTrigger>
2373
- <DropdownMenuContent align="end">
2374
- <DropdownMenuItem asChild>
2375
- <Link
2376
- href={`/finance/accounts-receivable/installments/${titulo.id}`}
2377
- >
2378
- <Eye className="mr-2 h-4 w-4" />
2379
- {t('table.actions.viewDetails')}
2380
- </Link>
2381
- </DropdownMenuItem>
2382
- <DropdownMenuItem
2383
- disabled={titulo.status !== 'rascunho'}
2384
- onClick={() => setEditingTitleId(titulo.id)}
2385
- >
2386
- <Edit className="mr-2 h-4 w-4" />
2387
- {t('table.actions.edit')}
2388
- </DropdownMenuItem>
2389
- <DropdownMenuSeparator />
2390
- <DropdownMenuItem
2391
- disabled={
2392
- !['aberto', 'parcial'].includes(titulo.status)
2393
- }
2394
- >
2395
- <Download className="mr-2 h-4 w-4" />
2396
- {t('table.actions.registerReceipt')}
2397
- </DropdownMenuItem>
2398
- <DropdownMenuItem>
2399
- <Send className="mr-2 h-4 w-4" />
2400
- {t('table.actions.sendCollection')}
2401
- </DropdownMenuItem>
2402
- </DropdownMenuContent>
2403
- </DropdownMenu>
2404
- </TableCell>
2405
- </TableRow>
2406
- );
2407
- })}
2408
- </TableBody>
2409
- </Table>
2426
+ <div className="min-w-0">
2427
+ <SearchBar
2428
+ searchQuery={search}
2429
+ onSearchChange={setSearch}
2430
+ onSearch={() => undefined}
2431
+ placeholder={t('filters.searchPlaceholder')}
2432
+ controls={[
2433
+ {
2434
+ id: 'status',
2435
+ type: 'select',
2436
+ value: statusFilter,
2437
+ onChange: setStatusFilter,
2438
+ placeholder: t('filters.status'),
2439
+ options: [
2440
+ { value: 'all', label: t('statuses.all') },
2441
+ { value: 'aberto', label: t('statuses.aberto') },
2442
+ { value: 'parcial', label: t('statuses.parcial') },
2443
+ { value: 'liquidado', label: t('statuses.liquidado') },
2444
+ { value: 'vencido', label: t('statuses.vencido') },
2445
+ { value: 'cancelado', label: t('statuses.cancelado') },
2446
+ ],
2447
+ },
2448
+ ]}
2449
+ />
2410
2450
  </div>
2411
2451
 
2412
- <div className="flex items-center justify-between">
2413
- <p className="text-sm text-muted-foreground">
2414
- {t('footer.showing', {
2415
- filtered: titulosReceber.length,
2416
- total: paginatedTitlesResponse?.total || 0,
2417
- })}
2418
- </p>
2419
- <div className="flex items-center gap-2">
2420
- <Button
2421
- variant="outline"
2422
- size="sm"
2423
- disabled={!paginatedTitlesResponse?.prev || isFetchingTitles}
2424
- onClick={() => setPage((current) => Math.max(1, current - 1))}
2425
- >
2426
- {t('footer.previous')}
2427
- </Button>
2428
- <Button
2429
- variant="outline"
2430
- size="sm"
2431
- disabled={!paginatedTitlesResponse?.next || isFetchingTitles}
2432
- onClick={() => setPage((current) => current + 1)}
2433
- >
2434
- {t('footer.next')}
2435
- </Button>
2452
+ <FinancePageSection className="border-none shadow-none p-0">
2453
+ {titulosReceber.length > 0 ? (
2454
+ <div className="overflow-x-auto">
2455
+ <Table className="min-w-[760px]">
2456
+ <TableHeader>
2457
+ <TableRow>
2458
+ <TableHead>{t('table.headers.document')}</TableHead>
2459
+ <TableHead>{t('table.headers.client')}</TableHead>
2460
+ <TableHead>{t('table.headers.competency')}</TableHead>
2461
+ <TableHead>{t('table.headers.dueDate')}</TableHead>
2462
+ <TableHead className="text-right">
2463
+ {t('table.headers.value')}
2464
+ </TableHead>
2465
+ <TableHead>{t('table.headers.channel')}</TableHead>
2466
+ <TableHead>{t('table.headers.status')}</TableHead>
2467
+ <TableHead className="w-[50px]" />
2468
+ </TableRow>
2469
+ </TableHeader>
2470
+ <TableBody>
2471
+ {titulosReceber.map((titulo) => {
2472
+ const cliente = getPessoaById(titulo.clienteId);
2473
+ const canal =
2474
+ canalBadge[titulo.canal as keyof typeof canalBadge] ||
2475
+ canalBadge.transferencia;
2476
+ const proximaParcela = titulo.parcelas.find(
2477
+ (p: any) => p.status === 'aberto' || p.status === 'vencido'
2478
+ );
2479
+
2480
+ return (
2481
+ <TableRow key={titulo.id} className="hover:bg-muted/30">
2482
+ <TableCell className="font-medium">
2483
+ <Link
2484
+ href={`/finance/accounts-receivable/installments/${titulo.id}`}
2485
+ className="cursor-pointer hover:underline"
2486
+ >
2487
+ {titulo.documento}
2488
+ </Link>
2489
+ {titulo.anexos.length > 0 && (
2490
+ <Button
2491
+ type="button"
2492
+ variant="ghost"
2493
+ size="icon"
2494
+ className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2495
+ onClick={(event) => {
2496
+ event.preventDefault();
2497
+ event.stopPropagation();
2498
+ const firstAttachmentId =
2499
+ titulo.anexosDetalhes?.[0]?.id;
2500
+ void handleOpenAttachment(firstAttachmentId);
2501
+ }}
2502
+ aria-label={t('table.actions.openAttachment')}
2503
+ >
2504
+ <Paperclip className="h-3 w-3" />
2505
+ </Button>
2506
+ )}
2507
+ </TableCell>
2508
+ <TableCell>{cliente?.nome}</TableCell>
2509
+ <TableCell>{titulo.competencia}</TableCell>
2510
+ <TableCell>
2511
+ {proximaParcela
2512
+ ? formatarData(proximaParcela.vencimento)
2513
+ : '-'}
2514
+ </TableCell>
2515
+ <TableCell className="text-right">
2516
+ <Money value={titulo.valorTotal} />
2517
+ </TableCell>
2518
+ <TableCell>
2519
+ <Badge className={canal.className} variant="outline">
2520
+ {canal.label}
2521
+ </Badge>
2522
+ </TableCell>
2523
+ <TableCell>
2524
+ <StatusBadge status={titulo.status} />
2525
+ </TableCell>
2526
+ <TableCell>
2527
+ <DropdownMenu>
2528
+ <DropdownMenuTrigger asChild>
2529
+ <Button variant="ghost" size="icon">
2530
+ <MoreHorizontal className="h-4 w-4" />
2531
+ <span className="sr-only">
2532
+ {t('table.actions.srActions')}
2533
+ </span>
2534
+ </Button>
2535
+ </DropdownMenuTrigger>
2536
+ <DropdownMenuContent align="end">
2537
+ <DropdownMenuItem asChild>
2538
+ <Link
2539
+ href={`/finance/accounts-receivable/installments/${titulo.id}`}
2540
+ >
2541
+ <Eye className="mr-2 h-4 w-4" />
2542
+ {t('table.actions.viewDetails')}
2543
+ </Link>
2544
+ </DropdownMenuItem>
2545
+ <DropdownMenuItem
2546
+ disabled={titulo.status !== 'rascunho'}
2547
+ onClick={() => setEditingTitleId(titulo.id)}
2548
+ >
2549
+ <Edit className="mr-2 h-4 w-4" />
2550
+ {t('table.actions.edit')}
2551
+ </DropdownMenuItem>
2552
+ <DropdownMenuSeparator />
2553
+ <DropdownMenuItem
2554
+ disabled={
2555
+ !['aberto', 'parcial'].includes(titulo.status)
2556
+ }
2557
+ >
2558
+ <Download className="mr-2 h-4 w-4" />
2559
+ {t('table.actions.registerReceipt')}
2560
+ </DropdownMenuItem>
2561
+ <DropdownMenuItem>
2562
+ <Send className="mr-2 h-4 w-4" />
2563
+ {t('table.actions.sendCollection')}
2564
+ </DropdownMenuItem>
2565
+ </DropdownMenuContent>
2566
+ </DropdownMenu>
2567
+ </TableCell>
2568
+ </TableRow>
2569
+ );
2570
+ })}
2571
+ </TableBody>
2572
+ </Table>
2573
+ </div>
2574
+ ) : (
2575
+ <div className="px-4 py-8 sm:px-6">
2576
+ <EmptyState
2577
+ icon={<FileText className="h-12 w-12" />}
2578
+ title={t('empty.title')}
2579
+ description={t('empty.description')}
2580
+ actionLabel={t('newTitle.action')}
2581
+ onAction={() => void refetchTitles()}
2582
+ />
2583
+ </div>
2584
+ )}
2585
+
2586
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2587
+ <PaginationFooter
2588
+ currentPage={page}
2589
+ pageSize={pageSize}
2590
+ totalItems={paginatedTitlesResponse?.total || 0}
2591
+ onPageChange={setPage}
2592
+ onPageSizeChange={() => undefined}
2593
+ pageSizeOptions={[10]}
2594
+ />
2436
2595
  </div>
2437
- </div>
2596
+ </FinancePageSection>
2438
2597
  </Page>
2439
2598
  );
2440
2599
  }