@hed-hog/finance 0.0.318 → 0.0.319

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (66) hide show
  1. package/dist/dto/create-bank-account.dto.d.ts +1 -0
  2. package/dist/dto/create-bank-account.dto.d.ts.map +1 -1
  3. package/dist/dto/create-bank-account.dto.js +7 -0
  4. package/dist/dto/create-bank-account.dto.js.map +1 -1
  5. package/dist/dto/create-bank-statement-entry.dto.d.ts +8 -0
  6. package/dist/dto/create-bank-statement-entry.dto.d.ts.map +1 -0
  7. package/dist/dto/create-bank-statement-entry.dto.js +54 -0
  8. package/dist/dto/create-bank-statement-entry.dto.js.map +1 -0
  9. package/dist/dto/update-bank-account.dto.d.ts +1 -0
  10. package/dist/dto/update-bank-account.dto.d.ts.map +1 -1
  11. package/dist/dto/update-bank-account.dto.js +7 -0
  12. package/dist/dto/update-bank-account.dto.js.map +1 -1
  13. package/dist/dto/update-bank-statement-entry.dto.d.ts +6 -0
  14. package/dist/dto/update-bank-statement-entry.dto.d.ts.map +1 -0
  15. package/dist/dto/update-bank-statement-entry.dto.js +42 -0
  16. package/dist/dto/update-bank-statement-entry.dto.js.map +1 -0
  17. package/dist/finance-bank-accounts.controller.d.ts +25 -13
  18. package/dist/finance-bank-accounts.controller.d.ts.map +1 -1
  19. package/dist/finance-bank-accounts.controller.js +5 -3
  20. package/dist/finance-bank-accounts.controller.js.map +1 -1
  21. package/dist/finance-data.controller.d.ts +4 -0
  22. package/dist/finance-data.controller.d.ts.map +1 -1
  23. package/dist/finance-installments.controller.d.ts +3 -2
  24. package/dist/finance-installments.controller.d.ts.map +1 -1
  25. package/dist/finance-installments.controller.js +10 -6
  26. package/dist/finance-installments.controller.js.map +1 -1
  27. package/dist/finance-statements.controller.d.ts +61 -12
  28. package/dist/finance-statements.controller.d.ts.map +1 -1
  29. package/dist/finance-statements.controller.js +50 -8
  30. package/dist/finance-statements.controller.js.map +1 -1
  31. package/dist/finance-transfers.controller.d.ts +13 -8
  32. package/dist/finance-transfers.controller.d.ts.map +1 -1
  33. package/dist/finance-transfers.controller.js +11 -5
  34. package/dist/finance-transfers.controller.js.map +1 -1
  35. package/dist/finance.service.d.ts +124 -35
  36. package/dist/finance.service.d.ts.map +1 -1
  37. package/dist/finance.service.js +389 -55
  38. package/dist/finance.service.js.map +1 -1
  39. package/hedhog/data/role.yaml +9 -1
  40. package/hedhog/data/route.yaml +42 -0
  41. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +87 -72
  42. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +53 -25
  43. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +8 -0
  44. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +60 -24
  45. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +114 -31
  46. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +25 -3
  47. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +732 -61
  48. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +101 -15
  49. package/hedhog/table/bank_statement_line.yaml +1 -1
  50. package/hedhog/table/cashflow_projection.yaml +1 -1
  51. package/hedhog/table/financial_installment.yaml +2 -2
  52. package/hedhog/table/financial_title.yaml +1 -1
  53. package/hedhog/table/installment_allocation.yaml +1 -1
  54. package/hedhog/table/receivable_schedule.yaml +1 -1
  55. package/hedhog/table/settlement.yaml +1 -1
  56. package/hedhog/table/settlement_allocation.yaml +5 -5
  57. package/package.json +7 -7
  58. package/src/dto/create-bank-account.dto.ts +18 -1
  59. package/src/dto/create-bank-statement-entry.dto.ts +50 -0
  60. package/src/dto/update-bank-account.dto.ts +11 -1
  61. package/src/dto/update-bank-statement-entry.dto.ts +31 -0
  62. package/src/finance-bank-accounts.controller.ts +3 -2
  63. package/src/finance-installments.controller.ts +9 -3
  64. package/src/finance-statements.controller.ts +40 -0
  65. package/src/finance-transfers.controller.ts +7 -1
  66. package/src/finance.service.ts +543 -61
@@ -1,8 +1,14 @@
1
1
  'use client';
2
2
 
3
3
  import { FinancePageSection } from '@/app/(app)/(libraries)/finance/_components/finance-layout';
4
- import { EmptyState, Page, PageHeader } from '@/components/entity-list';
4
+ import {
5
+ EmptyState,
6
+ Page,
7
+ PageHeader,
8
+ PaginationFooter,
9
+ } from '@/components/entity-list';
5
10
  import { Button } from '@/components/ui/button';
