@hed-hog/finance 0.0.301 → 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.
@@ -1,46 +1,46 @@
1
1
  'use client';
2
2
 
3
- import {
4
- CategoryFieldWithCreate,
5
- CostCenterFieldWithCreate,
6
- } from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
7
- import {
8
- FinancePageSection,
9
- FinanceSheetBody,
10
- FinanceSheetSection,
11
- } from '@/app/(app)/(libraries)/finance/_components/finance-layout';
12
- import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
13
- import {
14
- EmptyState,
15
- Page,
16
- PageHeader,
17
- PaginationFooter,
18
- SearchBar,
19
- } from '@/components/entity-list';
20
- import { Badge } from '@/components/ui/badge';
21
- import { Button } from '@/components/ui/button';
22
- import { Checkbox } from '@/components/ui/checkbox';
3
+ import {
4
+ CategoryFieldWithCreate,
5
+ CostCenterFieldWithCreate,
6
+ } from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
7
+ import {
8
+ FinancePageSection,
9
+ FinanceSheetBody,
10
+ FinanceSheetSection,
11
+ } from '@/app/(app)/(libraries)/finance/_components/finance-layout';
12
+ import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
13
+ import {
14
+ EmptyState,
15
+ Page,
16
+ PageHeader,
17
+ PaginationFooter,
18
+ SearchBar,
19
+ } from '@/components/entity-list';
20
+ import { Badge } from '@/components/ui/badge';
21
+ import { Button } from '@/components/ui/button';
22
+ import { Checkbox } from '@/components/ui/checkbox';
23
23
  import {
24
24
  DropdownMenu,
25
- DropdownMenuContent,
26
- DropdownMenuItem,
27
- DropdownMenuSeparator,
28
- DropdownMenuTrigger,
29
- } from '@/components/ui/dropdown-menu';
30
- import {
31
- Form,
32
- FormControl,
25
+ DropdownMenuContent,
26
+ DropdownMenuItem,
27
+ DropdownMenuSeparator,
28
+ DropdownMenuTrigger,
29
+ } from '@/components/ui/dropdown-menu';
30
+ import {
31
+ Form,
32
+ FormControl,
33
33
  FormField,
34
34
  FormItem,
35
35
  FormLabel,
36
36
  FormMessage,
37
- } from '@/components/ui/form';
38
- import { Input } from '@/components/ui/input';
39
- import { InputMoney } from '@/components/ui/input-money';
40
- import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
41
- import { Label } from '@/components/ui/label';
42
- import { Money } from '@/components/ui/money';
43
- import { Progress } from '@/components/ui/progress';
37
+ } from '@/components/ui/form';
38
+ import { Input } from '@/components/ui/input';
39
+ import { InputMoney } from '@/components/ui/input-money';
40
+ import { KpiCardsGrid } from '@/components/ui/kpi-cards-grid';
41
+ import { Label } from '@/components/ui/label';
42
+ import { Money } from '@/components/ui/money';
43
+ import { Progress } from '@/components/ui/progress';
44
44
  import {
45
45
  Select,
46
46
  SelectContent,
@@ -72,22 +72,22 @@ import {
72
72
  TooltipTrigger,
73
73
  } from '@/components/ui/tooltip';
74
74
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
75
- import { zodResolver } from '@hookform/resolvers/zod';
76
- import {
77
- AlertTriangle,
78
- Download,
79
- Edit,
80
- Eye,
81
- FileText,
82
- Loader2,
83
- MoreHorizontal,
84
- Paperclip,
75
+ import { zodResolver } from '@hookform/resolvers/zod';
76
+ import {
77
+ AlertTriangle,
78
+ Download,
79
+ Edit,
80
+ Eye,
81
+ FileText,
82
+ Loader2,
83
+ MoreHorizontal,
84
+ Paperclip,
85
85
  Plus,
86
- Send,
87
- Trash2,
88
- Upload,
89
- Wallet,
90
- } from 'lucide-react';
86
+ Send,
87
+ Trash2,
88
+ Upload,
89
+ Wallet,
90
+ } from 'lucide-react';
91
91
  import { useTranslations } from 'next-intl';
92
92
  import Link from 'next/link';
93
93
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
@@ -642,511 +642,523 @@ function NovoTituloSheet({
642
642
  {t('newTitle.action')}
643
643
  </Button>
644
644
  </SheetTrigger>
645
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
646
- <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
647
- <SheetTitle>{t('newTitle.title')}</SheetTitle>
648
- <SheetDescription>{t('newTitle.description')}</SheetDescription>
649
- </SheetHeader>
650
- <Form {...form}>
651
- <form
652
- className="flex h-full flex-col overflow-hidden"
653
- onSubmit={form.handleSubmit(handleSubmit)}
654
- >
655
- <FinanceSheetBody>
656
- <FinanceSheetSection
657
- title={t('sections.main.title')}
658
- description={t('sections.main.description')}
659
- >
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;
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">
647
+ <SheetTitle>{t('newTitle.title')}</SheetTitle>
648
+ <SheetDescription>{t('newTitle.description')}</SheetDescription>
649
+ </SheetHeader>
650
+ <Form {...form}>
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
672
678
  }
679
+ />
673
680
 
674
- clearUploadedFile();
675
- void uploadRelatedFile(file);
676
- }}
677
- disabled={
678
- isUploadingFile ||
679
- isExtractingFileData ||
680
- form.formState.isSubmitting
681
- }
682
- />
683
-
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 && (
681
+ <div className="grid w-full grid-cols-2 gap-2">
723
682
  <Tooltip>
724
683
  <TooltipTrigger asChild>
725
684
  <Button
726
685
  type="button"
727
686
  variant="outline"
728
- className="w-full"
729
- onClick={clearUploadedFile}
730
- 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
+ }
731
696
  disabled={
732
697
  isUploadingFile ||
733
698
  isExtractingFileData ||
734
699
  form.formState.isSubmitting
735
700
  }
