@hed-hog/finance 0.0.274 → 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 +228 -126
  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 +7 -7
  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,686 +1,686 @@
1
- 'use client';
2
-
3
- import { Page, PageHeader } from '@/components/entity-list';
4
- import {
5
- AlertDialog,
6
- AlertDialogAction,
7
- AlertDialogCancel,
8
- AlertDialogContent,
9
- AlertDialogDescription,
10
- AlertDialogFooter,
11
- AlertDialogHeader,
12
- AlertDialogTitle,
13
- } from '@/components/ui/alert-dialog';
14
- import { Badge } from '@/components/ui/badge';
15
- import { Button } from '@/components/ui/button';
16
- import {
17
- Card,
18
- CardContent,
19
- CardDescription,
20
- CardHeader,
21
- CardTitle,
22
- } from '@/components/ui/card';
23
- import {
24
- Form,
25
- FormControl,
26
- FormField,
27
- FormItem,
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 { Money } from '@/components/ui/money';
34
- import {
35
- Select,
36
- SelectContent,
37
- SelectItem,
38
- SelectTrigger,
39
- SelectValue,
40
- } from '@/components/ui/select';
41
- import {
42
- Sheet,
43
- SheetContent,
44
- SheetDescription,
45
- SheetHeader,
46
- SheetTitle,
47
- } from '@/components/ui/sheet';
48
- import { useApp, useQuery } from '@hed-hog/next-app-provider';
49
- import { zodResolver } from '@hookform/resolvers/zod';
50
- import {
51
- Building2,
52
- Eye,
53
- Landmark,
54
- Pencil,
55
- PiggyBank,
56
- Plus,
57
- RefreshCw,
58
- Trash2,
59
- TrendingUp,
60
- Upload,
61
- Wallet,
62
- } from 'lucide-react';
63
- import { useTranslations } from 'next-intl';
64
- import Link from 'next/link';
65
- import { useEffect, useState } from 'react';
66
- import { useForm } from 'react-hook-form';
67
- import { z } from 'zod';
68
-
69
- const bankAccountFormSchema = z.object({
70
- banco: z.string().trim().min(1, 'Banco é obrigatório'),
71
- agencia: z.string().optional(),
72
- conta: z.string().optional(),
73
- tipo: z.string().min(1, 'Tipo é obrigatório'),
74
- descricao: z.string().optional(),
75
- saldoInicial: z.number().min(0, 'Saldo inicial inválido'),
76
- });
77
-
78
- type BankAccountFormValues = z.infer<typeof bankAccountFormSchema>;
79
-
80
- type BankAccount = {
81
- id: string;
82
- codigo: string;
83
- descricao: string;
84
- banco: string;
85
- agencia: string;
86
- conta: string;
87
- tipo: 'corrente' | 'poupanca' | 'investimento' | 'caixa';
88
- saldoAtual: number;
89
- saldoConciliado: number;
90
- ativo: boolean;
91
- };
92
-
93
- function NovaContaSheet({
94
- t,
95
- onCreated,
96
- open,
97
- onOpenChange,
98
- editingAccount,
99
- onEditingAccountChange,
100
- }: {
101
- t: ReturnType<typeof useTranslations>;
102
- onCreated: () => Promise<any> | void;
103
- open: boolean;
104
- onOpenChange: (open: boolean) => void;
105
- editingAccount: BankAccount | null;
106
- onEditingAccountChange: (account: BankAccount | null) => void;
107
- }) {
108
- const { request, showToastHandler } = useApp();
109
-
110
- const createSuccessMessage = t.has('messages.createSuccess')
111
- ? t('messages.createSuccess')
112
- : 'Conta bancária cadastrada com sucesso';
113
- const createErrorMessage = t.has('messages.createError')
114
- ? t('messages.createError')
115
- : 'Erro ao cadastrar conta bancária';
116
- const updateSuccessMessage = t.has('messages.updateSuccess')
117
- ? t('messages.updateSuccess')
118
- : 'Conta bancária atualizada com sucesso';
119
- const updateErrorMessage = t.has('messages.updateError')
120
- ? t('messages.updateError')
121
- : 'Erro ao atualizar conta bancária';
122
-
123
- const form = useForm<BankAccountFormValues>({
124
- resolver: zodResolver(bankAccountFormSchema),
125
- defaultValues: {
126
- banco: '',
127
- agencia: '',
128
- conta: '',
129
- tipo: '',
130
- descricao: '',
131
- saldoInicial: 0,
132
- },
133
- });
134
-
135
- useEffect(() => {
136
- if (!open) {
137
- return;
138
- }
139
-
140
- if (editingAccount) {
141
- form.reset({
142
- banco: editingAccount.banco,
143
- agencia: editingAccount.agencia === '-' ? '' : editingAccount.agencia,
144
- conta: editingAccount.conta === '-' ? '' : editingAccount.conta,
145
- tipo: editingAccount.tipo,
146
- descricao: editingAccount.descricao,
147
- saldoInicial: editingAccount.saldoAtual,
148
- });
149
- return;
150
- }
151
-
152
- form.reset({
153
- banco: '',
154
- agencia: '',
155
- conta: '',
156
- tipo: '',
157
- descricao: '',
158
- saldoInicial: 0,
159
- });
160
- }, [editingAccount, form, open]);
161
-
162
- const handleSubmit = async (values: BankAccountFormValues) => {
163
- try {
164
- if (editingAccount) {
165
- await request({
166
- url: `/finance/bank-accounts/${editingAccount.id}`,
167
- method: 'PATCH',
168
- data: {
169
- bank: values.banco,
170
- branch: values.agencia || undefined,
171
- account: values.conta || undefined,
172
- type: values.tipo,
173
- description: values.descricao?.trim() || undefined,
174
- },
175
- });
176
- } else {
177
- await request({
178
- url: '/finance/bank-accounts',
179
- method: 'POST',
180
- data: {
181
- bank: values.banco,
182
- branch: values.agencia || undefined,
183
- account: values.conta || undefined,
184
- type: values.tipo,
185
- description: values.descricao?.trim() || undefined,
186
- initial_balance: values.saldoInicial,
187
- },
188
- });
189
- }
190
-
191
- await onCreated();
192
- form.reset();
193
- onOpenChange(false);
194
- onEditingAccountChange(null);
195
- showToastHandler?.(
196
- 'success',
197
- editingAccount ? updateSuccessMessage : createSuccessMessage
198
- );
199
- } catch {
200
- showToastHandler?.(
201
- 'error',
202
- editingAccount ? updateErrorMessage : createErrorMessage
203
- );
204
- }
205
- };
206
-
207
- const handleCancel = () => {
208
- form.reset();
209
- onEditingAccountChange(null);
210
- onOpenChange(false);
211
- };
212
-
213
- return (
214
- <Sheet
215
- open={open}
216
- onOpenChange={(nextOpen) => {
217
- onOpenChange(nextOpen);
218
- if (!nextOpen) {
219
- onEditingAccountChange(null);
220
- }
221
- }}
222
- >
223
- <SheetContent className="w-full sm:max-w-lg">
224
- <SheetHeader>
225
- <SheetTitle>
226
- {editingAccount ? t('common.edit') : t('newAccount.title')}
227
- </SheetTitle>
228
- <SheetDescription>{t('newAccount.description')}</SheetDescription>
229
- </SheetHeader>
230
- <Form {...form}>
231
- <form className="p-4" onSubmit={form.handleSubmit(handleSubmit)}>
232
- <div className="grid gap-4">
233
- <FormField
234
- control={form.control}
235
- name="banco"
236
- render={({ field }) => (
237
- <FormItem>
238
- <FormLabel>{t('fields.bank')}</FormLabel>
239
- <FormControl>
240
- <Input
241
- placeholder={t('fields.bankPlaceholder')}
242
- {...field}
243
- />
244
- </FormControl>
245
- <FormMessage />
246
- </FormItem>
247
- )}
248
- />
249
-
250
- <div className="grid grid-cols-2 gap-4">
251
- <FormField
252
- control={form.control}
253
- name="agencia"
254
- render={({ field }) => (
255
- <FormItem>
256
- <FormLabel>{t('fields.branch')}</FormLabel>
257
- <FormControl>
258
- <Input
259
- placeholder="0000"
260
- {...field}
261
- value={field.value || ''}
262
- />
263
- </FormControl>
264
- <FormMessage />
265
- </FormItem>
266
- )}
267
- />
268
-
269
- <FormField
270
- control={form.control}
271
- name="conta"
272
- render={({ field }) => (
273
- <FormItem>
274
- <FormLabel>{t('fields.account')}</FormLabel>
275
- <FormControl>
276
- <Input
277
- placeholder="00000-0"
278
- {...field}
279
- value={field.value || ''}
280
- />
281
- </FormControl>
282
- <FormMessage />
283
- </FormItem>
284
- )}
285
- />
286
- </div>
287
-
288
- <div className="grid grid-cols-2 gap-4">
289
- <FormField
290
- control={form.control}
291
- name="tipo"
292
- render={({ field }) => (
293
- <FormItem>
294
- <FormLabel>{t('fields.type')}</FormLabel>
295
- <Select
296
- value={field.value}
297
- onValueChange={field.onChange}
298
- >
299
- <FormControl>
300
- <SelectTrigger className="w-full">
301
- <SelectValue placeholder={t('common.select')} />
302
- </SelectTrigger>
303
- </FormControl>
304
- <SelectContent>
305
- <SelectItem value="corrente">
306
- {t('types.corrente')}
307
- </SelectItem>
308
- <SelectItem value="poupanca">
309
- {t('types.poupanca')}
310
- </SelectItem>
311
- <SelectItem value="investimento">
312
- {t('types.investimento')}
313
- </SelectItem>
314
- <SelectItem value="caixa">
315
- {t('types.caixa')}
316
- </SelectItem>
317
- </SelectContent>
318
- </Select>
319
- <FormMessage />
320
- </FormItem>
321
- )}
322
- />
323
-
324
- <FormField
325
- control={form.control}
326
- name="saldoInicial"
327
- render={({ field }) => (
328
- <FormItem>
329
- <FormLabel>{t('fields.initialBalance')}</FormLabel>
330
- <FormControl>
331
- <InputMoney
332
- ref={field.ref}
333
- name={field.name}
334
- value={field.value}
335
- onBlur={field.onBlur}
336
- onValueChange={(value) => field.onChange(value ?? 0)}
337
- placeholder="0,00"
338
- disabled={!!editingAccount}
339
- />
340
- </FormControl>
341
- <FormMessage />
342
- </FormItem>
343
- )}
344
- />
345
- </div>
346
-
347
- <FormField
348
- control={form.control}
349
- name="descricao"
350
- render={({ field }) => (
351
- <FormItem>
352
- <FormLabel>{t('fields.description')}</FormLabel>
353
- <FormControl>
354
- <Input
355
- placeholder={t('fields.descriptionPlaceholder')}
356
- {...field}
357
- value={field.value || ''}
358
- />
359
- </FormControl>
360
- <FormMessage />
361
- </FormItem>
362
- )}
363
- />
364
- </div>
365
-
366
- <div className="flex justify-end gap-2 pt-4">
367
- <Button type="button" variant="outline" onClick={handleCancel}>
368
- {t('common.cancel')}
369
- </Button>
370
- <Button type="submit" disabled={form.formState.isSubmitting}>
371
- {t('common.save')}
372
- </Button>
373
- </div>
374
- </form>
375
- </Form>
376
- </SheetContent>
377
- </Sheet>
378
- );
379
- }
380
-
381
- export default function ContasBancariasPage() {
382
- const t = useTranslations('finance.BankAccountsPage');
383
- const { request, showToastHandler, currentLocaleCode } = useApp();
384
-
385
- const deleteSuccessMessage = t.has('messages.deleteSuccess')
386
- ? t('messages.deleteSuccess')
387
- : 'Conta bancária inativada com sucesso';
388
- const deleteErrorMessage = t.has('messages.deleteError')
389
- ? t('messages.deleteError')
390
- : 'Erro ao inativar conta bancária';
391
- const deleteDialogTitle = t.has('deleteDialog.title')
392
- ? t('deleteDialog.title')
393
- : 'Inativar conta bancária';
394
- const deleteDialogDescription = t.has('deleteDialog.description')
395
- ? t('deleteDialog.description')
396
- : 'Deseja realmente inativar esta conta bancária?';
397
- const deleteDialogConfirm = t.has('deleteDialog.confirm')
398
- ? t('deleteDialog.confirm')
399
- : 'Inativar';
400
-
401
- const [sheetOpen, setSheetOpen] = useState(false);
402
- const [editingAccount, setEditingAccount] = useState<BankAccount | null>(
403
- null
404
- );
405
- const [accountIdToDelete, setAccountIdToDelete] = useState<string | null>(
406
- null
407
- );
408
- const { data: contasBancarias, refetch } = useQuery<BankAccount[]>({
409
- queryKey: ['finance-bank-accounts', currentLocaleCode],
410
- queryFn: async () => {
411
- const response = await request({
412
- url: '/finance/bank-accounts',
413
- method: 'GET',
414
- });
415
-
416
- return (response.data || []) as BankAccount[];
417
- },
418
- placeholderData: [],
419
- });
420
- const accounts = contasBancarias ?? [];
421
-
422
- const tipoConfig = {
423
- corrente: { label: t('types.corrente'), icon: Building2 },
424
- poupanca: { label: t('types.poupanca'), icon: PiggyBank },
425
- investimento: { label: t('types.investimento'), icon: TrendingUp },
426
- caixa: { label: t('types.caixa'), icon: Wallet },
427
- };
428
-
429
- const saldoTotal = accounts
430
- .filter((c) => c.ativo)
431
- .reduce((acc, c) => acc + c.saldoAtual, 0);
432
-
433
- const saldoConciliadoTotal = accounts
434
- .filter((c) => c.ativo)
435
- .reduce((acc, c) => acc + c.saldoConciliado, 0);
436
-
437
- const handleCreate = () => {
438
- setEditingAccount(null);
439
- setSheetOpen(true);
440
- };
441
-
442
- const handleEdit = (account: BankAccount) => {
443
- setEditingAccount(account);
444
- setSheetOpen(true);
445
- };
446
-
447
- const handleDelete = async () => {
448
- if (!accountIdToDelete) {
449
- return;
450
- }
451
-
452
- try {
453
- await request({
454
- url: `/finance/bank-accounts/${accountIdToDelete}`,
455
- method: 'DELETE',
456
- });
457
-
458
- await refetch();
459
- showToastHandler?.('success', deleteSuccessMessage);
460
- setAccountIdToDelete(null);
461
- } catch {
462
- showToastHandler?.('error', deleteErrorMessage);
463
- }
464
- };
465
-
466
- return (
467
- <Page>
468
- <PageHeader
469
- title={t('header.title')}
470
- description={t('header.description')}
471
- breadcrumbs={[
472
- { label: t('breadcrumbs.home'), href: '/' },
473
- { label: t('breadcrumbs.finance'), href: '/finance' },
474
- { label: t('breadcrumbs.current') },
475
- ]}
476
- actions={
477
- <Button onClick={handleCreate}>
478
- <Plus className="mr-2 h-4 w-4" />
479
- {t('newAccount.action')}
480
- </Button>
481
- }
482
- />
483
-
484
- <NovaContaSheet
485
- t={t}
486
- onCreated={refetch}
487
- open={sheetOpen}
488
- onOpenChange={setSheetOpen}
489
- editingAccount={editingAccount}
490
- onEditingAccountChange={setEditingAccount}
491
- />
492
-
493
- <AlertDialog
494
- open={!!accountIdToDelete}
495
- onOpenChange={(open) => {
496
- if (!open) {
497
- setAccountIdToDelete(null);
498
- }
499
- }}
500
- >
501
- <AlertDialogContent>
502
- <AlertDialogHeader>
503
- <AlertDialogTitle>{deleteDialogTitle}</AlertDialogTitle>
504
- <AlertDialogDescription>
505
- {deleteDialogDescription}
506
- </AlertDialogDescription>
507
- </AlertDialogHeader>
508
- <AlertDialogFooter>
509
- <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
510
- <AlertDialogAction onClick={handleDelete}>
511
- {deleteDialogConfirm}
512
- </AlertDialogAction>
513
- </AlertDialogFooter>
514
- </AlertDialogContent>
515
- </AlertDialog>
516
-
517
- <div className="grid gap-4 md:grid-cols-2">
518
- <Card>
519
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
520
- <CardTitle className="text-sm font-medium">
521
- {t('cards.totalBalance')}
522
- </CardTitle>
523
- <Landmark className="h-4 w-4 text-muted-foreground" />
524
- </CardHeader>
525
- <CardContent>
526
- <div className="text-2xl font-bold">
527
- <Money value={saldoTotal} />
528
- </div>
529
- <p className="text-xs text-muted-foreground">
530
- {t('cards.activeAccounts', {
531
- count: accounts.filter((c) => c.ativo).length,
532
- })}
533
- </p>
534
- </CardContent>
535
- </Card>
536
- <Card>
537
- <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
538
- <CardTitle className="text-sm font-medium">
539
- {t('cards.reconciledBalance')}
540
- </CardTitle>
541
- <RefreshCw className="h-4 w-4 text-muted-foreground" />
542
- </CardHeader>
543
- <CardContent>
544
- <div className="text-2xl font-bold">
545
- <Money value={saldoConciliadoTotal} />
546
- </div>
547
- <p className="text-xs text-muted-foreground">
548
- {t('cards.difference')}:{' '}
549
- <Money value={saldoTotal - saldoConciliadoTotal} />
550
- </p>
551
- </CardContent>
552
- </Card>
553
- </div>
554
-
555
- <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
556
- {accounts.map((conta) => {
557
- const tipo =
558
- tipoConfig[conta.tipo as keyof typeof tipoConfig] ||
559
- tipoConfig.corrente;
560
- const TipoIcon = tipo.icon;
561
- const diferenca = conta.saldoAtual - conta.saldoConciliado;
562
-
563
- return (
564
- <Card key={conta.id} className={!conta.ativo ? 'opacity-60' : ''}>
565
- <CardHeader>
566
- <div className="flex items-center justify-between">
567
- <div className="flex items-center gap-2">
568
- <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted">
569
- <TipoIcon className="h-5 w-5" />
570
- </div>
571
- <div>
572
- <div className="flex gap-4 items-center">
573
- <CardTitle className="text-base">
574
- {conta.banco}
575
- </CardTitle>
576
- {conta.descricao && (
577
- <span className="block text-muted-foreground text-xs">
578
- {conta.descricao}
579
- </span>
580
- )}
581
- </div>
582
- <CardDescription className="space-y-0.5">
583
- {conta.agencia !== '-' && (
584
- <span className="block">
585
- {t('accountCard.bankAccount', {
586
- agency: conta.agencia,
587
- account: conta.conta,
588
- })}
589
- </span>
590
- )}
591
- </CardDescription>
592
- </div>
593
- </div>
594
- {!conta.ativo && (
595
- <Badge variant="outline" className="text-muted-foreground">
596
- {t('status.inactive')}
597
- </Badge>
598
- )}
599
- </div>
600
- </CardHeader>
601
- <CardContent>
602
- <div className="space-y-3">
603
- <div>
604
- <p className="text-sm text-muted-foreground">
605
- {t('accountCard.currentBalance')}
606
- </p>
607
- <p className="text-2xl font-bold">
608
- <Money value={conta.saldoAtual} />
609
- </p>
610
- </div>
611
- <div className="flex items-center justify-between text-sm">
612
- <span className="text-muted-foreground">
613
- {t('accountCard.reconciledBalance')}
614
- </span>
615
- <Money value={conta.saldoConciliado} />
616
- </div>
617
- {diferenca !== 0 && (
618
- <div className="flex items-center justify-between text-sm">
619
- <span className="text-muted-foreground">
620
- {t('accountCard.difference')}
621
- </span>
622
- <span
623
- className={
624
- diferenca > 0 ? 'text-green-600' : 'text-red-600'
625
- }
626
- >
627
- <Money value={diferenca} showSign />
628
- </span>
629
- </div>
630
- )}
631
- <div className="flex flex-wrap gap-2 pt-2">
632
- <Button
633
- variant="outline"
634
- size="sm"
635
- className="min-w-0 flex-1 basis-[calc(50%-0.25rem)] bg-transparent"
636
- asChild
637
- >
638
- <Link
639
- href={`/finance/cash-and-banks/statements?bank_account_id=${conta.id}`}
640
- >
641
- <Eye className="mr-2 h-4 w-4" />
642
- {t('accountCard.statement')}
643
- </Link>
644
- </Button>
645
- <Button
646
- variant="outline"
647
- size="sm"
648
- className="min-w-0 flex-1 basis-[calc(50%-0.25rem)] bg-transparent"
649
- asChild
650
- >
651
- <Link
652
- href={`/finance/cash-and-banks/bank-reconciliation?bank_account_id=${conta.id}`}
653
- >
654
- <RefreshCw className="mr-2 h-4 w-4" />
655
- {t('accountCard.reconcile')}
656
- </Link>
657
- </Button>
658
- <div className="ml-auto flex shrink-0 gap-2">
659
- <Button variant="outline" size="sm">
660
- <Upload className="h-4 w-4" />
661
- </Button>
662
- <Button
663
- variant="outline"
664
- size="sm"
665
- onClick={() => handleEdit(conta)}
666
- >
667
- <Pencil className="h-4 w-4" />
668
- </Button>
669
- <Button
670
- variant="outline"
671
- size="sm"
672
- onClick={() => setAccountIdToDelete(conta.id)}
673
- >
674
- <Trash2 className="h-4 w-4" />
675
- </Button>
676
- </div>
677
- </div>
678
- </div>
679
- </CardContent>
680
- </Card>
681
- );
682
- })}
683
- </div>
684
- </Page>
685
- );
686
- }
1
+ 'use client';
2
+
3
+ import { Page, PageHeader } from '@/components/entity-list';
4
+ import {
5
+ AlertDialog,
6
+ AlertDialogAction,
7
+ AlertDialogCancel,
8
+ AlertDialogContent,
9
+ AlertDialogDescription,
10
+ AlertDialogFooter,
11
+ AlertDialogHeader,
12
+ AlertDialogTitle,
13
+ } from '@/components/ui/alert-dialog';
14
+ import { Badge } from '@/components/ui/badge';
15
+ import { Button } from '@/components/ui/button';
16
+ import {
17
+ Card,
18
+ CardContent,
19
+ CardDescription,
20
+ CardHeader,
21
+ CardTitle,
22
+ } from '@/components/ui/card';
23
+ import {
24
+ Form,
25
+ FormControl,
26
+ FormField,
27
+ FormItem,
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 { Money } from '@/components/ui/money';
34
+ import {
35
+ Select,
36
+ SelectContent,
37
+ SelectItem,
38
+ SelectTrigger,
39
+ SelectValue,
40
+ } from '@/components/ui/select';
41
+ import {
42
+ Sheet,
43
+ SheetContent,
44
+ SheetDescription,
45
+ SheetHeader,
46
+ SheetTitle,
47
+ } from '@/components/ui/sheet';
48
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
49
+ import { zodResolver } from '@hookform/resolvers/zod';
50
+ import {
51
+ Building2,
52
+ Eye,
53
+ Landmark,
54
+ Pencil,
55
+ PiggyBank,
56
+ Plus,
57
+ RefreshCw,
58
+ Trash2,
59
+ TrendingUp,
60
+ Upload,
61
+ Wallet,
62
+ } from 'lucide-react';
63
+ import { useTranslations } from 'next-intl';
64
+ import Link from 'next/link';
65
+ import { useEffect, useState } from 'react';
66
+ import { useForm } from 'react-hook-form';
67
+ import { z } from 'zod';
68
+
69
+ const bankAccountFormSchema = z.object({
70
+ banco: z.string().trim().min(1, 'Banco é obrigatório'),
71
+ agencia: z.string().optional(),
72
+ conta: z.string().optional(),
73
+ tipo: z.string().min(1, 'Tipo é obrigatório'),
74
+ descricao: z.string().optional(),
75
+ saldoInicial: z.number().min(0, 'Saldo inicial inválido'),
76
+ });
77
+
78
+ type BankAccountFormValues = z.infer<typeof bankAccountFormSchema>;
79
+
80
+ type BankAccount = {
81
+ id: string;
82
+ codigo: string;
83
+ descricao: string;
84
+ banco: string;
85
+ agencia: string;
86
+ conta: string;
87
+ tipo: 'corrente' | 'poupanca' | 'investimento' | 'caixa';
88
+ saldoAtual: number;
89
+ saldoConciliado: number;
90
+ ativo: boolean;
91
+ };
92
+
93
+ function NovaContaSheet({
94
+ t,
95
+ onCreated,
96
+ open,
97
+ onOpenChange,
98
+ editingAccount,
99
+ onEditingAccountChange,
100
+ }: {
101
+ t: ReturnType<typeof useTranslations>;
102
+ onCreated: () => Promise<any> | void;
103
+ open: boolean;
104
+ onOpenChange: (open: boolean) => void;
105
+ editingAccount: BankAccount | null;
106
+ onEditingAccountChange: (account: BankAccount | null) => void;
107
+ }) {
108
+ const { request, showToastHandler } = useApp();
109
+
110
+ const createSuccessMessage = t.has('messages.createSuccess')
111
+ ? t('messages.createSuccess')
112
+ : 'Conta bancária cadastrada com sucesso';
113
+ const createErrorMessage = t.has('messages.createError')
114
+ ? t('messages.createError')
115
+ : 'Erro ao cadastrar conta bancária';
116
+ const updateSuccessMessage = t.has('messages.updateSuccess')
117
+ ? t('messages.updateSuccess')
118
+ : 'Conta bancária atualizada com sucesso';
119
+ const updateErrorMessage = t.has('messages.updateError')
120
+ ? t('messages.updateError')
121
+ : 'Erro ao atualizar conta bancária';
122
+
123
+ const form = useForm<BankAccountFormValues>({
124
+ resolver: zodResolver(bankAccountFormSchema),
125
+ defaultValues: {
126
+ banco: '',
127
+ agencia: '',
128
+ conta: '',
129
+ tipo: '',
130
+ descricao: '',
131
+ saldoInicial: 0,
132
+ },
133
+ });
134
+
135
+ useEffect(() => {
136
+ if (!open) {
137
+ return;
138
+ }
139
+
140
+ if (editingAccount) {
141
+ form.reset({
142
+ banco: editingAccount.banco,
143
+ agencia: editingAccount.agencia === '-' ? '' : editingAccount.agencia,
144
+ conta: editingAccount.conta === '-' ? '' : editingAccount.conta,
145
+ tipo: editingAccount.tipo,
146
+ descricao: editingAccount.descricao,
147
+ saldoInicial: editingAccount.saldoAtual,
148
+ });
149
+ return;
150
+ }
151
+
152
+ form.reset({
153
+ banco: '',
154
+ agencia: '',
155
+ conta: '',
156
+ tipo: '',
157
+ descricao: '',
158
+ saldoInicial: 0,
159
+ });
160
+ }, [editingAccount, form, open]);
161
+
162
+ const handleSubmit = async (values: BankAccountFormValues) => {
163
+ try {
164
+ if (editingAccount) {
165
+ await request({
166
+ url: `/finance/bank-accounts/${editingAccount.id}`,
167
+ method: 'PATCH',
168
+ data: {
169
+ bank: values.banco,
170
+ branch: values.agencia || undefined,
171
+ account: values.conta || undefined,
172
+ type: values.tipo,
173
+ description: values.descricao?.trim() || undefined,
174
+ },
175
+ });
176
+ } else {
177
+ await request({
178
+ url: '/finance/bank-accounts',
179
+ method: 'POST',
180
+ data: {
181
+ bank: values.banco,
182
+ branch: values.agencia || undefined,
183
+ account: values.conta || undefined,
184
+ type: values.tipo,
185
+ description: values.descricao?.trim() || undefined,
186
+ initial_balance: values.saldoInicial,
187
+ },
188
+ });
189
+ }
190
+
191
+ await onCreated();
192
+ form.reset();
193
+ onOpenChange(false);
194
+ onEditingAccountChange(null);
195
+ showToastHandler?.(
196
+ 'success',
197
+ editingAccount ? updateSuccessMessage : createSuccessMessage
198
+ );
199
+ } catch {
200
+ showToastHandler?.(
201
+ 'error',
202
+ editingAccount ? updateErrorMessage : createErrorMessage
203
+ );
204
+ }
205
+ };
206
+
207
+ const handleCancel = () => {
208
+ form.reset();
209
+ onEditingAccountChange(null);
210
+ onOpenChange(false);
211
+ };
212
+
213
+ return (
214
+ <Sheet
215
+ open={open}
216
+ onOpenChange={(nextOpen) => {
217
+ onOpenChange(nextOpen);
218
+ if (!nextOpen) {
219
+ onEditingAccountChange(null);
220
+ }
221
+ }}
222
+ >
223
+ <SheetContent className="w-full sm:max-w-lg">
224
+ <SheetHeader>
225
+ <SheetTitle>
226
+ {editingAccount ? t('common.edit') : t('newAccount.title')}
227
+ </SheetTitle>
228
+ <SheetDescription>{t('newAccount.description')}</SheetDescription>
229
+ </SheetHeader>
230
+ <Form {...form}>
231
+ <form className="p-4" onSubmit={form.handleSubmit(handleSubmit)}>
232
+ <div className="grid gap-4">
233
+ <FormField
234
+ control={form.control}
235
+ name="banco"
236
+ render={({ field }) => (
237
+ <FormItem>
238
+ <FormLabel>{t('fields.bank')}</FormLabel>
239
+ <FormControl>
240
+ <Input
241
+ placeholder={t('fields.bankPlaceholder')}
242
+ {...field}
243
+ />
244
+ </FormControl>
245
+ <FormMessage />
246
+ </FormItem>
247
+ )}
248
+ />
249
+
250
+ <div className="grid grid-cols-2 gap-4">
251
+ <FormField
252
+ control={form.control}
253
+ name="agencia"
254
+ render={({ field }) => (
255
+ <FormItem>
256
+ <FormLabel>{t('fields.branch')}</FormLabel>
257
+ <FormControl>
258
+ <Input
259
+ placeholder="0000"
260
+ {...field}
261
+ value={field.value || ''}
262
+ />
263
+ </FormControl>
264
+ <FormMessage />
265
+ </FormItem>
266
+ )}
267
+ />
268
+
269
+ <FormField
270
+ control={form.control}
271
+ name="conta"
272
+ render={({ field }) => (
273
+ <FormItem>
274
+ <FormLabel>{t('fields.account')}</FormLabel>
275
+ <FormControl>
276
+ <Input
277
+ placeholder="00000-0"
278
+ {...field}
279
+ value={field.value || ''}
280
+ />
281
+ </FormControl>
282
+ <FormMessage />
283
+ </FormItem>
284
+ )}
285
+ />
286
+ </div>
287
+
288
+ <div className="grid grid-cols-2 gap-4">
289
+ <FormField
290
+ control={form.control}
291
+ name="tipo"
292
+ render={({ field }) => (
293
+ <FormItem>
294
+ <FormLabel>{t('fields.type')}</FormLabel>
295
+ <Select
296
+ value={field.value}
297
+ onValueChange={field.onChange}
298
+ >
299
+ <FormControl>
300
+ <SelectTrigger className="w-full">
301
+ <SelectValue placeholder={t('common.select')} />
302
+ </SelectTrigger>
303
+ </FormControl>
304
+ <SelectContent>
305
+ <SelectItem value="corrente">
306
+ {t('types.corrente')}
307
+ </SelectItem>
308
+ <SelectItem value="poupanca">
309
+ {t('types.poupanca')}
310
+ </SelectItem>
311
+ <SelectItem value="investimento">
312
+ {t('types.investimento')}
313
+ </SelectItem>
314
+ <SelectItem value="caixa">
315
+ {t('types.caixa')}
316
+ </SelectItem>
317
+ </SelectContent>
318
+ </Select>
319
+ <FormMessage />
320
+ </FormItem>
321
+ )}
322
+ />
323
+
324
+ <FormField
325
+ control={form.control}
326
+ name="saldoInicial"
327
+ render={({ field }) => (
328
+ <FormItem>
329
+ <FormLabel>{t('fields.initialBalance')}</FormLabel>
330
+ <FormControl>
331
+ <InputMoney
332
+ ref={field.ref}
333
+ name={field.name}
334
+ value={field.value}
335
+ onBlur={field.onBlur}
336
+ onValueChange={(value) => field.onChange(value ?? 0)}
337
+ placeholder="0,00"
338
+ disabled={!!editingAccount}
339
+ />
340
+ </FormControl>
341
+ <FormMessage />
342
+ </FormItem>
343
+ )}
344
+ />
345
+ </div>
346
+
347
+ <FormField
348
+ control={form.control}
349
+ name="descricao"
350
+ render={({ field }) => (
351
+ <FormItem>
352
+ <FormLabel>{t('fields.description')}</FormLabel>
353
+ <FormControl>
354
+ <Input
355
+ placeholder={t('fields.descriptionPlaceholder')}
356
+ {...field}
357
+ value={field.value || ''}
358
+ />
359
+ </FormControl>
360
+ <FormMessage />
361
+ </FormItem>
362
+ )}
363
+ />
364
+ </div>
365
+
366
+ <div className="flex justify-end gap-2 pt-4">
367
+ <Button type="button" variant="outline" onClick={handleCancel}>
368
+ {t('common.cancel')}
369
+ </Button>
370
+ <Button type="submit" disabled={form.formState.isSubmitting}>
371
+ {t('common.save')}
372
+ </Button>
373
+ </div>
374
+ </form>
375
+ </Form>
376
+ </SheetContent>
377
+ </Sheet>
378
+ );
379
+ }
380
+
381
+ export default function ContasBancariasPage() {
382
+ const t = useTranslations('finance.BankAccountsPage');
383
+ const { request, showToastHandler, currentLocaleCode } = useApp();
384
+
385
+ const deleteSuccessMessage = t.has('messages.deleteSuccess')
386
+ ? t('messages.deleteSuccess')
387
+ : 'Conta bancária inativada com sucesso';
388
+ const deleteErrorMessage = t.has('messages.deleteError')
389
+ ? t('messages.deleteError')
390
+ : 'Erro ao inativar conta bancária';
391
+ const deleteDialogTitle = t.has('deleteDialog.title')
392
+ ? t('deleteDialog.title')
393
+ : 'Inativar conta bancária';
394
+ const deleteDialogDescription = t.has('deleteDialog.description')
395
+ ? t('deleteDialog.description')
396
+ : 'Deseja realmente inativar esta conta bancária?';
397
+ const deleteDialogConfirm = t.has('deleteDialog.confirm')
398
+ ? t('deleteDialog.confirm')
399
+ : 'Inativar';
400
+
401
+ const [sheetOpen, setSheetOpen] = useState(false);
402
+ const [editingAccount, setEditingAccount] = useState<BankAccount | null>(
403
+ null
404
+ );
405
+ const [accountIdToDelete, setAccountIdToDelete] = useState<string | null>(
406
+ null
407
+ );
408
+ const { data: contasBancarias, refetch } = useQuery<BankAccount[]>({
409
+ queryKey: ['finance-bank-accounts', currentLocaleCode],
410
+ queryFn: async () => {
411
+ const response = await request({
412
+ url: '/finance/bank-accounts',
413
+ method: 'GET',
414
+ });
415
+
416
+ return (response.data || []) as BankAccount[];
417
+ },
418
+ placeholderData: [],
419
+ });
420
+ const accounts = contasBancarias ?? [];
421
+
422
+ const tipoConfig = {
423
+ corrente: { label: t('types.corrente'), icon: Building2 },
424
+ poupanca: { label: t('types.poupanca'), icon: PiggyBank },
425
+ investimento: { label: t('types.investimento'), icon: TrendingUp },
426
+ caixa: { label: t('types.caixa'), icon: Wallet },
427
+ };
428
+
429
+ const saldoTotal = accounts
430
+ .filter((c) => c.ativo)
431
+ .reduce((acc, c) => acc + c.saldoAtual, 0);
432
+
433
+ const saldoConciliadoTotal = accounts
434
+ .filter((c) => c.ativo)
435
+ .reduce((acc, c) => acc + c.saldoConciliado, 0);
436
+
437
+ const handleCreate = () => {
438
+ setEditingAccount(null);
439
+ setSheetOpen(true);
440
+ };
441
+
442
+ const handleEdit = (account: BankAccount) => {
443
+ setEditingAccount(account);
444
+ setSheetOpen(true);
445
+ };
446
+
447
+ const handleDelete = async () => {
448
+ if (!accountIdToDelete) {
449
+ return;
450
+ }
451
+
452
+ try {
453
+ await request({
454
+ url: `/finance/bank-accounts/${accountIdToDelete}`,
455
+ method: 'DELETE',
456
+ });
457
+
458
+ await refetch();
459
+ showToastHandler?.('success', deleteSuccessMessage);
460
+ setAccountIdToDelete(null);
461
+ } catch {
462
+ showToastHandler?.('error', deleteErrorMessage);
463
+ }
464
+ };
465
+
466
+ return (
467
+ <Page>
468
+ <PageHeader
469
+ title={t('header.title')}
470
+ description={t('header.description')}
471
+ breadcrumbs={[
472
+ { label: t('breadcrumbs.home'), href: '/' },
473
+ { label: t('breadcrumbs.finance'), href: '/finance' },
474
+ { label: t('breadcrumbs.current') },
475
+ ]}
476
+ actions={
477
+ <Button onClick={handleCreate}>
478
+ <Plus className="mr-2 h-4 w-4" />
479
+ {t('newAccount.action')}
480
+ </Button>
481
+ }
482
+ />
483
+
484
+ <NovaContaSheet
485
+ t={t}
486
+ onCreated={refetch}
487
+ open={sheetOpen}
488
+ onOpenChange={setSheetOpen}
489
+ editingAccount={editingAccount}
490
+ onEditingAccountChange={setEditingAccount}
491
+ />
492
+
493
+ <AlertDialog
494
+ open={!!accountIdToDelete}
495
+ onOpenChange={(open) => {
496
+ if (!open) {
497
+ setAccountIdToDelete(null);
498
+ }
499
+ }}
500
+ >
501
+ <AlertDialogContent>
502
+ <AlertDialogHeader>
503
+ <AlertDialogTitle>{deleteDialogTitle}</AlertDialogTitle>
504
+ <AlertDialogDescription>
505
+ {deleteDialogDescription}
506
+ </AlertDialogDescription>
507
+ </AlertDialogHeader>
508
+ <AlertDialogFooter>
509
+ <AlertDialogCancel>{t('common.cancel')}</AlertDialogCancel>
510
+ <AlertDialogAction onClick={handleDelete}>
511
+ {deleteDialogConfirm}
512
+ </AlertDialogAction>
513
+ </AlertDialogFooter>
514
+ </AlertDialogContent>
515
+ </AlertDialog>
516
+
517
+ <div className="grid gap-4 md:grid-cols-2">
518
+ <Card>
519
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
520
+ <CardTitle className="text-sm font-medium">
521
+ {t('cards.totalBalance')}
522
+ </CardTitle>
523
+ <Landmark className="h-4 w-4 text-muted-foreground" />
524
+ </CardHeader>
525
+ <CardContent>
526
+ <div className="text-2xl font-bold">
527
+ <Money value={saldoTotal} />
528
+ </div>
529
+ <p className="text-xs text-muted-foreground">
530
+ {t('cards.activeAccounts', {
531
+ count: accounts.filter((c) => c.ativo).length,
532
+ })}
533
+ </p>
534
+ </CardContent>
535
+ </Card>
536
+ <Card>
537
+ <CardHeader className="flex flex-row items-center justify-between space-y-0 pb-2">
538
+ <CardTitle className="text-sm font-medium">
539
+ {t('cards.reconciledBalance')}
540
+ </CardTitle>
541
+ <RefreshCw className="h-4 w-4 text-muted-foreground" />
542
+ </CardHeader>
543
+ <CardContent>
544
+ <div className="text-2xl font-bold">
545
+ <Money value={saldoConciliadoTotal} />
546
+ </div>
547
+ <p className="text-xs text-muted-foreground">
548
+ {t('cards.difference')}:{' '}
549
+ <Money value={saldoTotal - saldoConciliadoTotal} />
550
+ </p>
551
+ </CardContent>
552
+ </Card>
553
+ </div>
554
+
555
+ <div className="grid gap-4 md:grid-cols-2 lg:grid-cols-3">
556
+ {accounts.map((conta) => {
557
+ const tipo =
558
+ tipoConfig[conta.tipo as keyof typeof tipoConfig] ||
559
+ tipoConfig.corrente;
560
+ const TipoIcon = tipo.icon;
561
+ const diferenca = conta.saldoAtual - conta.saldoConciliado;
562
+
563
+ return (
564
+ <Card key={conta.id} className={!conta.ativo ? 'opacity-60' : ''}>
565
+ <CardHeader>
566
+ <div className="flex items-center justify-between">
567
+ <div className="flex items-center gap-2">
568
+ <div className="flex h-10 w-10 items-center justify-center rounded-lg bg-muted">
569
+ <TipoIcon className="h-5 w-5" />
570
+ </div>
571
+ <div>
572
+ <div className="flex gap-4 items-center">
573
+ <CardTitle className="text-base">
574
+ {conta.banco}
575
+ </CardTitle>
576
+ {conta.descricao && (
577
+ <span className="block text-muted-foreground text-xs">
578
+ {conta.descricao}
579
+ </span>
580
+ )}
581
+ </div>
582
+ <CardDescription className="space-y-0.5">
583
+ {conta.agencia !== '-' && (
584
+ <span className="block">
585
+ {t('accountCard.bankAccount', {
586
+ agency: conta.agencia,
587
+ account: conta.conta,
588
+ })}
589
+ </span>
590
+ )}
591
+ </CardDescription>
592
+ </div>
593
+ </div>
594
+ {!conta.ativo && (
595
+ <Badge variant="outline" className="text-muted-foreground">
596
+ {t('status.inactive')}
597
+ </Badge>
598
+ )}
599
+ </div>
600
+ </CardHeader>
601
+ <CardContent>
602
+ <div className="space-y-3">
603
+ <div>
604
+ <p className="text-sm text-muted-foreground">
605
+ {t('accountCard.currentBalance')}
606
+ </p>
607
+ <p className="text-2xl font-bold">
608
+ <Money value={conta.saldoAtual} />
609
+ </p>
610
+ </div>
611
+ <div className="flex items-center justify-between text-sm">
612
+ <span className="text-muted-foreground">
613
+ {t('accountCard.reconciledBalance')}
614
+ </span>
615
+ <Money value={conta.saldoConciliado} />
616
+ </div>
617
+ {diferenca !== 0 && (
618
+ <div className="flex items-center justify-between text-sm">
619
+ <span className="text-muted-foreground">
620
+ {t('accountCard.difference')}
621
+ </span>
622
+ <span
623
+ className={
624
+ diferenca > 0 ? 'text-green-600' : 'text-red-600'
625
+ }
626
+ >
627
+ <Money value={diferenca} showSign />
628
+ </span>
629
+ </div>
630
+ )}
631
+ <div className="flex flex-wrap gap-2 pt-2">
632
+ <Button
633
+ variant="outline"
634
+ size="sm"
635
+ className="min-w-0 flex-1 basis-[calc(50%-0.25rem)] bg-transparent"
636
+ asChild
637
+ >
638
+ <Link
639
+ href={`/finance/cash-and-banks/statements?bank_account_id=${conta.id}`}
640
+ >
641
+ <Eye className="mr-2 h-4 w-4" />
642
+ {t('accountCard.statement')}
643
+ </Link>
644
+ </Button>
645
+ <Button
646
+ variant="outline"
647
+ size="sm"
648
+ className="min-w-0 flex-1 basis-[calc(50%-0.25rem)] bg-transparent"
649
+ asChild
650
+ >
651
+ <Link
652
+ href={`/finance/cash-and-banks/bank-reconciliation?bank_account_id=${conta.id}`}
653
+ >
654
+ <RefreshCw className="mr-2 h-4 w-4" />
655
+ {t('accountCard.reconcile')}
656
+ </Link>
657
+ </Button>
658
+ <div className="ml-auto flex shrink-0 gap-2">
659
+ <Button variant="outline" size="sm">
660
+ <Upload className="h-4 w-4" />
661
+ </Button>
662
+ <Button
663
+ variant="outline"
664
+ size="sm"
665
+ onClick={() => handleEdit(conta)}
666
+ >
667
+ <Pencil className="h-4 w-4" />
668
+ </Button>
669
+ <Button
670
+ variant="outline"
671
+ size="sm"
672
+ onClick={() => setAccountIdToDelete(conta.id)}
673
+ >
674
+ <Trash2 className="h-4 w-4" />
675
+ </Button>
676
+ </div>
677
+ </div>
678
+ </div>
679
+ </CardContent>
680
+ </Card>
681
+ );
682
+ })}
683
+ </div>
684
+ </Page>
685
+ );
686
+ }