@hed-hog/finance 0.0.275 → 0.0.276

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 (40) hide show
  1. package/README.md +36 -22
  2. package/dist/dto/create-bank-reconciliation.dto.d.ts +8 -0
  3. package/dist/dto/create-bank-reconciliation.dto.d.ts.map +1 -0
  4. package/dist/dto/create-bank-reconciliation.dto.js +43 -0
  5. package/dist/dto/create-bank-reconciliation.dto.js.map +1 -0
  6. package/dist/finance-data.controller.d.ts +2 -0
  7. package/dist/finance-data.controller.d.ts.map +1 -1
  8. package/dist/finance-statements.controller.d.ts +42 -0
  9. package/dist/finance-statements.controller.d.ts.map +1 -1
  10. package/dist/finance-statements.controller.js +13 -0
  11. package/dist/finance-statements.controller.js.map +1 -1
  12. package/dist/finance.service.d.ts +44 -0
  13. package/dist/finance.service.d.ts.map +1 -1
  14. package/dist/finance.service.js +98 -9
  15. package/dist/finance.service.js.map +1 -1
  16. package/hedhog/data/route.yaml +9 -0
  17. package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +126 -126
  18. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +373 -373
  19. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +1270 -1270
  20. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +982 -982
  21. package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +686 -686
  22. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +152 -32
  23. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +986 -986
  24. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +492 -492
  25. package/hedhog/frontend/app/page.tsx.ejs +372 -372
  26. package/hedhog/frontend/app/planning/cash-flow-forecast/page.tsx.ejs +329 -329
  27. package/hedhog/frontend/app/planning/receivables-calendar/page.tsx.ejs +227 -227
  28. package/hedhog/frontend/app/planning/scenarios/page.tsx.ejs +408 -408
  29. package/hedhog/frontend/messages/en.json +15 -5
  30. package/hedhog/frontend/messages/pt.json +15 -5
  31. package/package.json +6 -6
  32. package/src/dto/create-bank-reconciliation.dto.ts +24 -0
  33. package/src/finance-statements.controller.ts +14 -0
  34. package/src/finance.module.ts +43 -43
  35. package/src/finance.service.ts +118 -0
  36. package/src/index.ts +14 -14
  37. package/dist/finance.controller.d.ts +0 -276
  38. package/dist/finance.controller.d.ts.map +0 -1
  39. package/dist/finance.controller.js +0 -110
  40. package/dist/finance.controller.js.map +0 -1