736
701
  >
737
- <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
+ )}
738
710
  </Button>
739
711
  </TooltipTrigger>
740
712
  <TooltipContent>
741
- {t('common.upload.remove')}
713
+ {uploadedFileId
714
+ ? t('common.upload.change')
715
+ : t('common.upload.upload')}
742
716
  </TooltipContent>
743
717
  </Tooltip>
744
- )}
745
- </div>
746
718
 
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
- )}
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>
753
743
 
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
- })}
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}
761
748
  </p>
762
- </div>
763
- )}
749
+ )}
764
750
 
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
- )}
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
+ )}
771
761
 
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
- })}
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')}
779
766
  </p>
780
767
  )}
781
768
 
782
- {!isExtractingFileData && extractionWarnings.length > 0 && (
783
- <p className="truncate text-xs text-muted-foreground">
784
- {extractionWarnings[0]}
785
- </p>
786
- )}
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>
787
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
+ />
788
801
  </div>
789
802
 
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="FAT-00000" {...field} />
798
- </FormControl>
799
- <FormMessage />
800
- </FormItem>
801
- )}
803
+ <PersonFieldWithCreate
804
+ form={form}
805
+ name="clienteId"
806
+ label={t('fields.client')}
807
+ entityLabel="cliente"
808
+ selectPlaceholder={t('common.select')}
802
809
  />
803
- </div>
804
810
 
805
- <PersonFieldWithCreate
806
- form={form}
807
- name="clienteId"
808
- label={t('fields.client')}
809
- entityLabel="cliente"
810
- selectPlaceholder={t('common.select')}
811
- />
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>
812
849
 
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>
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>
904
+
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>
829
948
  )}
830
- />
831
949
 
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 || ''}
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
+ )}
843
982
  />
844
- </FormControl>
845
- <FormMessage />
846
- </FormItem>
847
- )}
848
- />
849
- </div>
850
983
 
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"
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
+ )}
866
1023
  />
867
- </FormControl>
868
- <FormMessage />
869
- </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>
870
1045
  )}
871
- />
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
+ />
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>
872
1107
 
1108
+ <FinanceSheetSection
1109
+ title={t('sections.notes.title')}
1110
+ description={t('sections.notes.description')}
1111
+ >
873
1112
  <FormField
874
1113
  control={form.control}
875
- name="installmentsCount"
1114
+ name="descricao"
876
1115
  render={({ field }) => (
877
1116
  <FormItem>
878
- <FormLabel>
879
- {t('installmentsEditor.countLabel')}
880
- </FormLabel>
1117
+ <FormLabel>{t('fields.description')}</FormLabel>
881
1118
  <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
- }}
1119
+ <Textarea
1120
+ placeholder={t('newTitle.descriptionPlaceholder')}
1121
+ {...field}
1122
+ value={field.value || ''}
893
1123
  />
894
1124
  </FormControl>
895
1125
  <FormMessage />
896
1126
  </FormItem>
897
1127
  )}
