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