11
+ import { DateRangePicker } from '@/components/ui/date-range-picker';
6
12
  import {
7
13
  Dialog,
8
14
  DialogContent,
@@ -57,7 +63,9 @@ import {
57
63
  ArrowDownRight,
58
64
  ArrowUpRight,
59
65
  Download,
66
+ Pencil,
60
67
  Plus,
68
+ Trash2,
61
69
  Upload,
62
70
  } from 'lucide-react';
63
71
  import { useTranslations } from 'next-intl';
@@ -74,6 +82,10 @@ type BankAccount = {
74
82
  saldoAtual: number;
75
83
  };
76
84
 
85
+ type PaginatedBankAccountsResponse = {
86
+ data: BankAccount[];
87
+ };
88
+
77
89
  type Statement = {
78
90
  id: string;
79
91
  contaBancariaId: string;
@@ -87,14 +99,41 @@ type Statement = {
87
99
  | 'conciliado'
88
100
  | 'estornado'
89
101
  | 'ajustado';
102
+ canEdit?: boolean;
103
+ canDelete?: boolean;
104
+ isTransfer?: boolean;
90
105
  };
91
106
 
107
+ type PaginatedStatementsResponse = {
108
+ data: Statement[];
109
+ total: number;
110
+ page: number;
111
+ pageSize: number;
112
+ prev: number | null;
113
+ next: number | null;
114
+ lastPage: number;
115
+ summary?: {
116
+ totalEntradas?: number;
117
+ totalSaidas?: number;
118
+ };
119
+ };
120
+
121
+ const statementEntrySchema = z.object({
122
+ date: z.string().trim().min(1, 'Data é obrigatória'),
123
+ description: z.string().trim().min(1, 'Descrição é obrigatória'),
124
+ type: z.enum(['entrada', 'saida']),
125
+ amount: z.number().min(0.01, 'Valor deve ser maior que zero'),
126
+ });
127
+
128
+ type StatementEntryFormValues = z.infer<typeof statementEntrySchema>;
129
+
92
130
  const bankAccountFormSchema = z.object({
93
131
  banco: z.string().trim().min(1, 'Banco é obrigatório'),
94
132
  agencia: z.string().optional(),
95
133
  conta: z.string().optional(),
96
134
  tipo: z.string().min(1, 'Tipo é obrigatório'),
97
135
  descricao: z.string().optional(),
136
+ dataInicial: z.string().optional(),
98
137
  saldoInicial: z.number().min(0, 'Saldo inicial inválido'),
99
138
  });
100
139
 
@@ -148,6 +187,7 @@ function NovaContaBancariaSheet({
148
187
  conta: '',
149
188
  tipo: '',
150
189
  descricao: '',
190
+ dataInicial: new Date().toISOString().slice(0, 10),
151
191
  saldoInicial: 0,
152
192
  },
153
193
  });
@@ -170,6 +210,9 @@ function NovaContaBancariaSheet({
170
210
  conta: watchedBankValues.conta ?? '',
171
211
  tipo: watchedBankValues.tipo ?? '',
172
212
  descricao: watchedBankValues.descricao ?? '',
213
+ dataInicial:
214
+ watchedBankValues.dataInicial ??
215
+ new Date().toISOString().slice(0, 10),
173
216
  saldoInicial: watchedBankValues.saldoInicial ?? 0,
174
217
  },
175
218
  },
@@ -179,6 +222,8 @@ function NovaContaBancariaSheet({
179
222
  (watchedBankValues.conta ?? '').trim() ||
180
223
  (watchedBankValues.tipo ?? '').trim() ||
181
224
  (watchedBankValues.descricao ?? '').trim() ||
225
+ (watchedBankValues.dataInicial ?? '').trim() !==
226
+ new Date().toISOString().slice(0, 10) ||
182
227
  (watchedBankValues.saldoInicial ?? 0) > 0
183
228
  ),
184
229
  enabled: open,
@@ -224,6 +269,7 @@ function NovaContaBancariaSheet({
224
269
  conta: '',
225
270
  tipo: '',
226
271
  descricao: '',
272
+ dataInicial: new Date().toISOString().slice(0, 10),
227
273
  saldoInicial: 0,
228
274
  }
229
275
  );
@@ -240,6 +286,7 @@ function NovaContaBancariaSheet({
240
286
  account: values.conta || undefined,
241
287
  type: values.tipo,
242
288
  description: values.descricao?.trim() || undefined,
289
+ start_date: values.dataInicial || undefined,
243
290
  initial_balance: values.saldoInicial,
244
291
  },
245
292
  });
@@ -370,6 +417,28 @@ function NovaContaBancariaSheet({
370
417
  )}
371
418
  />
372
419
 
420
+ <FormField
421
+ control={form.control}
422
+ name="dataInicial"
423
+ render={({ field }) => (
424
+ <FormItem>
425
+ <FormLabel>Data inicial</FormLabel>
426
+ <FormControl>
427
+ <Input
428
+ type="date"
429
+ {...field}
430
+ value={
431
+ field.value || new Date().toISOString().slice(0, 10)
432
+ }
433
+ />
434
+ </FormControl>
435
+ <FormMessage />
436
+ </FormItem>
437
+ )}
438
+ />
439
+ </div>
440
+
441
+ <div className="grid grid-cols-2 gap-4">
373
442
  <FormField
374
443
  control={form.control}
375
444
  name="saldoInicial"
@@ -390,25 +459,25 @@ function NovaContaBancariaSheet({
390
459
  </FormItem>
391
460
  )}
392
461
  />
393
- </div>
394
462
 
395
- <FormField
396
- control={form.control}
397
- name="descricao"
398
- render={({ field }) => (
399
- <FormItem>
400
- <FormLabel>{tBank('fields.description')}</FormLabel>
401
- <FormControl>
402
- <Input
403
- placeholder={tBank('fields.descriptionPlaceholder')}
404
- {...field}
405
- value={field.value || ''}
406
- />
407
- </FormControl>
408
- <FormMessage />
409
- </FormItem>
410
- )}
411
- />
463
+ <FormField
464
+ control={form.control}
465
+ name="descricao"
466
+ render={({ field }) => (
467
+ <FormItem>
468
+ <FormLabel>{tBank('fields.description')}</FormLabel>
469
+ <FormControl>
470
+ <Input
471
+ placeholder={tBank('fields.descriptionPlaceholder')}
472
+ {...field}
473
+ value={field.value || ''}
474
+ />
475
+ </FormControl>
476
+ <FormMessage />
477
+ </FormItem>
478
+ )}
479
+ />
480
+ </div>
412
481
 
413
482
  {draftStatusContent ? (
414
483
  <p className="text-xs text-muted-foreground">
@@ -681,6 +750,179 @@ function ImportarExtratoSheet({
681
750
  );
682
751
  }
683
752
 
753
+ function NovoLancamentoSheet({
754
+ open,
755
+ onOpenChange,
756
+ bankAccountId,
757
+ onCreated,
758
+ }: {
759
+ open: boolean;
760
+ onOpenChange: (open: boolean) => void;
761
+ bankAccountId?: string;
762
+ onCreated: () => Promise<void> | void;
763
+ }) {
764
+ const { request, showToastHandler } = useApp();
765
+ const form = useForm<StatementEntryFormValues>({
766
+ resolver: zodResolver(statementEntrySchema),
767
+ defaultValues: {
768
+ date: new Date().toISOString().slice(0, 10),
769
+ description: '',
770
+ type: 'entrada',
771
+ amount: 0,
772
+ },
773
+ });
774
+
775
+ useEffect(() => {
776
+ if (open) {
777
+ form.reset({
778
+ date: new Date().toISOString().slice(0, 10),
779
+ description: '',
780
+ type: 'entrada',
781
+ amount: 0,
782
+ });
783
+ }
784
+ }, [form, open]);
785
+
786
+ const handleSubmit = async (values: StatementEntryFormValues) => {
787
+ if (!bankAccountId) {
788
+ showToastHandler?.('error', 'Selecione uma conta bancária');
789
+ return;
790
+ }
791
+
792
+ try {
793
+ await request({
794
+ url: '/finance/statements',
795
+ method: 'POST',
796
+ data: {
797
+ bank_account_id: Number(bankAccountId),
798
+ date: values.date,
799
+ description: values.description.trim(),
800
+ type: values.type,
801
+ amount: values.amount,
802
+ },
803
+ });
804
+
805
+ await onCreated();
806
+ onOpenChange(false);
807
+ showToastHandler?.('success', 'Movimentação adicionada com sucesso');
808
+ } catch (error: any) {
809
+ const message = error?.response?.data?.message;
810
+ showToastHandler?.(
811
+ 'error',
812
+ typeof message === 'string' && message.trim()
813
+ ? message
814
+ : 'Não foi possível adicionar a movimentação'
815
+ );
816
+ }
817
+ };
818
+
819
+ return (
820
+ <Sheet open={open} onOpenChange={onOpenChange}>
821
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
822
+ <SheetHeader>
823
+ <SheetTitle>Novo lançamento</SheetTitle>
824
+ <SheetDescription>
825
+ Adicione uma movimentação manual ao extrato da conta bancária.
826
+ </SheetDescription>
827
+ </SheetHeader>
828
+
829
+ <Form {...form}>
830
+ <form
831
+ className="space-y-4 px-4"
832
+ onSubmit={form.handleSubmit(handleSubmit)}
833
+ >
834
+ <div className="grid grid-cols-2 gap-4">
835
+ <FormField
836
+ control={form.control}
837
+ name="date"
838
+ render={({ field }) => (
839
+ <FormItem>
840
+ <FormLabel>Data</FormLabel>
841
+ <FormControl>
842
+ <Input type="date" {...field} />
843
+ </FormControl>
844
+ <FormMessage />
845
+ </FormItem>
846
+ )}
847
+ />
848
+
849
+ <FormField
850
+ control={form.control}
851
+ name="type"
852
+ render={({ field }) => (
853
+ <FormItem>
854
+ <FormLabel>Tipo</FormLabel>
855
+ <Select value={field.value} onValueChange={field.onChange}>
856
+ <FormControl>
857
+ <SelectTrigger className="w-full">
858
+ <SelectValue />
859
+ </SelectTrigger>
860
+ </FormControl>
861
+ <SelectContent>
862
+ <SelectItem value="entrada">Entrada</SelectItem>
863
+ <SelectItem value="saida">Saída</SelectItem>
864
+ </SelectContent>
865
+ </Select>
866
+ <FormMessage />
867
+ </FormItem>
868
+ )}
869
+ />
870
+ </div>
871
+
872
+ <FormField
873
+ control={form.control}
874
+ name="description"
875
+ render={({ field }) => (
876
+ <FormItem>
877
+ <FormLabel>Descrição</FormLabel>
878
+ <FormControl>
879
+ <Input {...field} />
880
+ </FormControl>
881
+ <FormMessage />
882
+ </FormItem>
883
+ )}
884
+ />
885
+
886
+ <FormField
887
+ control={form.control}
888
+ name="amount"
889
+ render={({ field }) => (
890
+ <FormItem>
891
+ <FormLabel>Valor</FormLabel>
892
+ <FormControl>
893
+ <InputMoney
894
+ ref={field.ref}
895
+ name={field.name}
896
+ value={field.value}
897
+ onBlur={field.onBlur}
898
+ onValueChange={(value) => field.onChange(value ?? 0)}
899
+ placeholder="0,00"
900
+ />
901
+ </FormControl>
902
+ <FormMessage />
903
+ </FormItem>
904
+ )}
905
+ />
906
+
907
+ <div className="flex justify-end gap-2 pt-2">
908
+ <Button
909
+ type="button"
910
+ variant="outline"
911
+ onClick={() => onOpenChange(false)}
912
+ >
913
+ Cancelar
914
+ </Button>
915
+ <Button type="submit" disabled={form.formState.isSubmitting}>
916
+ Salvar
917
+ </Button>
918
+ </div>
919
+ </form>
920
+ </Form>
921
+ </SheetContent>
922
+ </Sheet>
923
+ );
924
+ }
925
+
684
926
  export default function ExtratosPage() {
685
927
  const t = useTranslations('finance.StatementsPage');
686
928
  const { request, showToastHandler } = useApp();
@@ -695,8 +937,29 @@ export default function ExtratosPage() {
695
937
  const [search, setSearch] = useState('');
696
938
  const [debouncedSearch, setDebouncedSearch] = useState('');
697
939
  const [isImportSheetOpen, setIsImportSheetOpen] = useState(false);
940
+ const [isNewEntrySheetOpen, setIsNewEntrySheetOpen] = useState(false);
698
941
  const [extratoSelecionado, setExtratoSelecionado] =
699
942
  useState<Statement | null>(null);
943
+ const [fromDate, setFromDate] = useState('');
944
+ const [toDate, setToDate] = useState('');
945
+ const [page, setPage] = useState(1);
946
+ // Inline editing state — keyed by statement id
947
+ const [editingValueId, setEditingValueId] = useState<string | null>(null);
948
+ const [editingValue, setEditingValue] = useState(0);
949
+ const [editingDescId, setEditingDescId] = useState<string | null>(null);
950
+ const [editingDesc, setEditingDesc] = useState('');
951
+ const [editingDateId, setEditingDateId] = useState<string | null>(null);
952
+ const [editingDate, setEditingDate] = useState('');
953
+ // Pending edits to save
954
+ const [pendingEdits, setPendingEdits] = useState<
955
+ Map<string, { amount?: number; description?: string; date?: string }>
956
+ >(new Map());
957
+ const [isSavingEdits, setIsSavingEdits] = useState(false);
958
+ const [statementToDelete, setStatementToDelete] = useState<Statement | null>(
959
+ null
960
+ );
961
+ const [isDeletingStatement, setIsDeletingStatement] = useState(false);
962
+ const pageSize = 10;
700
963
 
701
964
  useEffect(() => {
702
965
  const timeoutId = window.setTimeout(() => {
@@ -715,9 +978,20 @@ export default function ExtratosPage() {
715
978
  const response = await request({
716
979
  url: '/finance/bank-accounts',
717
980
  method: 'GET',
981
+ params: {
982
+ page: 1,
983
+ pageSize: 100,
984
+ },
718
985
  });
719
986
 
720
- return (response?.data || []) as BankAccount[];
987
+ const payload = response?.data as
988
+ | { data?: BankAccount[] }
989
+ | BankAccount[]
990
+ | undefined;
991
+
992
+ return (
993
+ Array.isArray(payload) ? payload : payload?.data || []
994
+ ) as BankAccount[];
721
995
  },
722
996
  });
723
997
 
@@ -769,44 +1043,98 @@ export default function ExtratosPage() {
769
1043
  router.replace(`${pathname}?${params.toString()}`);
770
1044
  }, [activeContaFilter, bankAccountIdFromUrl, pathname, router, searchParams]);
771
1045
 
772
- const { data: extratos = [], refetch: refetchExtratos } = useQuery<
773
- Statement[]
774
- >({
775
- queryKey: ['finance-statements', activeContaFilter, debouncedSearch],
776
- queryFn: async () => {
777
- if (!activeContaFilter) {
778
- return [];
779
- }
1046
+ const { data: extratosResponse, refetch: refetchExtratos } =
1047
+ useQuery<PaginatedStatementsResponse>({
1048
+ queryKey: [
1049
+ 'finance-statements',
1050
+ activeContaFilter,
1051
+ debouncedSearch,
1052
+ fromDate,
1053
+ toDate,
1054
+ page,
1055
+ pageSize,
1056
+ ],
1057
+ queryFn: async () => {
1058
+ if (!activeContaFilter) {
1059
+ return {
1060
+ data: [],
1061
+ total: 0,
1062
+ page,
1063
+ pageSize,
1064
+ prev: null,
1065
+ next: null,
1066
+ lastPage: 1,
1067
+ summary: {
1068
+ totalEntradas: 0,
1069
+ totalSaidas: 0,
1070
+ },
1071
+ } as PaginatedStatementsResponse;
1072
+ }
780
1073
 
781
- const params = new URLSearchParams();
782
- params.set('bank_account_id', activeContaFilter);
1074
+ const params: Record<string, string | number> = {
1075
+ bank_account_id: activeContaFilter,
1076
+ page,
1077
+ pageSize,
1078
+ };
783
1079
 
784
- const trimmedSearch = debouncedSearch.trim();
785
- if (trimmedSearch) {
786
- params.set('search', trimmedSearch);
787
- }
1080
+ const trimmedSearch = debouncedSearch.trim();
1081
+ if (trimmedSearch) {
1082
+ params.search = trimmedSearch;
1083
+ }
788
1084
 
789
- const response = await request({
790
- url: `/finance/statements?${params.toString()}`,
791
- method: 'GET',
792
- });
1085
+ if (fromDate) {
1086
+ params.from = fromDate;
1087
+ }
793
1088
 
794
- return (response?.data || []) as Statement[];
795
- },
796
- });
1089
+ if (toDate) {
1090
+ params.to = toDate;
1091
+ }
1092
+
1093
+ const response = await request({
1094
+ url: '/finance/statements',
1095
+ method: 'GET',
1096
+ params,
1097
+ });
797
1098
 
1099
+ return (response?.data || {
1100
+ data: [],
1101
+ total: 0,
1102
+ page,
1103
+ pageSize,
1104
+ prev: null,
1105
+ next: null,
1106
+ lastPage: 1,
1107
+ summary: {
1108
+ totalEntradas: 0,
1109
+ totalSaidas: 0,
1110
+ },
1111
+ }) as PaginatedStatementsResponse;
1112
+ },
1113
+ placeholderData: (old) => old,
1114
+ });
1115
+
1116
+ useEffect(() => {
1117
+ setPage(1);
1118
+ }, [activeContaFilter, debouncedSearch, fromDate, toDate]);
1119
+
1120
+ const extratos = extratosResponse?.data ?? [];
1121
+ const totalExtratos = extratosResponse?.total ?? 0;
798
1122
  const conta = contasBancarias.find((item) => item.id === activeContaFilter);
799
1123
  const contaExtratoSelecionado = extratoSelecionado
800
1124
  ? contasBancarias.find(
801
1125
  (item) => item.id === extratoSelecionado.contaBancariaId
802
1126
  )
803
1127
  : undefined;
804
- const totalEntradas = extratos
805
- .filter((e) => e.tipo === 'entrada')
806
- .reduce((acc, e) => acc + e.valor, 0);
807
- const totalSaidas = extratos
808
- .filter((e) => e.tipo === 'saida')
809
- .reduce((acc, e) => acc + e.valor, 0);
1128
+ const totalEntradas =
1129
+ extratosResponse?.summary?.totalEntradas ??
1130
+ extratos
1131
+ .filter((statement) => statement.tipo === 'entrada')
1132
+ .reduce((acc, statement) => acc + statement.valor, 0);
1133
+ const totalSaidas =
1134
+ extratosResponse?.summary?.totalSaidas ??
1135
+ extratos
1136
+ .filter((statement) => statement.tipo === 'saida')
1137
+ .reduce((acc, statement) => acc + statement.valor, 0);
810
1138
  const summaryCards = [
811
1139
  {
812
1140
  key: 'inflows',
@@ -836,7 +1164,6 @@ export default function ExtratosPage() {
836
1164
  layout: 'compact' as const,
837
1165
  },
838
1166
  ];
839
-
840
1167
  const handleExport = async () => {
841
1168
  if (!activeContaFilter) {
842
1169
  showToastHandler?.('error', 'Selecione uma conta bancária para exportar');
@@ -852,6 +1179,14 @@ export default function ExtratosPage() {
852
1179
  params.set('search', trimmedSearch);
853
1180
  }
854
1181
 
1182
+ if (fromDate) {
1183
+ params.set('from', fromDate);
1184
+ }
1185
+
1186
+ if (toDate) {
1187
+ params.set('to', toDate);
1188
+ }
1189
+
855
1190
  const response = await request<Blob>({
856
1191
  url: `/finance/statements/export?${params.toString()}`,
857
1192
  method: 'GET',
@@ -886,6 +1221,136 @@ export default function ExtratosPage() {
886
1221
  }
887
1222
  };
888
1223
 
1224
+ const handleStartInlineEdit = (extrato: Statement) => {
1225
+ if (!extrato.canEdit) {
1226
+ return;
1227
+ }
1228
+
1229
+ setEditingValueId(extrato.id);
1230
+ setEditingValue(Math.abs(Number(extrato.valor || 0)));
1231
+ };
1232
+
1233
+ const handleChangeInlineValue = (extrato: Statement, value: number) => {
1234
+ setEditingValue(value);
1235
+ setPendingEdits((prev) => {
1236
+ const next = new Map(prev);
1237
+ const existing = next.get(extrato.id) ?? {};
1238
+ next.set(extrato.id, { ...existing, amount: value });
1239
+ return next;
1240
+ });
1241
+ };
1242
+
1243
+ const handleStartInlineEditDesc = (extrato: Statement) => {
1244
+ if (!extrato.canEdit) return;
1245
+ setEditingDescId(extrato.id);
1246
+ setEditingDesc(extrato.descricao);
1247
+ };
1248
+
1249
+ const handleChangeInlineDesc = (extrato: Statement, value: string) => {
1250
+ setEditingDesc(value);
1251
+ setPendingEdits((prev) => {
1252
+ const next = new Map(prev);
1253
+ const existing = next.get(extrato.id) ?? {};
1254
+ next.set(extrato.id, { ...existing, description: value });
1255
+ return next;
1256
+ });
1257
+ };
1258
+
1259
+ const handleStartInlineEditDate = (extrato: Statement) => {
1260
+ if (!extrato.canEdit) return;
1261
+ setEditingDateId(extrato.id);
1262
+ setEditingDate(extrato.data.slice(0, 10));
1263
+ };
1264
+
1265
+ const handleChangeInlineDate = (extrato: Statement, value: string) => {
1266
+ setEditingDate(value);
1267
+ setPendingEdits((prev) => {
1268
+ const next = new Map(prev);
1269
+ const existing = next.get(extrato.id) ?? {};
1270
+ next.set(extrato.id, { ...existing, date: value });
1271
+ return next;
1272
+ });
1273
+ };
1274
+
1275
+ const handleCancelPendingEdits = () => {
1276
+ setPendingEdits(new Map());
1277
+ setEditingValueId(null);
1278
+ setEditingDescId(null);
1279
+ setEditingDateId(null);
1280
+ };
1281
+
1282
+ const handleSaveAllPendingEdits = async () => {
1283
+ if (pendingEdits.size === 0) return;
1284
+ setIsSavingEdits(true);
1285
+ let hasError = false;
1286
+ try {
1287
+ for (const [id, edits] of pendingEdits.entries()) {
1288
+ await request({
1289
+ url: `/finance/statements/${id}`,
1290
+ method: 'PATCH',
1291
+ data: edits,
1292
+ });
1293
+ }
1294
+ } catch (error: any) {
1295
+ hasError = true;
1296
+ const message = error?.response?.data?.message;
1297
+ showToastHandler?.(
1298
+ 'error',
1299
+ typeof message === 'string' && message.trim()
1300
+ ? message
1301
+ : 'Não foi possível salvar as alterações'
1302
+ );
1303
+ } finally {
1304
+ setIsSavingEdits(false);
1305
+ }
1306
+
1307
+ if (!hasError) {
1308
+ setPendingEdits(new Map());
1309
+ setEditingValueId(null);
1310
+ setEditingDescId(null);
1311
+ setEditingDateId(null);
1312
+ await Promise.all([refetchExtratos(), refetchContasBancarias()]);
1313
+ showToastHandler?.('success', 'Alterações salvas com sucesso');
1314
+ }
1315
+ };
1316
+
1317
+ const handleDeleteStatement = async (extrato: Statement) => {
1318
+ if (!extrato.canDelete) {
1319
+ return;
1320
+ }
1321
+
1322
+ setStatementToDelete(extrato);
1323
+ };
1324
+
1325
+ const confirmDeleteStatement = async () => {
1326
+ if (!statementToDelete) return;
1327
+ setIsDeletingStatement(true);
1328
+ try {
1329
+ await request({
1330
+ url: `/finance/statements/${statementToDelete.id}`,
1331
+ method: 'DELETE',
1332
+ });
1333
+
1334
+ if (extratoSelecionado?.id === statementToDelete.id) {
1335
+ setExtratoSelecionado(null);
1336
+ }
1337
+
1338
+ setStatementToDelete(null);
1339
+ await Promise.all([refetchExtratos(), refetchContasBancarias()]);
1340
+ showToastHandler?.('success', 'Movimentação excluída com sucesso');
1341
+ } catch (error: any) {
1342
+ const message = error?.response?.data?.message;
1343
+ showToastHandler?.(
1344
+ 'error',
1345
+ typeof message === 'string' && message.trim()
1346
+ ? message
1347
+ : 'Não foi possível excluir a movimentação'
1348
+ );
1349
+ } finally {
1350
+ setIsDeletingStatement(false);
1351
+ }
1352
+ };
1353
+
889
1354
  return (
890
1355
  <Page>
891
1356
  <PageHeader
@@ -902,6 +1367,10 @@ export default function ExtratosPage() {
902
1367
  <Download className="mr-2 h-4 w-4" />
903
1368
  {t('actions.export')}
904
1369
  </Button>
1370
+ <Button onClick={() => setIsNewEntrySheetOpen(true)}>
1371
+ <Plus className="mr-2 h-4 w-4" />
1372
+ Novo lançamento
1373
+ </Button>
905
1374
  <ImportarExtratoSheet
906
1375
  contasBancarias={contasBancarias}
907
1376
  t={t}
@@ -915,6 +1384,15 @@ export default function ExtratosPage() {
915
1384
  }
916
1385
  />
917
1386
 
1387
+ <NovoLancamentoSheet
1388
+ open={isNewEntrySheetOpen}
1389
+ onOpenChange={setIsNewEntrySheetOpen}
1390
+ bankAccountId={activeContaFilter}
1391
+ onCreated={async () => {
1392
+ await Promise.all([refetchExtratos(), refetchContasBancarias()]);
1393
+ }}
1394
+ />
1395
+
918
1396
  <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
919
1397
  <Select
920
1398
  value={activeContaFilter}
@@ -944,6 +1422,13 @@ export default function ExtratosPage() {
944
1422
  onSearchChange={setSearch}
945
1423
  />
946
1424
  </div>
1425
+ <DateRangePicker
1426
+ fromDate={fromDate}
1427
+ toDate={toDate}
1428
+ onFromDateChange={setFromDate}
1429
+ onToDateChange={setToDate}
1430
+ defaultPreset="thisMonth"
1431
+ />
947
1432
  </div>
948
1433
 
949
1434
  <KpiCardsGrid items={summaryCards} columns={3} />
@@ -951,10 +1436,10 @@ export default function ExtratosPage() {
951
1436
  <FinancePageSection
952
1437
  variant="flat"
953
1438
  title={t('table.title')}
954
- description={t('table.foundTransactions', { count: extratos.length })}
1439
+ description={t('table.foundTransactions', { count: totalExtratos })}
955
1440
  contentClassName="p-4 sm:p-5"
956
1441
  >
957
- {extratos.length > 0 ? (
1442
+ {totalExtratos > 0 ? (
958
1443
  <div className="overflow-x-auto">
959
1444
  <Table className="min-w-190 table-fixed">
960
1445
  <TableHeader>
@@ -972,6 +1457,7 @@ export default function ExtratosPage() {
972
1457
  <TableHead className="w-35">
973
1458
  {t('table.headers.reconciliation')}
974
1459
  </TableHead>
1460
+ <TableHead className="w-24 text-right">Ações</TableHead>
975
1461
  </TableRow>
976
1462
  </TableHeader>
977
1463
  <TableBody>
@@ -989,20 +1475,123 @@ export default function ExtratosPage() {
989
1475
  role="button"
990
1476
  tabIndex={0}
991
1477
  >
992
- <TableCell>{formatarData(extrato.data)}</TableCell>
1478
+ <TableCell>
1479
+ {editingDateId === extrato.id ? (
1480
+ <Input
1481
+ type="date"
1482
+ className="h-7 px-1 text-sm"
1483
+ value={editingDate}
1484
+ onChange={(e) =>
1485
+ handleChangeInlineDate(extrato, e.target.value)
1486
+ }
1487
+ onKeyDown={(e) => {
1488
+ e.stopPropagation();
1489
+ if (e.key === 'Escape') setEditingDateId(null);
1490
+ if (e.key === 'Enter') {
1491
+ e.preventDefault();
1492
+ setEditingDateId(null);
1493
+ handleSaveAllPendingEdits();
1494
+ }
1495
+ }}
1496
+ onClick={(e) => e.stopPropagation()}
1497
+ autoFocus
1498
+ />
1499
+ ) : (
1500
+ <button
1501
+ type="button"
1502
+ className={`inline-flex items-center gap-1 ${extrato.canEdit ? 'cursor-pointer underline-offset-2 hover:underline' : 'cursor-default'}`}
1503
+ onClick={(e) => {
1504
+ e.stopPropagation();
1505
+ handleStartInlineEditDate(extrato);
1506
+ }}
1507
+ >
1508
+ {extrato.canEdit ? (
1509
+ <Pencil className="h-3 w-3" />
1510
+ ) : null}
1511
+ {formatarData(extrato.data)}
1512
+ </button>
1513
+ )}
1514
+ </TableCell>
993
1515
  <TableCell className="truncate" title={extrato.descricao}>
994
- {extrato.descricao}
1516
+ {editingDescId === extrato.id ? (
1517
+ <Input
1518
+ className="h-7 px-1 text-sm"
1519
+ value={editingDesc}
1520
+ onChange={(e) =>
1521
+ handleChangeInlineDesc(extrato, e.target.value)
1522
+ }
1523
+ onKeyDown={(e) => {
1524
+ e.stopPropagation();
1525
+ if (e.key === 'Escape') setEditingDescId(null);
1526
+ if (e.key === 'Enter') {
1527
+ e.preventDefault();
1528
+ setEditingDescId(null);
1529
+ handleSaveAllPendingEdits();
1530
+ }
1531
+ }}
1532
+ onClick={(e) => e.stopPropagation()}
1533
+ autoFocus
1534
+ />
1535
+ ) : (
1536
+ <button
1537
+ type="button"
1538
+ className={`inline-flex max-w-full items-center gap-1 truncate ${extrato.canEdit ? 'cursor-pointer underline-offset-2 hover:underline' : 'cursor-default'}`}
1539
+ onClick={(e) => {
1540
+ e.stopPropagation();
1541
+ handleStartInlineEditDesc(extrato);
1542
+ }}
1543
+ title={extrato.descricao}
1544
+ >
1545
+ {extrato.canEdit ? (
1546
+ <Pencil className="h-3 w-3 shrink-0" />
1547
+ ) : null}
1548
+ <span className="truncate">{extrato.descricao}</span>
1549
+ </button>
1550
+ )}
995
1551
  </TableCell>
996
1552
  <TableCell className="text-right">
997
- <span
998
- className={
999
- extrato.tipo === 'entrada'
1000
- ? 'text-green-600'
1001
- : 'text-red-600'
1002
- }
1003
- >
1004
- <Money value={extrato.valor} />
1005
- </span>
1553
+ {editingValueId === extrato.id ? (
1554
+ <InputMoney
1555
+ value={editingValue}
1556
+ onValueChange={(value) =>
1557
+ handleChangeInlineValue(extrato, Number(value || 0))
1558
+ }
1559
+ onKeyDown={(event) => {
1560
+ event.stopPropagation();
1561
+ if (event.key === 'Escape') {
1562
+ setEditingValueId(null);
1563
+ }
1564
+ if (event.key === 'Enter') {
1565
+ event.preventDefault();
1566
+ setEditingValueId(null);
1567
+ handleSaveAllPendingEdits();
1568
+ }
1569
+ }}
1570
+ autoFocus
1571
+ />
1572
+ ) : (
1573
+ <button
1574
+ type="button"
1575
+ className={`inline-flex items-center gap-1 ${
1576
+ extrato.canEdit
1577
+ ? 'cursor-pointer underline-offset-2 hover:underline'
1578
+ : 'cursor-default'
1579
+ } ${
1580
+ extrato.tipo === 'entrada'
1581
+ ? 'text-green-600'
1582
+ : 'text-red-600'
1583
+ }`}
1584
+ onClick={(event) => {
1585
+ event.stopPropagation();
1586
+ handleStartInlineEdit(extrato);
1587
+ }}
1588
+ >
1589
+ {extrato.canEdit ? (
1590
+ <Pencil className="h-3 w-3" />
1591
+ ) : null}
1592
+ <Money value={extrato.valor} />
1593
+ </button>
1594
+ )}
1006
1595
  </TableCell>
1007
1596
  <TableCell>
1008
1597
  {extrato.tipo === 'entrada' ? (
@@ -1023,6 +1612,21 @@ export default function ExtratosPage() {
1023
1612
  type="conciliacao"
1024
1613
  />
1025
1614
  </TableCell>
1615
+ <TableCell className="text-right">
1616
+ {extrato.canDelete ? (
1617
+ <Button
1618
+ type="button"
1619
+ variant="ghost"
1620
+ size="icon"
1621
+ onClick={(event) => {
1622
+ event.stopPropagation();
1623
+ void handleDeleteStatement(extrato);
1624
+ }}
1625
+ >
1626
+ <Trash2 className="h-4 w-4" />
1627
+ </Button>
1628
+ ) : null}
1629
+ </TableCell>
1026
1630
  </TableRow>
1027
1631
  ))}
1028
1632
  </TableBody>
@@ -1037,6 +1641,18 @@ export default function ExtratosPage() {
1037
1641
  onAction={() => setIsImportSheetOpen(true)}
1038
1642
  />
1039
1643
  )}
1644
+ {totalExtratos > 0 ? (
1645
+ <div className="border-t border-border/50 px-4 py-4 sm:px-6">
1646
+ <PaginationFooter
1647
+ currentPage={page}
1648
+ pageSize={pageSize}
1649
+ totalItems={totalExtratos}
1650
+ onPageChange={setPage}
1651
+ onPageSizeChange={() => undefined}
1652
+ pageSizeOptions={[10]}
1653
+ />
1654
+ </div>
1655
+ ) : null}
1040
1656
  </FinancePageSection>
1041
1657
 
1042
1658
  <Dialog
@@ -1139,6 +1755,61 @@ export default function ExtratosPage() {
1139
1755
  </DialogFooter>
1140
1756
  </DialogContent>
1141
1757
  </Dialog>
1758
+
1759
+ {pendingEdits.size > 0 ? (
1760
+ <div className="fixed bottom-6 right-6 z-50 flex items-center gap-2 rounded-lg bg-white p-3 shadow-lg ring-1 ring-black/10 dark:bg-zinc-900 dark:ring-white/10">
1761
+ <Button
1762
+ type="button"
1763
+ variant="outline"
1764
+ size="sm"
1765
+ onClick={handleCancelPendingEdits}
1766
+ disabled={isSavingEdits}
1767
+ >
1768
+ Cancelar
1769
+ </Button>
1770
+ <Button
1771
+ type="button"
1772
+ size="sm"
1773
+ onClick={() => void handleSaveAllPendingEdits()}
1774
+ disabled={isSavingEdits}
1775
+ >
1776
+ {isSavingEdits ? 'Salvando...' : 'Salvar alterações'}
1777
+ </Button>
1778
+ </div>
1779
+ ) : null}
1780
+
1781
+ <Dialog
1782
+ open={statementToDelete !== null}
1783
+ onOpenChange={(open) => {
1784
+ if (!open && !isDeletingStatement) setStatementToDelete(null);
1785
+ }}
1786
+ >
1787
+ <DialogContent className="sm:max-w-sm">
1788
+ <DialogHeader>
1789
+ <DialogTitle>Excluir movimentação</DialogTitle>
1790
+ <DialogDescription>
1791
+ Esta ação não pode ser desfeita. A movimentação será removida
1792
+ permanentemente do extrato.
1793
+ </DialogDescription>
1794
+ </DialogHeader>
1795
+ <DialogFooter className="mt-2">
1796
+ <Button
1797
+ variant="outline"
1798
+ onClick={() => setStatementToDelete(null)}
1799
+ disabled={isDeletingStatement}
1800
+ >
1801
+ Cancelar
1802
+ </Button>
1803
+ <Button
1804
+ variant="destructive"
1805
+ onClick={() => void confirmDeleteStatement()}
1806
+ disabled={isDeletingStatement}
1807
+ >
1808
+ {isDeletingStatement ? 'Excluindo...' : 'Excluir'}
1809
+ </Button>
1810
+ </DialogFooter>
1811
+ </DialogContent>
1812
+ </Dialog>
1142
1813
  </Page>
1143
1814
  );
1144
1815
  }