898
1128
  />
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-receivable"
928
- checked={autoRedistributeInstallments}
929
- onCheckedChange={(checked) =>
930
- setAutoRedistributeInstallments(checked === true)
931
- }
932
- />
933
- <Label
934
- htmlFor="auto-redistribute-installments-receivable"
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
- }`}
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)}
1030
1138
  >
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
- )}
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>
1042
1158
  </div>
1043
-
1044
- </FinanceSheetSection>
1045
-
1046
- <FinanceSheetSection
1047
- title={t('sections.classification.title')}
1048
- description={t('sections.classification.description')}
1049
- >
1050
- <CategoryFieldWithCreate
1051
- form={form}
1052
- name="categoriaId"
1053
- label={t('fields.category')}
1054
- selectPlaceholder={t('common.select')}
1055
- categories={categorias}
1056
- categoryKind="receita"
1057
- onCreated={onOptionsUpdated}
1058
- />
1059
-
1060
- <CostCenterFieldWithCreate
1061
- form={form}
1062
- name="centroCustoId"
1063
- label={t('fields.costCenter')}
1064
- selectPlaceholder={t('common.select')}
1065
- costCenters={centrosCusto}
1066
- onCreated={onOptionsUpdated}
1067
- />
1068
-
1069
- <FormField
1070
- control={form.control}
1071
- name="canal"
1072
- render={({ field }) => (
1073
- <FormItem>
1074
- <FormLabel>{t('fields.channel')}</FormLabel>
1075
- <Select value={field.value} onValueChange={field.onChange}>
1076
- <FormControl>
1077
- <SelectTrigger className="w-full">
1078
- <SelectValue placeholder={t('common.select')} />
1079
- </SelectTrigger>
1080
- </FormControl>
1081
- <SelectContent>
1082
- <SelectItem value="boleto">
1083
- {t('channels.boleto')}
1084
- </SelectItem>
1085
- <SelectItem value="pix">PIX</SelectItem>
1086
- <SelectItem value="cartao">
1087
- {t('channels.card')}
1088
- </SelectItem>
1089
- <SelectItem value="transferencia">
1090
- {t('channels.transfer')}
1091
- </SelectItem>
1092
- </SelectContent>
1093
- </Select>
1094
- <FormMessage />
1095
- </FormItem>
1096
- )}
1097
- />
1098
- </FinanceSheetSection>
1099
-
1100
- <FinanceSheetSection
1101
- title={t('sections.notes.title')}
1102
- description={t('sections.notes.description')}
1103
- >
1104
- <FormField
1105
- control={form.control}
1106
- name="descricao"
1107
- render={({ field }) => (
1108
- <FormItem>
1109
- <FormLabel>{t('fields.description')}</FormLabel>
1110
- <FormControl>
1111
- <Textarea
1112
- placeholder={t('newTitle.descriptionPlaceholder')}
1113
- {...field}
1114
- value={field.value || ''}
1115
- />
1116
- </FormControl>
1117
- <FormMessage />
1118
- </FormItem>
1119
- )}
1120
- />
1121
- </FinanceSheetSection>
1122
- </FinanceSheetBody>
1123
-
1124
- <div className="border-t border-border/50 px-4 py-4 sm:px-6">
1125
- <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
1126
- <Button type="button" variant="outline" onClick={() => setOpen(false)}>
1127
- {t('common.cancel')}
1128
- </Button>
1129
- <Button
1130
- type="submit"
1131
- disabled={
1132
- form.formState.isSubmitting ||
1133
- isUploadingFile ||
1134
- isExtractingFileData
1135
- }
1136
- >
1137
- {(isUploadingFile || isExtractingFileData) && (
1138
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1139
- )}
1140
- {isExtractingFileData
1141
- ? t('common.upload.fillingWithAi')
1142
- : isUploadingFile
1143
- ? t('common.upload.uploadingFile')
1144
- : t('common.save')}
1145
- </Button>
1146
- </div>
1147
- </div>
1148
- </form>
1149
- </Form>
1159
+ </div>
1160
+ </form>
1161
+ </Form>
1150
1162
  </SheetContent>
1151
1163
  </Sheet>
1152
1164
  );
@@ -1573,9 +1585,9 @@ function EditarTituloSheet({
1573
1585
  }
1574
1586
  };
1575
1587
 
1576
- const handleSubmit = async (values: NewTitleFormValues) => {
1577
- if (!titulo?.id) {
1578
- showToastHandler?.('error', t('messages.invalidTitleForEdit'));
1588
+ const handleSubmit = async (values: NewTitleFormValues) => {
1589
+ if (!titulo?.id) {
1590
+ showToastHandler?.('error', t('messages.invalidTitleForEdit'));
1579
1591
  return;
1580
1592
  }
1581
1593
 
@@ -1612,523 +1624,534 @@ function EditarTituloSheet({
1612
1624
  showToastHandler?.('success', t('messages.updateSuccess'));
1613
1625
  onOpenChange(false);
1614
1626
  } catch {
1615
- showToastHandler?.('error', t('messages.updateError'));
1616
- }
1617
- };
1618
-
1619
- const handleCancel = () => {
1620
- onOpenChange(false);
1621
- };
1622
-
1623
- return (
1624
- <Sheet open={open} onOpenChange={onOpenChange}>
1625
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl lg:max-w-4xl">
1626
- <SheetHeader className="border-b border-border/50 px-4 py-4 sm:px-6">
1627
- <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1628
- <SheetDescription>{t('editTitle.description')}</SheetDescription>
1629
- </SheetHeader>
1630
- <Form {...form}>
1631
- <form
1632
- className="flex h-full flex-col overflow-hidden"
1633
- onSubmit={form.handleSubmit(handleSubmit)}
1634
- >
1635
- <FinanceSheetBody>
1636
- <FinanceSheetSection
1637
- title={t('sections.main.title')}
1638
- description={t('sections.main.description')}
1639
- >
1640
- <div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
1641
- <div className="grid gap-2">
1642
- <FormLabel>{t('common.upload.label')}</FormLabel>
1643
- <Input
1644
- ref={fileInputRef}
1645
- className="hidden"
1646
- type="file"
1647
- accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
1648
- onChange={(event) => {
1649
- const file = event.target.files?.[0];
1650
- if (!file) {
1651
- return;
1652
- }
1627
+ showToastHandler?.('error', t('messages.updateError'));
1628
+ }
1629
+ };
1653
1630
 
1654
- clearUploadedFile();
1655
- void uploadRelatedFile(file);
1656
- }}
1657
- disabled={
1658
- isUploadingFile ||
1659
- isExtractingFileData ||
1660
- form.formState.isSubmitting
1661
- }
1662
- />
1631
+ const handleCancel = () => {
1632
+ onOpenChange(false);
1633
+ };
1663
1634
 
1664
- <div className="grid w-full grid-cols-2 gap-2">
1665
- <Tooltip>
1666
- <TooltipTrigger asChild>
1667
- <Button
1668
- type="button"
1669
- variant="outline"
1670
- className={
1671
- uploadedFileId ? 'w-full' : 'col-span-2 w-full'
1672
- }
1673
- onClick={handleSelectFile}
1674
- aria-label={
1675
- uploadedFileId
1676
- ? t('common.upload.change')
1677
- : t('common.upload.upload')
1678
- }
1679
- disabled={
1680
- isUploadingFile ||
1681
- isExtractingFileData ||
1682
- form.formState.isSubmitting
1683
- }
1684
- >
1685
- {uploadedFileId ? (
1686
- <Upload className="h-4 w-4" />
1687
- ) : (
1688
- <>
1689
- <Upload className="mr-2 h-4 w-4" />
1690
- {t('common.upload.upload')}
1691
- </>
1692
- )}
1693
- </Button>
1694
- </TooltipTrigger>
1695
- <TooltipContent>
1696
- {uploadedFileId
1697
- ? t('common.upload.change')
1698
- : t('common.upload.upload')}
1699
- </TooltipContent>
1700
- </Tooltip>
1701
-
1702
- {uploadedFileId && (
1635
+ return (
1636
+ <Sheet open={open} onOpenChange={onOpenChange}>
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">
1639
+ <SheetTitle>{t('table.actions.edit')}</SheetTitle>
1640
+ <SheetDescription>{t('editTitle.description')}</SheetDescription>
1641
+ </SheetHeader>
1642
+ <Form {...form}>
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
1673
+ }
1674
+ />
1675
+
1676
+ <div className="grid w-full grid-cols-2 gap-2">
1703
1677
  <Tooltip>
1704
1678
  <TooltipTrigger asChild>
1705
1679
  <Button
1706
1680
  type="button"
1707
1681
  variant="outline"
1708
- className="w-full"
1709
- onClick={clearUploadedFile}
1710
- 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
+ }
1711
1691
  disabled={
1712
1692
  isUploadingFile ||
1713
1693
  isExtractingFileData ||
1714
1694
  form.formState.isSubmitting
1715
1695
  }
1716
1696
  >
1717
- <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
+ )}
1718
1705
  </Button>
1719
1706
  </TooltipTrigger>
1720
1707
  <TooltipContent>
1721
- {t('common.upload.remove')}
1708
+ {uploadedFileId
1709
+ ? t('common.upload.change')
1710
+ : t('common.upload.upload')}
1722
1711
  </TooltipContent>
1723
1712
  </Tooltip>
1724
- )}
1725
- </div>
1726
1713
 
1727
- <div className="space-y-1">
1728
- {(uploadedFileId || uploadedFileName) && (
1729
- <p className="truncate text-xs text-muted-foreground">
1730
- {t('common.upload.selectedPrefix')} {uploadedFileName}
1731
- </p>
1732
- )}
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>
1733
1738
 
1734
- {isUploadingFile && !isExtractingFileData && (
1735
- <div className="space-y-1">
1736
- <Progress value={uploadProgress} className="h-2" />
1737
- <p className="text-xs text-muted-foreground">
1738
- {t('common.upload.uploadingProgress', {
1739
- progress: uploadProgress,
1740
- })}
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}
1741
1743
  </p>
1742
- </div>
1743
- )}
1744
+ )}
1744
1745
 
1745
- {isExtractingFileData && (
1746
- <p className="flex items-center gap-2 text-xs text-primary">
1747
- <Loader2 className="h-4 w-4 animate-spin" />
1748
- {t('common.upload.processingAi')}
1749
- </p>
1750
- )}
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
+ )}
1751
1756
 
1752
- {!isExtractingFileData &&
1753
- extractionConfidence !== null &&
1754
- extractionConfidence < 70 && (
1755
- <p className="text-xs text-destructive">
1756
- {t('common.upload.lowConfidence', {
1757
- confidence: Math.round(extractionConfidence),
1758
- })}
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')}
1759
1761
  </p>
1760
1762
  )}
1761
1763
 
1762
- {!isExtractingFileData && extractionWarnings.length > 0 && (
1763
- <p className="truncate text-xs text-muted-foreground">
1764
- {extractionWarnings[0]}
1765
- </p>
1766
- )}
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>
1767
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
+ />
1768
1796
  </div>
1769
1797
 
1770
- <FormField
1771
- control={form.control}
1772
- name="documento"
1773
- render={({ field }) => (
1774
- <FormItem>
1775
- <FormLabel>{t('fields.document')}</FormLabel>
1776
- <FormControl>
1777
- <Input placeholder="FAT-00000" {...field} />
1778
- </FormControl>
1779
- <FormMessage />
1780
- </FormItem>
1781
- )}
1798
+ <PersonFieldWithCreate
1799
+ form={form}
1800
+ name="clienteId"
1801
+ label={t('fields.client')}
1802
+ entityLabel="cliente"
1803
+ selectPlaceholder={t('common.select')}
1782
1804
  />
1783
- </div>
1784
1805
 
1785
- <PersonFieldWithCreate
1786
- form={form}
1787
- name="clienteId"
1788
- label={t('fields.client')}
1789
- entityLabel="cliente"
1790
- selectPlaceholder={t('common.select')}
1791
- />
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>
1792
1844
 
1793
- <div className="grid grid-cols-2 items-start gap-3">
1794
- <FormField
1795
- control={form.control}
1796
- name="competencia"
1797
- render={({ field }) => (
1798
- <FormItem>
1799
- <FormLabel>{t('fields.competency')}</FormLabel>
1800
- <FormControl>
1801
- <Input
1802
- type="month"
1803
- {...field}
1804
- value={field.value || ''}
1805
- />
1806
- </FormControl>
1807
- <FormMessage />
1808
- </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>
1809
1945
  )}
1810
- />
1811
1946
 
1812
- <FormField
1813
- control={form.control}
1814
- name="vencimento"
1815
- render={({ field }) => (
1816
- <FormItem>
1817
- <FormLabel>{t('fields.dueDate')}</FormLabel>
1818
- <FormControl>
1819
- <Input
1820
- type="date"
1821
- {...field}
1822
- 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
+ )}
1823
1979
  />
1824
- </FormControl>
1825
- <FormMessage />
1826
- </FormItem>
1827
- )}
1828
- />
1829
- </div>
1830
1980
 
1831
- <div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
1832
- <FormField
1833
- control={form.control}
1834
- name="valor"
1835
- render={({ field }) => (
1836
- <FormItem>
1837
- <FormLabel>{t('fields.totalValue')}</FormLabel>
1838
- <FormControl>
1839
- <InputMoney
1840
- ref={field.ref}
1841
- name={field.name}
1842
- value={field.value}
1843
- onBlur={field.onBlur}
1844
- onValueChange={(value) => field.onChange(value ?? 0)}
1845
- 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
+ )}
1846
2020
  />
1847
- </FormControl>
1848
- <FormMessage />
1849
- </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>
1850
2042
  )}
1851
- />
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>
1852
2104
 
2105
+ <FinanceSheetSection
2106
+ title={t('sections.notes.title')}
2107
+ description={t('sections.notes.description')}
2108
+ >
1853
2109
  <FormField
1854
2110
  control={form.control}
1855
- name="installmentsCount"
2111
+ name="descricao"
1856
2112
  render={({ field }) => (
1857
2113
  <FormItem>
1858
- <FormLabel>
1859
- {t('installmentsEditor.countLabel')}
1860
- </FormLabel>
2114
+ <FormLabel>{t('fields.description')}</FormLabel>
1861
2115
  <FormControl>
1862
- <Input
1863
- type="number"
1864
- min={1}
1865
- max={120}
1866
- value={field.value}
1867
- onChange={(event) => {
1868
- const nextValue = Number(event.target.value || 1);
1869
- field.onChange(
1870
- Number.isNaN(nextValue) ? 1 : nextValue
1871
- );
1872
- setIsInstallmentsEdited(false);
1873
- }}
2116
+ <Textarea
2117
+ placeholder={t('newTitle.descriptionPlaceholder')}
2118
+ {...field}
2119
+ value={field.value || ''}
1874
2120
  />
1875
2121
  </FormControl>
1876
2122
  <FormMessage />
1877
2123
  </FormItem>
1878
2124
  )}
1879
2125
  />
1880
- </div>
1881
-
1882
- <div className="space-y-3 rounded-md border p-3">
1883
- <div className="flex items-center justify-between gap-2">
1884
- <p className="text-sm font-medium">
1885
- {t('installmentsEditor.title')}
1886
- </p>
1887
- <Button
1888
- type="button"
1889
- variant="outline"
1890
- size="sm"
1891
- onClick={() => {
1892
- setIsInstallmentsEdited(false);
1893
- replaceInstallments(
1894
- buildEqualInstallments(
1895
- form.getValues('installmentsCount'),
1896
- form.getValues('valor'),
1897
- form.getValues('vencimento')
1898
- )
1899
- );
1900
- }}
1901
- >
1902
- {t('installmentsEditor.recalculate')}
1903
- </Button>
1904
- </div>
1905
-
1906
- <div className="flex items-center gap-2">
1907
- <Checkbox
1908
- id="auto-redistribute-installments-edit-receivable"
1909
- checked={autoRedistributeInstallments}
1910
- onCheckedChange={(checked) =>
1911
- setAutoRedistributeInstallments(checked === true)
1912
- }
1913
- />
1914
- <Label
1915
- htmlFor="auto-redistribute-installments-edit-receivable"
1916
- className="text-xs text-muted-foreground"
1917
- >
1918
- {t('installmentsEditor.autoRedistributeLabel')}
1919
- </Label>
1920
- </div>
1921
-
1922
- {autoRedistributeInstallments && (
1923
- <p className="text-xs text-muted-foreground">
1924
- {t('installmentsEditor.autoRedistributeHint')}
1925
- </p>
1926
- )}
1927
-
1928
- <div className="space-y-2">
1929
- {installmentFields.map((installment, index) => (
1930
- <div
1931
- key={installment.id}
1932
- className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
1933
- >
1934
- <div className="flex items-center text-sm text-muted-foreground">
1935
- #{index + 1}
1936
- </div>
1937
-
1938
- <FormField
1939
- control={form.control}
1940
- name={`installments.${index}.dueDate` as const}
1941
- render={({ field }) => (
1942
- <FormItem>
1943
- <FormLabel className="text-xs">
1944
- {t('installmentsEditor.dueDateLabel')}
1945
- </FormLabel>
1946
- <FormControl>
1947
- <Input
1948
- type="date"
1949
- {...field}
1950
- value={field.value || ''}
1951
- onChange={(event) => {
1952
- setIsInstallmentsEdited(true);
1953
- field.onChange(event);
1954
- }}
1955
- />
1956
- </FormControl>
1957
- <FormMessage />
1958
- </FormItem>
1959
- )}
1960
- />
1961
-
1962
- <FormField
1963
- control={form.control}
1964
- name={`installments.${index}.amount` as const}
1965
- render={({ field }) => (
1966
- <FormItem>
1967
- <FormLabel className="text-xs">
1968
- {t('installmentsEditor.amountLabel')}
1969
- </FormLabel>
1970
- <FormControl>
1971
- <InputMoney
1972
- ref={field.ref}
1973
- name={field.name}
1974
- value={field.value}
1975
- onBlur={() => {
1976
- field.onBlur();
1977
-
1978
- if (!autoRedistributeInstallments) {
1979
- return;
1980
- }
1981
-
1982
- clearScheduledRedistribution(index);
1983
- runInstallmentRedistribution(index);
1984
- }}
1985
- onValueChange={(value) => {
1986
- setIsInstallmentsEdited(true);
1987
- field.onChange(value ?? 0);
1988
-
1989
- if (!autoRedistributeInstallments) {
1990
- return;
1991
- }
1992
-
1993
- scheduleInstallmentRedistribution(index);
1994
- }}
1995
- placeholder="0,00"
1996
- />
1997
- </FormControl>
1998
- <FormMessage />
1999
- </FormItem>
2000
- )}
2001
- />
2002
- </div>
2003
- ))}
2004
- </div>
2005
-
2006
- <p
2007
- className={`text-xs ${
2008
- installmentsDiffCents === 0
2009
- ? 'text-muted-foreground'
2010
- : 'text-destructive'
2011
- }`}
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
+ }
2012
2141
  >
2013
- {t('installmentsEditor.totalPrefix', {
2014
- total: installmentsTotal.toFixed(2),
2015
- })}
2016
- {installmentsDiffCents > 0 &&
2017
- ` ${t('installmentsEditor.adjustmentNeeded')}`}
2018
- </p>
2019
- {form.formState.errors.installments?.message && (
2020
- <p className="text-xs text-destructive">
2021
- {form.formState.errors.installments.message}
2022
- </p>
2023
- )}
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>
2024
2151
  </div>
2025
-
2026
- </FinanceSheetSection>
2027
-
2028
- <FinanceSheetSection
2029
- title={t('sections.classification.title')}
2030
- description={t('sections.classification.description')}
2031
- >
2032
- <CategoryFieldWithCreate
2033
- form={form}
2034
- name="categoriaId"
2035
- label={t('fields.category')}
2036
- selectPlaceholder={t('common.select')}
2037
- categories={categorias}
2038
- categoryKind="receita"
2039
- onCreated={onOptionsUpdated}
2040
- />
2041
-
2042
- <CostCenterFieldWithCreate
2043
- form={form}
2044
- name="centroCustoId"
2045
- label={t('fields.costCenter')}
2046
- selectPlaceholder={t('common.select')}
2047
- costCenters={centrosCusto}
2048
- onCreated={onOptionsUpdated}
2049
- />
2050
-
2051
- <FormField
2052
- control={form.control}
2053
- name="canal"
2054
- render={({ field }) => (
2055
- <FormItem>
2056
- <FormLabel>{t('fields.channel')}</FormLabel>
2057
- <Select value={field.value} onValueChange={field.onChange}>
2058
- <FormControl>
2059
- <SelectTrigger className="w-full">
2060
- <SelectValue placeholder={t('common.select')} />
2061
- </SelectTrigger>
2062
- </FormControl>
2063
- <SelectContent>
2064
- <SelectItem value="boleto">
2065
- {t('channels.boleto')}
2066
- </SelectItem>
2067
- <SelectItem value="pix">PIX</SelectItem>
2068
- <SelectItem value="cartao">
2069
- {t('channels.card')}
2070
- </SelectItem>
2071
- <SelectItem value="transferencia">
2072
- {t('channels.transfer')}
2073
- </SelectItem>
2074
- </SelectContent>
2075
- </Select>
2076
- <FormMessage />
2077
- </FormItem>
2078
- )}
2079
- />
2080
- </FinanceSheetSection>
2081
-
2082
- <FinanceSheetSection
2083
- title={t('sections.notes.title')}
2084
- description={t('sections.notes.description')}
2085
- >
2086
- <FormField
2087
- control={form.control}
2088
- name="descricao"
2089
- render={({ field }) => (
2090
- <FormItem>
2091
- <FormLabel>{t('fields.description')}</FormLabel>
2092
- <FormControl>
2093
- <Textarea
2094
- placeholder={t('newTitle.descriptionPlaceholder')}
2095
- {...field}
2096
- value={field.value || ''}
2097
- />
2098
- </FormControl>
2099
- <FormMessage />
2100
- </FormItem>
2101
- )}
2102
- />
2103
- </FinanceSheetSection>
2104
- </FinanceSheetBody>
2105
-
2106
- <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2107
- <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2108
- <Button type="button" variant="outline" onClick={handleCancel}>
2109
- {t('common.cancel')}
2110
- </Button>
2111
- <Button
2112
- type="submit"
2113
- disabled={
2114
- form.formState.isSubmitting ||
2115
- isUploadingFile ||
2116
- isExtractingFileData
2117
- }
2118
- >
2119
- {(isUploadingFile || isExtractingFileData) && (
2120
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2121
- )}
2122
- {isExtractingFileData
2123
- ? t('common.upload.fillingWithAi')
2124
- : isUploadingFile
2125
- ? t('common.upload.uploadingFile')
2126
- : t('common.save')}
2127
- </Button>
2128
- </div>
2129
- </div>
2130
- </form>
2131
- </Form>
2152
+ </div>
2153
+ </form>
2154
+ </Form>
2132
2155
  </SheetContent>
2133
2156
  </Sheet>
2134
2157
  );
@@ -2148,20 +2171,17 @@ export default function TitulosReceberPage() {
2148
2171
  centrosCusto,
2149
2172
  } = data;
2150
2173
 
2151
- const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
2152
-
2153
- const [search, setSearch] = useState('');
2154
- const [statusFilter, setStatusFilter] = useState<string>('all');
2155
- const [page, setPage] = useState(1);
2156
- const pageSize = 10;
2174
+ const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
2175
+
2176
+ const [search, setSearch] = useState('');
2177
+ const [statusFilter, setStatusFilter] = useState<string>('all');
2178
+ const [page, setPage] = useState(1);
2179
+ const pageSize = 10;
2157
2180
 
2158
2181
  const normalizedStatusFilter =
2159
2182
  statusFilter && statusFilter !== 'all' ? statusFilter : undefined;
2160
2183
 
2161
- const {
2162
- data: paginatedTitlesResponse,
2163
- refetch: refetchTitles,
2164
- } = useQuery<{
2184
+ const { data: paginatedTitlesResponse, refetch: refetchTitles } = useQuery<{
2165
2185
  data: any[];
2166
2186
  total: number;
2167
2187
  page: number;
@@ -2201,74 +2221,76 @@ export default function TitulosReceberPage() {
2201
2221
  },
2202
2222
  placeholderData: (old) => old,
2203
2223
  });
2204
-
2205
- const titulosReceber = paginatedTitlesResponse?.data || [];
2206
- const visibleTitlesTotal = useMemo(
2207
- () =>
2208
- titulosReceber.reduce(
2209
- (acc, title) => acc + Number(title?.valorTotal || 0),
2210
- 0
2211
- ),
2212
- [titulosReceber]
2213
- );
2214
- const visiblePendingTitles = useMemo(
2215
- () =>
2216
- titulosReceber.filter((title) =>
2217
- ['aberto', 'parcial', 'vencido'].includes(String(title?.status || ''))
2218
- ).length,
2219
- [titulosReceber]
2220
- );
2221
- const visibleOverdueTitles = useMemo(
2222
- () =>
2223
- titulosReceber.filter(
2224
- (title) =>
2225
- title?.status === 'vencido' ||
2226
- (Array.isArray(title?.parcelas) &&
2227
- title.parcelas.some((installment: any) => installment.status === 'vencido'))
2228
- ).length,
2229
- [titulosReceber]
2230
- );
2231
- const summaryCards = useMemo(
2232
- () => [
2233
- {
2234
- key: 'visible',
2235
- title: t('summary.cards.visible.title'),
2236
- value: titulosReceber.length,
2237
- description: t('summary.cards.visible.description', {
2238
- total: paginatedTitlesResponse?.total || 0,
2239
- }),
2240
- icon: FileText,
2241
- layout: 'compact' as const,
2242
- },
2243
- {
2244
- key: 'value',
2245
- title: t('summary.cards.value.title'),
2246
- value: <Money value={visibleTitlesTotal} />,
2247
- description: t('summary.cards.value.description'),
2248
- icon: Wallet,
2249
- layout: 'compact' as const,
2250
- },
2251
- {
2252
- key: 'attention',
2253
- title: t('summary.cards.attention.title'),
2254
- value: visiblePendingTitles,
2255
- description: t('summary.cards.attention.description', {
2256
- overdue: visibleOverdueTitles,
2257
- }),
2258
- icon: AlertTriangle,
2259
- layout: 'compact' as const,
2260
- },
2261
- ],
2262
- [
2263
- paginatedTitlesResponse?.total,
2264
- t,
2265
- titulosReceber.length,
2266
- visibleOverdueTitles,
2267
- visiblePendingTitles,
2268
- visibleTitlesTotal,
2269
- ]
2270
- );
2271
- const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
2224
+
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
+ );
2293
+ const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
2272
2294
 
2273
2295
  const editingTitle = useMemo(
2274
2296
  () =>
@@ -2364,9 +2386,9 @@ export default function TitulosReceberPage() {
2364
2386
 
2365
2387
  return (
2366
2388
  <Page>
2367
- <PageHeader
2368
- title={t('header.title')}
2369
- description={t('header.description')}
2389
+ <PageHeader
2390
+ title={t('header.title')}
2391
+ description={t('header.description')}
2370
2392
  breadcrumbs={[
2371
2393
  { label: t('breadcrumbs.home'), href: '/' },
2372
2394
  { label: t('breadcrumbs.finance'), href: '/finance' },
@@ -2395,186 +2417,183 @@ export default function TitulosReceberPage() {
2395
2417
  }}
2396
2418
  onOptionsUpdated={refetchFinanceData}
2397
2419
  />
2398
- </>
2399
- }
2400
- />
2401
-
2402
- <KpiCardsGrid items={summaryCards} columns={3} />
2403
-
2404
- <div className="min-w-0">
2405
- <SearchBar
2406
- searchQuery={search}
2407
- onSearchChange={setSearch}
2408
- onSearch={() => undefined}
2409
- placeholder={t('filters.searchPlaceholder')}
2410
- controls={[
2411
- {
2412
- id: 'status',
2413
- type: 'select',
2414
- value: statusFilter,
2415
- onChange: setStatusFilter,
2416
- placeholder: t('filters.status'),
2417
- options: [
2418
- { value: 'all', label: t('statuses.all') },
2419
- { value: 'aberto', label: t('statuses.aberto') },
2420
- { value: 'parcial', label: t('statuses.parcial') },
2421
- { value: 'liquidado', label: t('statuses.liquidado') },
2422
- { value: 'vencido', label: t('statuses.vencido') },
2423
- { value: 'cancelado', label: t('statuses.cancelado') },
2424
- ],
2425
- },
2426
- ]}
2427
- />
2428
- </div>
2429
-
2430
- <FinancePageSection
2431
- title={t('list.title')}
2432
- description={t('list.description')}
2433
- >
2434
- {titulosReceber.length > 0 ? (
2435
- <div className="overflow-x-auto">
2436
- <Table className="min-w-[760px]">
2437
- <TableHeader>
2438
- <TableRow>
2439
- <TableHead>{t('table.headers.document')}</TableHead>
2440
- <TableHead>{t('table.headers.client')}</TableHead>
2441
- <TableHead>{t('table.headers.competency')}</TableHead>
2442
- <TableHead>{t('table.headers.dueDate')}</TableHead>
2443
- <TableHead className="text-right">
2444
- {t('table.headers.value')}
2445
- </TableHead>
2446
- <TableHead>{t('table.headers.channel')}</TableHead>
2447
- <TableHead>{t('table.headers.status')}</TableHead>
2448
- <TableHead className="w-[50px]" />
2449
- </TableRow>
2450
- </TableHeader>
2451
- <TableBody>
2452
- {titulosReceber.map((titulo) => {
2453
- const cliente = getPessoaById(titulo.clienteId);
2454
- const canal =
2455
- canalBadge[titulo.canal as keyof typeof canalBadge] ||
2456
- canalBadge.transferencia;
2457
- const proximaParcela = titulo.parcelas.find(
2458
- (p: any) => p.status === 'aberto' || p.status === 'vencido'
2459
- );
2460
-
2461
- return (
2462
- <TableRow key={titulo.id} className="hover:bg-muted/30">
2463
- <TableCell className="font-medium">
2464
- <Link
2465
- href={`/finance/accounts-receivable/installments/${titulo.id}`}
2466
- className="cursor-pointer hover:underline"
2467
- >
2468
- {titulo.documento}
2469
- </Link>
2470
- {titulo.anexos.length > 0 && (
2471
- <Button
2472
- type="button"
2473
- variant="ghost"
2474
- size="icon"
2475
- className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
2476
- onClick={(event) => {
2477
- event.preventDefault();
2478
- event.stopPropagation();
2479
- const firstAttachmentId =
2480
- titulo.anexosDetalhes?.[0]?.id;
2481
- void handleOpenAttachment(firstAttachmentId);
2482
- }}
2483
- aria-label={t('table.actions.openAttachment')}
2484
- >
2485
- <Paperclip className="h-3 w-3" />
2486
- </Button>
2487
- )}
2488
- </TableCell>
2489
- <TableCell>{cliente?.nome}</TableCell>
2490
- <TableCell>{titulo.competencia}</TableCell>
2491
- <TableCell>
2492
- {proximaParcela
2493
- ? formatarData(proximaParcela.vencimento)
2494
- : '-'}
2495
- </TableCell>
2496
- <TableCell className="text-right">
2497
- <Money value={titulo.valorTotal} />
2498
- </TableCell>
2499
- <TableCell>
2500
- <Badge className={canal.className} variant="outline">
2501
- {canal.label}
2502
- </Badge>
2503
- </TableCell>
2504
- <TableCell>
2505
- <StatusBadge status={titulo.status} />
2506
- </TableCell>
2507
- <TableCell>
2508
- <DropdownMenu>
2509
- <DropdownMenuTrigger asChild>
2510
- <Button variant="ghost" size="icon">
2511
- <MoreHorizontal className="h-4 w-4" />
2512
- <span className="sr-only">
2513
- {t('table.actions.srActions')}
2514
- </span>
2515
- </Button>
2516
- </DropdownMenuTrigger>
2517
- <DropdownMenuContent align="end">
2518
- <DropdownMenuItem asChild>
2519
- <Link
2520
- href={`/finance/accounts-receivable/installments/${titulo.id}`}
2521
- >
2522
- <Eye className="mr-2 h-4 w-4" />
2523
- {t('table.actions.viewDetails')}
2524
- </Link>
2525
- </DropdownMenuItem>
2526
- <DropdownMenuItem
2527
- disabled={titulo.status !== 'rascunho'}
2528
- onClick={() => setEditingTitleId(titulo.id)}
2529
- >
2530
- <Edit className="mr-2 h-4 w-4" />
2531
- {t('table.actions.edit')}
2532
- </DropdownMenuItem>
2533
- <DropdownMenuSeparator />
2534
- <DropdownMenuItem
2535
- disabled={
2536
- !['aberto', 'parcial'].includes(titulo.status)
2537
- }
2538
- >
2539
- <Download className="mr-2 h-4 w-4" />
2540
- {t('table.actions.registerReceipt')}
2541
- </DropdownMenuItem>
2542
- <DropdownMenuItem>
2543
- <Send className="mr-2 h-4 w-4" />
2544
- {t('table.actions.sendCollection')}
2545
- </DropdownMenuItem>
2546
- </DropdownMenuContent>
2547
- </DropdownMenu>
2548
- </TableCell>
2549
- </TableRow>
2550
- );
2551
- })}
2552
- </TableBody>
2553
- </Table>
2554
- </div>
2555
- ) : (
2556
- <div className="px-4 py-8 sm:px-6">
2557
- <EmptyState
2558
- icon={<FileText className="h-12 w-12" />}
2559
- title={t('empty.title')}
2560
- description={t('empty.description')}
2561
- actionLabel={t('newTitle.action')}
2562
- onAction={() => void refetchTitles()}
2563
- />
2564
- </div>
2565
- )}
2566
-
2567
- <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2568
- <PaginationFooter
2569
- currentPage={page}
2570
- pageSize={pageSize}
2571
- totalItems={paginatedTitlesResponse?.total || 0}
2572
- onPageChange={setPage}
2573
- onPageSizeChange={() => undefined}
2574
- pageSizeOptions={[10]}
2575
- />
2576
- </div>
2577
- </FinancePageSection>
2578
- </Page>
2579
- );
2580
- }
2420
+ </>
2421
+ }
2422
+ />
2423
+
2424
+ <KpiCardsGrid items={summaryCards} columns={3} />
2425
+
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
+ />
2450
+ </div>
2451
+
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
+ />
2595
+ </div>
2596
+ </FinancePageSection>
2597
+ </Page>
2598
+ );
2599
+ }