@@ -1,492 +1,492 @@
1
- 'use client';
2
-
3
- import { Page, PageHeader } from '@/components/entity-list';
4
- import { Button } from '@/components/ui/button';
5
- import {
6
- Card,
7
- CardContent,
8
- CardDescription,
9
- CardHeader,
10
- CardTitle,
11
- } from '@/components/ui/card';
12
- import { FilterBar } from '@/components/ui/filter-bar';
13
- import {
14
- Form,
15
- FormControl,
16
- FormField,
17
- FormItem,
18
- FormLabel,
19
- FormMessage,
20
- } from '@/components/ui/form';
21
- import { Input } from '@/components/ui/input';
22
- import { InputMoney } from '@/components/ui/input-money';
23
- import { Money } from '@/components/ui/money';
24
- import {
25
- Select,
26
- SelectContent,
27
- SelectItem,
28
- SelectTrigger,
29
- SelectValue,
30
- } from '@/components/ui/select';
31
- import {
32
- Sheet,
33
- SheetContent,
34
- SheetDescription,
35
- SheetHeader,
36
- SheetTitle,
37
- } from '@/components/ui/sheet';
38
- import {
39
- Table,
40
- TableBody,
41
- TableCell,
42
- TableHead,
43
- TableHeader,
44
- TableRow,
45
- } from '@/components/ui/table';
46
- import { Textarea } from '@/components/ui/textarea';
47
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
48
- import { zodResolver } from '@hookform/resolvers/zod';
49
- import { ArrowRight, Plus } from 'lucide-react';
50
- import { useTranslations } from 'next-intl';
51
- import { useEffect, useState } from 'react';
52
- import { useForm } from 'react-hook-form';
53
- import { z } from 'zod';
54
- import { formatarData } from '../../_lib/formatters';
55
-
56
- type BankAccount = {
57
- id: string;
58
- banco: string;
59
- descricao: string;
60
- ativo: boolean;
61
- };
62
-
63
- type Transfer = {
64
- id: string;
65
- contaOrigemId: string;
66
- contaDestinoId: string;
67
- data: string;
68
- valor: number;
69
- descricao: string;
70
- };
71
-
72
- const transferFormSchema = z
73
- .object({
74
- sourceAccountId: z.string().trim().min(1, 'Conta de origem é obrigatória'),
75
- destinationAccountId: z
76
- .string()
77
- .trim()
78
- .min(1, 'Conta de destino é obrigatória'),
79
- date: z.string().trim().min(1, 'Data é obrigatória'),
80
- amount: z.number().min(0.01, 'Valor deve ser maior que zero'),
81
- description: z.string().optional(),
82
- })
83
- .refine((values) => values.sourceAccountId !== values.destinationAccountId, {
84
- message: 'As contas de origem e destino devem ser diferentes',
85
- path: ['destinationAccountId'],
86
- });
87
-
88
- type TransferFormValues = z.infer<typeof transferFormSchema>;
89
-
90
- function NovaTransferenciaSheet({
91
- contasBancarias,
92
- t,
93
- onCreated,
94
- }: {
95
- contasBancarias: BankAccount[];
96
- t: ReturnType<typeof useTranslations>;
97
- onCreated: () => Promise<void> | void;
98
- }) {
99
- const { request, showToastHandler } = useApp();
100
- const [open, setOpen] = useState(false);
101
-
102
- const form = useForm<TransferFormValues>({
103
- resolver: zodResolver(transferFormSchema),
104
- defaultValues: {
105
- sourceAccountId: '',
106
- destinationAccountId: '',
107
- date: new Date().toISOString().slice(0, 10),
108
- amount: 0,
109
- description: '',
110
- },
111
- });
112
-
113
- useEffect(() => {
114
- if (!open) {
115
- return;
116
- }
117
-
118
- form.reset({
119
- sourceAccountId: '',
120
- destinationAccountId: '',
121
- date: new Date().toISOString().slice(0, 10),
122
- amount: 0,
123
- description: '',
124
- });
125
- }, [form, open]);
126
-
127
- const handleSubmit = async (values: TransferFormValues) => {
128
- try {
129
- await request({
130
- url: '/finance/transfers',
131
- method: 'POST',
132
- data: {
133
- source_account_id: Number(values.sourceAccountId),
134
- destination_account_id: Number(values.destinationAccountId),
135
- date: values.date,
136
- amount: values.amount,
137
- description: values.description?.trim() || undefined,
138
- },
139
- });
140
-
141
- await onCreated();
142
- setOpen(false);
143
- showToastHandler?.('success', 'Transferência cadastrada com sucesso');
144
- } catch {
145
- showToastHandler?.('error', 'Erro ao cadastrar transferência');
146
- }
147
- };
148
-
149
- return (
150
- <Sheet open={open} onOpenChange={setOpen}>
151
- <Button onClick={() => setOpen(true)}>
152
- <Plus className="mr-2 h-4 w-4" />
153
- {t('newTransfer.action')}
154
- </Button>
155
- <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
156
- <SheetHeader>
157
- <SheetTitle>{t('newTransfer.title')}</SheetTitle>
158
- <SheetDescription>{t('newTransfer.description')}</SheetDescription>
159
- </SheetHeader>
160
-
161
- <Form {...form}>
162
- <form
163
- className="space-y-4 px-4"
164
- onSubmit={form.handleSubmit(handleSubmit)}
165
- >
166
- <FormField
167
- control={form.control}
168
- name="sourceAccountId"
169
- render={({ field }) => (
170
- <FormItem>
171
- <FormLabel>{t('fields.sourceAccount')}</FormLabel>
172
- <Select value={field.value} onValueChange={field.onChange}>
173
- <FormControl>
174
- <SelectTrigger>
175
- <SelectValue placeholder={t('common.select')} />
176
- </SelectTrigger>
177
- </FormControl>
178
- <SelectContent>
179
- {contasBancarias.map((conta) => (
180
- <SelectItem key={conta.id} value={conta.id}>
181
- {conta.banco} - {conta.descricao}
182
- </SelectItem>
183
- ))}
184
- </SelectContent>
185
- </Select>
186
- <FormMessage />
187
- </FormItem>
188
- )}
189
- />
190
-
191
- <FormField
192
- control={form.control}
193
- name="destinationAccountId"
194
- render={({ field }) => (
195
- <FormItem>
196
- <FormLabel>{t('fields.destinationAccount')}</FormLabel>
197
- <Select value={field.value} onValueChange={field.onChange}>
198
- <FormControl>
199
- <SelectTrigger>
200
- <SelectValue placeholder={t('common.select')} />
201
- </SelectTrigger>
202
- </FormControl>
203
- <SelectContent>
204
- {contasBancarias.map((conta) => (
205
- <SelectItem key={conta.id} value={conta.id}>
206
- {conta.banco} - {conta.descricao}
207
- </SelectItem>
208
- ))}
209
- </SelectContent>
210
- </Select>
211
- <FormMessage />
212
- </FormItem>
213
- )}
214
- />
215
-
216
- <div className="grid grid-cols-2 gap-4">
217
- <FormField
218
- control={form.control}
219
- name="date"
220
- render={({ field }) => (
221
- <FormItem>
222
- <FormLabel>{t('fields.date')}</FormLabel>
223
- <FormControl>
224
- <Input type="date" {...field} />
225
- </FormControl>
226
- <FormMessage />
227
- </FormItem>
228
- )}
229
- />
230
-
231
- <FormField
232
- control={form.control}
233
- name="amount"
234
- render={({ field }) => (
235
- <FormItem>
236
- <FormLabel>{t('fields.value')}</FormLabel>
237
- <FormControl>
238
- <InputMoney
239
- ref={field.ref}
240
- name={field.name}
241
- value={field.value}
242
- onBlur={field.onBlur}
243
- onValueChange={(value) => field.onChange(value ?? 0)}
244
- placeholder="0,00"
245
- />
246
- </FormControl>
247
- <FormMessage />
248
- </FormItem>
249
- )}
250
- />
251
- </div>
252
-
253
- <FormField
254
- control={form.control}
255
- name="description"
256
- render={({ field }) => (
257
- <FormItem>
258
- <FormLabel>{t('fields.description')}</FormLabel>
259
- <FormControl>
260
- <Textarea
261
- placeholder={t('fields.descriptionPlaceholder')}
262
- {...field}
263
- value={field.value || ''}
264
- />
265
- </FormControl>
266
- <FormMessage />
267
- </FormItem>
268
- )}
269
- />
270
-
271
- <div className="flex justify-end gap-2 pt-4">
272
- <Button
273
- type="button"
274
- variant="outline"
275
- onClick={() => setOpen(false)}
276
- >
277
- {t('common.cancel')}
278
- </Button>
279
- <Button type="submit" disabled={form.formState.isSubmitting}>
280
- {t('newTransfer.submit')}
281
- </Button>
282
- </div>
283
- </form>
284
- </Form>
285
- </SheetContent>
286
- </Sheet>
287
- );
288
- }
289
-
290
- export default function TransferenciasPage() {
291
- const t = useTranslations('finance.TransfersPage');
292
- const { request } = useApp();
293
- const [search, setSearch] = useState('');
294
- const [debouncedSearch, setDebouncedSearch] = useState('');
295
- const [accountFilter, setAccountFilter] = useState('all');
296
-
297
- useEffect(() => {
298
- const timeoutId = window.setTimeout(() => {
299
- setDebouncedSearch(search);
300
- }, 300);
301
-
302
- return () => {
303
- window.clearTimeout(timeoutId);
304
- };
305
- }, [search]);
306
-
307
- const { data: contasBancarias = [], refetch: refetchContasBancarias } =
308
- useQuery<BankAccount[]>({
309
- queryKey: ['finance-bank-accounts'],
310
- queryFn: async () => {
311
- const response = await request({
312
- url: '/finance/bank-accounts',
313
- method: 'GET',
314
- });
315
-
316
- return (response?.data || []) as BankAccount[];
317
- },
318
- });
319
-
320
- const { data: transferencias = [], refetch: refetchTransferencias } =
321
- useQuery<Transfer[]>({
322
- queryKey: ['finance-transfers', debouncedSearch, accountFilter],
323
- queryFn: async () => {
324
- const params = new URLSearchParams();
325
-
326
- const trimmedSearch = debouncedSearch.trim();
327
- if (trimmedSearch) {
328
- params.set('search', trimmedSearch);
329
- }
330
-
331
- if (accountFilter !== 'all') {
332
- params.set('bank_account_id', accountFilter);
333
- }
334
-
335
- const query = params.toString();
336
- const response = await request({
337
- url: query ? `/finance/transfers?${query}` : '/finance/transfers',
338
- method: 'GET',
339
- });
340
-
341
- return (response?.data || []) as Transfer[];
342
- },
343
- });
344
-
345
- const handleTransferCreated = async () => {
346
- await Promise.all([refetchTransferencias(), refetchContasBancarias()]);
347
- };
348
-
349
- const totalTransferido = transferencias.reduce((acc, t) => acc + t.valor, 0);
350
-
351
- return (
352
- <Page>
353
- <PageHeader
354
- title={t('header.title')}
355
- description={t('header.description')}
356
- breadcrumbs={[
357
- { label: t('breadcrumbs.home'), href: '/' },
358
- { label: t('breadcrumbs.finance'), href: '/finance' },
359
- { label: t('breadcrumbs.current') },
360
- ]}
361
- actions={
362
- <NovaTransferenciaSheet
363
- contasBancarias={contasBancarias}
364
- t={t}
365
- onCreated={handleTransferCreated}
366
- />
367
- }
368
- />
369
-
370
- <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
371
- <Select value={accountFilter} onValueChange={setAccountFilter}>
372
- <SelectTrigger className="w-full sm:w-[280px]">
373
- <SelectValue placeholder={t('common.select')} />
374
- </SelectTrigger>
375
- <SelectContent>
376
- <SelectItem value="all">Todas as contas</SelectItem>
377
- {contasBancarias.map((conta) => (
378
- <SelectItem key={conta.id} value={conta.id}>
379
- {conta.banco} - {conta.descricao}
380
- </SelectItem>
381
- ))}
382
- </SelectContent>
383
- </Select>
384
-
385
- <div className="flex-1">
386
- <FilterBar
387
- searchPlaceholder="Buscar na descrição..."
388
- searchValue={search}
389
- onSearchChange={setSearch}
390
- />
391
- </div>
392
- </div>
393
-
394
- <div className="grid gap-4 md:grid-cols-2">
395
- <Card>
396
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
397
- <CardTitle className="text-sm font-medium">
398
- {t('cards.totalTransferred')}
399
- </CardTitle>
400
- </CardHeader>
401
- <CardContent>
402
- <div className="text-2xl font-bold">
403
- <Money value={totalTransferido} />
404
- </div>
405
- <p className="text-xs text-muted-foreground">
406
- {t('cards.transferCount', { count: transferencias.length })}
407
- </p>
408
- </CardContent>
409
- </Card>
410
- <Card>
411
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
412
- <CardTitle className="text-sm font-medium">
413
- {t('cards.activeAccounts')}
414
- </CardTitle>
415
- </CardHeader>
416
- <CardContent>
417
- <div className="text-2xl font-bold">
418
- {contasBancarias.filter((conta) => conta.ativo).length}
419
- </div>
420
- <p className="text-xs text-muted-foreground">
421
- {t('cards.available')}
422
- </p>
423
- </CardContent>
424
- </Card>
425
- </div>
426
-
427
- <Card>
428
- <CardHeader>
429
- <CardTitle>{t('table.title')}</CardTitle>
430
- <CardDescription>{t('table.description')}</CardDescription>
431
- </CardHeader>
432
- <CardContent>
433
- <Table>
434
- <TableHeader>
435
- <TableRow>
436
- <TableHead>{t('table.headers.date')}</TableHead>
437
- <TableHead>{t('table.headers.source')}</TableHead>
438
- <TableHead className="text-center">→</TableHead>
439
- <TableHead>{t('table.headers.destination')}</TableHead>
440
- <TableHead className="text-right">
441
- {t('table.headers.value')}
442
- </TableHead>
443
- <TableHead>{t('table.headers.description')}</TableHead>
444
- </TableRow>
445
- </TableHeader>
446
- <TableBody>
447
- {transferencias.map((transferencia) => {
448
- const contaOrigem = contasBancarias.find(
449
- (conta) => conta.id === transferencia.contaOrigemId
450
- );
451
- const contaDestino = contasBancarias.find(
452
- (conta) => conta.id === transferencia.contaDestinoId
453
- );
454
-
455
- return (
456
- <TableRow key={transferencia.id}>
457
- <TableCell>{formatarData(transferencia.data)}</TableCell>
458
- <TableCell>
459
- <div>
460
- <p className="font-medium">{contaOrigem?.banco}</p>
461
- <p className="text-xs text-muted-foreground">
462
- {contaOrigem?.descricao}
463
- </p>
464
- </div>
465
- </TableCell>
466
- <TableCell className="text-center">
467
- <ArrowRight className="h-4 w-4 text-muted-foreground mx-auto" />
468
- </TableCell>
469
- <TableCell>
470
- <div>
471
- <p className="font-medium">{contaDestino?.banco}</p>
472
- <p className="text-xs text-muted-foreground">
473
- {contaDestino?.descricao}
474
- </p>
475
- </div>
476
- </TableCell>
477
- <TableCell className="text-right font-semibold">
478
- <Money value={transferencia.valor} />
479
- </TableCell>
480
- <TableCell className="text-muted-foreground">
481
- {transferencia.descricao}
482
- </TableCell>
483
- </TableRow>
484
- );
485
- })}
486
- </TableBody>
487
- </Table>
488
- </CardContent>
489
- </Card>
490
- </Page>
491
- );
492
- }
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import { Button } from '@/components/ui/button';
5
+ import {
6
+ Card,
7
+ CardContent,
8
+ CardDescription,
9
+ CardHeader,
10
+ CardTitle,
11
+ } from '@/components/ui/card';
12
+ import { FilterBar } from '@/components/ui/filter-bar';
13
+ import {
14
+ Form,
15
+ FormControl,
16
+ FormField,
17
+ FormItem,
18
+ FormLabel,
19
+ FormMessage,
20
+ } from '@/components/ui/form';
21
+ import { Input } from '@/components/ui/input';
22
+ import { InputMoney } from '@/components/ui/input-money';
23
+ import { Money } from '@/components/ui/money';
24
+ import {
25
+ Select,
26
+ SelectContent,
27
+ SelectItem,
28
+ SelectTrigger,
29
+ SelectValue,
30
+ } from '@/components/ui/select';
31
+ import {
32
+ Sheet,
33
+ SheetContent,
34
+ SheetDescription,
35
+ SheetHeader,
36
+ SheetTitle,
37
+ } from '@/components/ui/sheet';
38
+ import {
39
+ Table,
40
+ TableBody,
41
+ TableCell,
42
+ TableHead,
43
+ TableHeader,
44
+ TableRow,
45
+ } from '@/components/ui/table';
46
+ import { Textarea } from '@/components/ui/textarea';
47
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
48
+ import { zodResolver } from '@hookform/resolvers/zod';
49
+ import { ArrowRight, Plus } from 'lucide-react';
50
+ import { useTranslations } from 'next-intl';
51
+ import { useEffect, useState } from 'react';
52
+ import { useForm } from 'react-hook-form';
53
+ import { z } from 'zod';
54
+ import { formatarData } from '../../_lib/formatters';
55
+
56
+ type BankAccount = {
57
+ id: string;
58
+ banco: string;
59
+ descricao: string;
60
+ ativo: boolean;
61
+ };
62
+
63
+ type Transfer = {
64
+ id: string;
65
+ contaOrigemId: string;
66
+ contaDestinoId: string;
67
+ data: string;
68
+ valor: number;
69
+ descricao: string;
70
+ };
71
+
72
+ const transferFormSchema = z
73
+ .object({
74
+ sourceAccountId: z.string().trim().min(1, 'Conta de origem é obrigatória'),
75
+ destinationAccountId: z
76
+ .string()
77
+ .trim()
78
+ .min(1, 'Conta de destino é obrigatória'),
79
+ date: z.string().trim().min(1, 'Data é obrigatória'),
80
+ amount: z.number().min(0.01, 'Valor deve ser maior que zero'),
81
+ description: z.string().optional(),
82
+ })
83
+ .refine((values) => values.sourceAccountId !== values.destinationAccountId, {
84
+ message: 'As contas de origem e destino devem ser diferentes',
85
+ path: ['destinationAccountId'],
86
+ });
87
+
88
+ type TransferFormValues = z.infer<typeof transferFormSchema>;
89
+
90
+ function NovaTransferenciaSheet({
91
+ contasBancarias,
92
+ t,
93
+ onCreated,
94
+ }: {
95
+ contasBancarias: BankAccount[];
96
+ t: ReturnType<typeof useTranslations>;
97
+ onCreated: () => Promise<void> | void;
98
+ }) {
99
+ const { request, showToastHandler } = useApp();
100
+ const [open, setOpen] = useState(false);
101
+
102
+ const form = useForm<TransferFormValues>({
103
+ resolver: zodResolver(transferFormSchema),
104
+ defaultValues: {
105
+ sourceAccountId: '',
106
+ destinationAccountId: '',
107
+ date: new Date().toISOString().slice(0, 10),
108
+ amount: 0,
109
+ description: '',
110
+ },
111
+ });
112
+
113
+ useEffect(() => {
114
+ if (!open) {
115
+ return;
116
+ }
117
+
118
+ form.reset({
119
+ sourceAccountId: '',
120
+ destinationAccountId: '',
121
+ date: new Date().toISOString().slice(0, 10),
122
+ amount: 0,
123
+ description: '',
124
+ });
125
+ }, [form, open]);
126
+
127
+ const handleSubmit = async (values: TransferFormValues) => {
128
+ try {
129
+ await request({
130
+ url: '/finance/transfers',
131
+ method: 'POST',
132
+ data: {
133
+ source_account_id: Number(values.sourceAccountId),
134
+ destination_account_id: Number(values.destinationAccountId),
135
+ date: values.date,
136
+ amount: values.amount,
137
+ description: values.description?.trim() || undefined,
138
+ },
139
+ });
140
+
141
+ await onCreated();
142
+ setOpen(false);
143
+ showToastHandler?.('success', 'Transferência cadastrada com sucesso');
144
+ } catch {
145
+ showToastHandler?.('error', 'Erro ao cadastrar transferência');
146
+ }
147
+ };
148
+
149
+ return (
150
+ <Sheet open={open} onOpenChange={setOpen}>
151
+ <Button onClick={() => setOpen(true)}>
152
+ <Plus className="mr-2 h-4 w-4" />
153
+ {t('newTransfer.action')}
154
+ </Button>
155
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
156
+ <SheetHeader>
157
+ <SheetTitle>{t('newTransfer.title')}</SheetTitle>
158
+ <SheetDescription>{t('newTransfer.description')}</SheetDescription>
159
+ </SheetHeader>
160
+
161
+ <Form {...form}>
162
+ <form
163
+ className="space-y-4 px-4"
164
+ onSubmit={form.handleSubmit(handleSubmit)}
165
+ >
166
+ <FormField
167
+ control={form.control}
168
+ name="sourceAccountId"
169
+ render={({ field }) => (
170
+ <FormItem>
171
+ <FormLabel>{t('fields.sourceAccount')}</FormLabel>
172
+ <Select value={field.value} onValueChange={field.onChange}>
173
+ <FormControl>
174
+ <SelectTrigger>
175
+ <SelectValue placeholder={t('common.select')} />
176
+ </SelectTrigger>
177
+ </FormControl>
178
+ <SelectContent>
179
+ {contasBancarias.map((conta) => (
180
+ <SelectItem key={conta.id} value={conta.id}>
181
+ {conta.banco} - {conta.descricao}
182
+ </SelectItem>
183
+ ))}
184
+ </SelectContent>
185
+ </Select>
186
+ <FormMessage />
187
+ </FormItem>
188
+ )}
189
+ />
190
+
191
+ <FormField
192
+ control={form.control}
193
+ name="destinationAccountId"
194
+ render={({ field }) => (
195
+ <FormItem>
196
+ <FormLabel>{t('fields.destinationAccount')}</FormLabel>
197
+ <Select value={field.value} onValueChange={field.onChange}>
198
+ <FormControl>
199
+ <SelectTrigger>
200
+ <SelectValue placeholder={t('common.select')} />
201
+ </SelectTrigger>
202
+ </FormControl>
203
+ <SelectContent>
204
+ {contasBancarias.map((conta) => (
205
+ <SelectItem key={conta.id} value={conta.id}>
206
+ {conta.banco} - {conta.descricao}
207
+ </SelectItem>
208
+ ))}
209
+ </SelectContent>
210
+ </Select>
211
+ <FormMessage />
212
+ </FormItem>
213
+ )}
214
+ />
215
+
216
+ <div className="grid grid-cols-2 gap-4">
217
+ <FormField
218
+ control={form.control}
219
+ name="date"
220
+ render={({ field }) => (
221
+ <FormItem>
222
+ <FormLabel>{t('fields.date')}</FormLabel>
223
+ <FormControl>
224
+ <Input type="date" {...field} />
225
+ </FormControl>
226
+ <FormMessage />
227
+ </FormItem>
228
+ )}
229
+ />
230
+
231
+ <FormField
232
+ control={form.control}
233
+ name="amount"
234
+ render={({ field }) => (
235
+ <FormItem>
236
+ <FormLabel>{t('fields.value')}</FormLabel>
237
+ <FormControl>
238
+ <InputMoney
239
+ ref={field.ref}
240
+ name={field.name}
241
+ value={field.value}
242
+ onBlur={field.onBlur}
243
+ onValueChange={(value) => field.onChange(value ?? 0)}
244
+ placeholder="0,00"
245
+ />
246
+ </FormControl>
247
+ <FormMessage />
248
+ </FormItem>
249
+ )}
250
+ />
251
+ </div>
252
+
253
+ <FormField
254
+ control={form.control}
255
+ name="description"
256
+ render={({ field }) => (
257
+ <FormItem>
258
+ <FormLabel>{t('fields.description')}</FormLabel>
259
+ <FormControl>
260
+ <Textarea
261
+ placeholder={t('fields.descriptionPlaceholder')}
262
+ {...field}
263
+ value={field.value || ''}
264
+ />
265
+ </FormControl>
266
+ <FormMessage />
267
+ </FormItem>
268
+ )}
269
+ />
270
+
271
+ <div className="flex justify-end gap-2 pt-4">
272
+ <Button
273
+ type="button"
274
+ variant="outline"
275
+ onClick={() => setOpen(false)}
276
+ >
277
+ {t('common.cancel')}
278
+ </Button>
279
+ <Button type="submit" disabled={form.formState.isSubmitting}>
280
+ {t('newTransfer.submit')}
281
+ </Button>
282
+ </div>
283
+ </form>
284
+ </Form>
285
+ </SheetContent>
286
+ </Sheet>
287
+ );
288
+ }
289
+
290
+ export default function TransferenciasPage() {
291
+ const t = useTranslations('finance.TransfersPage');
292
+ const { request } = useApp();
293
+ const [search, setSearch] = useState('');
294
+ const [debouncedSearch, setDebouncedSearch] = useState('');
295
+ const [accountFilter, setAccountFilter] = useState('all');
296
+
297
+ useEffect(() => {
298
+ const timeoutId = window.setTimeout(() => {
299
+ setDebouncedSearch(search);
300
+ }, 300);
301
+
302
+ return () => {
303
+ window.clearTimeout(timeoutId);
304
+ };
305
+ }, [search]);
306
+
307
+ const { data: contasBancarias = [], refetch: refetchContasBancarias } =
308
+ useQuery<BankAccount[]>({
309
+ queryKey: ['finance-bank-accounts'],
310
+ queryFn: async () => {
311
+ const response = await request({
312
+ url: '/finance/bank-accounts',
313
+ method: 'GET',
314
+ });
315
+
316
+ return (response?.data || []) as BankAccount[];
317
+ },
318
+ });
319
+
320
+ const { data: transferencias = [], refetch: refetchTransferencias } =
321
+ useQuery<Transfer[]>({
322
+ queryKey: ['finance-transfers', debouncedSearch, accountFilter],
323
+ queryFn: async () => {
324
+ const params = new URLSearchParams();
325
+
326
+ const trimmedSearch = debouncedSearch.trim();
327
+ if (trimmedSearch) {
328
+ params.set('search', trimmedSearch);
329
+ }
330
+
331
+ if (accountFilter !== 'all') {
332
+ params.set('bank_account_id', accountFilter);
333
+ }
334
+
335
+ const query = params.toString();
336
+ const response = await request({
337
+ url: query ? `/finance/transfers?${query}` : '/finance/transfers',
338
+ method: 'GET',
339
+ });
340
+
341
+ return (response?.data || []) as Transfer[];
342
+ },
343
+ });
344
+
345
+ const handleTransferCreated = async () => {
346
+ await Promise.all([refetchTransferencias(), refetchContasBancarias()]);
347
+ };
348
+
349
+ const totalTransferido = transferencias.reduce((acc, t) => acc + t.valor, 0);
350
+
351
+ return (
352
+ <Page>
353
+ <PageHeader
354
+ title={t('header.title')}
355
+ description={t('header.description')}
356
+ breadcrumbs={[
357
+ { label: t('breadcrumbs.home'), href: '/' },
358
+ { label: t('breadcrumbs.finance'), href: '/finance' },
359
+ { label: t('breadcrumbs.current') },
360
+ ]}
361
+ actions={
362
+ <NovaTransferenciaSheet
363
+ contasBancarias={contasBancarias}
364
+ t={t}
365
+ onCreated={handleTransferCreated}
366
+ />
367
+ }
368
+ />
369
+
370
+ <div className="flex flex-col gap-4 sm:flex-row sm:items-center">
371
+ <Select value={accountFilter} onValueChange={setAccountFilter}>
372
+ <SelectTrigger className="w-full sm:w-[280px]">
373
+ <SelectValue placeholder={t('common.select')} />
374
+ </SelectTrigger>
375
+ <SelectContent>
376
+ <SelectItem value="all">Todas as contas</SelectItem>
377
+ {contasBancarias.map((conta) => (
378
+ <SelectItem key={conta.id} value={conta.id}>
379
+ {conta.banco} - {conta.descricao}
380
+ </SelectItem>
381
+ ))}
382
+ </SelectContent>
383
+ </Select>
384
+
385
+ <div className="flex-1">
386
+ <FilterBar
387
+ searchPlaceholder="Buscar na descrição..."
388
+ searchValue={search}
389
+ onSearchChange={setSearch}
390
+ />
391
+ </div>
392
+ </div>
393
+
394
+ <div className="grid gap-4 md:grid-cols-2">
395
+ <Card>
396
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
397
+ <CardTitle className="text-sm font-medium">
398
+ {t('cards.totalTransferred')}
399
+ </CardTitle>
400
+ </CardHeader>
401
+ <CardContent>
402
+ <div className="text-2xl font-bold">
403
+ <Money value={totalTransferido} />
404
+ </div>
405
+ <p className="text-xs text-muted-foreground">
406
+ {t('cards.transferCount', { count: transferencias.length })}
407
+ </p>
408
+ </CardContent>
409
+ </Card>
410
+ <Card>
411
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
412
+ <CardTitle className="text-sm font-medium">
413
+ {t('cards.activeAccounts')}
414
+ </CardTitle>
415
+ </CardHeader>
416
+ <CardContent>
417
+ <div className="text-2xl font-bold">
418
+ {contasBancarias.filter((conta) => conta.ativo).length}
419
+ </div>
420
+ <p className="text-xs text-muted-foreground">
421
+ {t('cards.available')}
422
+ </p>
423
+ </CardContent>
424
+ </Card>
425
+ </div>
426
+
427
+ <Card>
428
+ <CardHeader>
429
+ <CardTitle>{t('table.title')}</CardTitle>
430
+ <CardDescription>{t('table.description')}</CardDescription>
431
+ </CardHeader>
432
+ <CardContent>
433
+ <Table>
434
+ <TableHeader>
435
+ <TableRow>
436
+ <TableHead>{t('table.headers.date')}</TableHead>
437
+ <TableHead>{t('table.headers.source')}</TableHead>
438
+ <TableHead className="text-center">→</TableHead>
439
+ <TableHead>{t('table.headers.destination')}</TableHead>
440
+ <TableHead className="text-right">
441
+ {t('table.headers.value')}
442
+ </TableHead>
443
+ <TableHead>{t('table.headers.description')}</TableHead>
444
+ </TableRow>
445
+ </TableHeader>
446
+ <TableBody>
447
+ {transferencias.map((transferencia) => {
448
+ const contaOrigem = contasBancarias.find(
449
+ (conta) => conta.id === transferencia.contaOrigemId
450
+ );
451
+ const contaDestino = contasBancarias.find(
452
+ (conta) => conta.id === transferencia.contaDestinoId
453
+ );
454
+
455
+ return (
456
+ <TableRow key={transferencia.id}>
457
+ <TableCell>{formatarData(transferencia.data)}</TableCell>
458
+ <TableCell>
459
+ <div>
460
+ <p className="font-medium">{contaOrigem?.banco}</p>
461
+ <p className="text-xs text-muted-foreground">
462
+ {contaOrigem?.descricao}
463
+ </p>
464
+ </div>
465
+ </TableCell>
466
+ <TableCell className="text-center">
467
+ <ArrowRight className="h-4 w-4 text-muted-foreground mx-auto" />
468
+ </TableCell>
469
+ <TableCell>
470
+ <div>
471
+ <p className="font-medium">{contaDestino?.banco}</p>
472
+ <p className="text-xs text-muted-foreground">
473
+ {contaDestino?.descricao}
474
+ </p>
475
+ </div>
476
+ </TableCell>
477
+ <TableCell className="text-right font-semibold">
478
+ <Money value={transferencia.valor} />
479
+ </TableCell>
480
+ <TableCell className="text-muted-foreground">
481
+ {transferencia.descricao}
482
+ </TableCell>
483
+ </TableRow>
484
+ );
485
+ })}
486
+ </TableBody>
487
+ </Table>
488
+ </CardContent>
489
+ </Card>
490
+ </Page>
491
+ );
492
+ }