@hed-hog/finance 0.0.257 → 0.0.261

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 (64) hide show
  1. package/dist/dto/create-bank-statement-adjustment.dto.d.ts +8 -0
  2. package/dist/dto/create-bank-statement-adjustment.dto.d.ts.map +1 -0
  3. package/dist/dto/create-bank-statement-adjustment.dto.js +50 -0
  4. package/dist/dto/create-bank-statement-adjustment.dto.js.map +1 -0
  5. package/dist/dto/create-transfer.dto.d.ts +8 -0
  6. package/dist/dto/create-transfer.dto.d.ts.map +1 -0
  7. package/dist/dto/create-transfer.dto.js +52 -0
  8. package/dist/dto/create-transfer.dto.js.map +1 -0
  9. package/dist/dto/register-collection-agreement.dto.d.ts +7 -0
  10. package/dist/dto/register-collection-agreement.dto.d.ts.map +1 -0
  11. package/dist/dto/register-collection-agreement.dto.js +37 -0
  12. package/dist/dto/register-collection-agreement.dto.js.map +1 -0
  13. package/dist/dto/send-collection.dto.d.ts +5 -0
  14. package/dist/dto/send-collection.dto.d.ts.map +1 -0
  15. package/dist/dto/send-collection.dto.js +29 -0
  16. package/dist/dto/send-collection.dto.js.map +1 -0
  17. package/dist/dto/settle-installment.dto.d.ts +1 -0
  18. package/dist/dto/settle-installment.dto.d.ts.map +1 -1
  19. package/dist/dto/settle-installment.dto.js +6 -0
  20. package/dist/dto/settle-installment.dto.js.map +1 -1
  21. package/dist/finance-collections.controller.d.ts +35 -0
  22. package/dist/finance-collections.controller.d.ts.map +1 -0
  23. package/dist/finance-collections.controller.js +65 -0
  24. package/dist/finance-collections.controller.js.map +1 -0
  25. package/dist/finance-data.controller.d.ts +4 -0
  26. package/dist/finance-data.controller.d.ts.map +1 -1
  27. package/dist/finance-installments.controller.d.ts +44 -0
  28. package/dist/finance-installments.controller.d.ts.map +1 -1
  29. package/dist/finance-statements.controller.d.ts +16 -2
  30. package/dist/finance-statements.controller.d.ts.map +1 -1
  31. package/dist/finance-statements.controller.js +34 -6
  32. package/dist/finance-statements.controller.js.map +1 -1
  33. package/dist/finance-transfers.controller.d.ts +23 -0
  34. package/dist/finance-transfers.controller.d.ts.map +1 -0
  35. package/dist/finance-transfers.controller.js +56 -0
  36. package/dist/finance-transfers.controller.js.map +1 -0
  37. package/dist/finance.module.d.ts.map +1 -1
  38. package/dist/finance.module.js +4 -0
  39. package/dist/finance.module.js.map +1 -1
  40. package/dist/finance.service.d.ts +115 -2
  41. package/dist/finance.service.d.ts.map +1 -1
  42. package/dist/finance.service.js +632 -8
  43. package/dist/finance.service.js.map +1 -1
  44. package/dist/index.d.ts +2 -0
  45. package/dist/index.d.ts.map +1 -1
  46. package/dist/index.js +2 -0
  47. package/dist/index.js.map +1 -1
  48. package/hedhog/data/route.yaml +63 -0
  49. package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +643 -440
  50. package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +825 -477
  51. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +367 -43
  52. package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +315 -75
  53. package/package.json +4 -4
  54. package/src/dto/create-bank-statement-adjustment.dto.ts +38 -0
  55. package/src/dto/create-transfer.dto.ts +46 -0
  56. package/src/dto/register-collection-agreement.dto.ts +27 -0
  57. package/src/dto/send-collection.dto.ts +14 -0
  58. package/src/dto/settle-installment.dto.ts +5 -0
  59. package/src/finance-collections.controller.ts +34 -0
  60. package/src/finance-statements.controller.ts +29 -1
  61. package/src/finance-transfers.controller.ts +26 -0
  62. package/src/finance.module.ts +4 -0
  63. package/src/finance.service.ts +775 -5
  64. package/src/index.ts +2 -0
@@ -27,6 +27,7 @@ import {
27
27
  FormMessage,
28
28
  } from '@/components/ui/form';
29
29
  import { Input } from '@/components/ui/input';
30
+ import { InputMoney } from '@/components/ui/input-money';
30
31
  import { Money } from '@/components/ui/money';
31
32
  import {
32
33
  Select,
@@ -53,10 +54,16 @@ import {
53
54
  } from '@/components/ui/table';
54
55
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
55
56
  import { zodResolver } from '@hookform/resolvers/zod';
56
- import { ArrowDownRight, ArrowUpRight, Download, Upload } from 'lucide-react';
57
+ import {
58
+ ArrowDownRight,
59
+ ArrowUpRight,
60
+ Download,
61
+ Plus,
62
+ Upload,
63
+ } from 'lucide-react';
57
64
  import { useTranslations } from 'next-intl';
58
65
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
59
- import { useEffect, useMemo, useState } from 'react';
66
+ import { useEffect, useState } from 'react';
60
67
  import { useForm } from 'react-hook-form';
61
68
  import { z } from 'zod';
62
69
  import { formatarData } from '../../_lib/formatters';
@@ -83,6 +90,15 @@ type Statement = {
83
90
  | 'ajustado';
84
91
  };
85
92
 
93
+ const bankAccountFormSchema = z.object({
94
+ banco: z.string().trim().min(1, 'Banco é obrigatório'),
95
+ agencia: z.string().optional(),
96
+ conta: z.string().optional(),
97
+ tipo: z.string().min(1, 'Tipo é obrigatório'),
98
+ descricao: z.string().optional(),
99
+ saldoInicial: z.number().min(0, 'Saldo inicial inválido'),
100
+ });
101
+
86
102
  const importStatementSchema = z.object({
87
103
  bankAccountId: z.string().trim().min(1, 'Conta bancária é obrigatória'),
88
104
  file: z.instanceof(File, { message: 'Arquivo é obrigatório' }).refine(
@@ -95,20 +111,262 @@ const importStatementSchema = z.object({
95
111
  });
96
112
 
97
113
  type ImportStatementFormValues = z.infer<typeof importStatementSchema>;
114
+ type BankAccountFormValues = z.infer<typeof bankAccountFormSchema>;
115
+
116
+ function NovaContaBancariaSheet({
117
+ open,
118
+ onOpenChange,
119
+ onCreated,
120
+ }: {
121
+ open: boolean;
122
+ onOpenChange: (open: boolean) => void;
123
+ onCreated: (createdBankAccountId?: string) => Promise<void> | void;
124
+ }) {
125
+ const tBank = useTranslations('finance.BankAccountsPage');
126
+ const { request, showToastHandler } = useApp();
127
+
128
+ const form = useForm<BankAccountFormValues>({
129
+ resolver: zodResolver(bankAccountFormSchema),
130
+ defaultValues: {
131
+ banco: '',
132
+ agencia: '',
133
+ conta: '',
134
+ tipo: '',
135
+ descricao: '',
136
+ saldoInicial: 0,
137
+ },
138
+ });
139
+
140
+ useEffect(() => {
141
+ if (!open) {
142
+ return;
143
+ }
144
+
145
+ form.reset({
146
+ banco: '',
147
+ agencia: '',
148
+ conta: '',
149
+ tipo: '',
150
+ descricao: '',
151
+ saldoInicial: 0,
152
+ });
153
+ }, [form, open]);
154
+
155
+ const handleSubmit = async (values: BankAccountFormValues) => {
156
+ try {
157
+ const response = await request<{ id?: string | number }>({
158
+ url: '/finance/bank-accounts',
159
+ method: 'POST',
160
+ data: {
161
+ bank: values.banco,
162
+ branch: values.agencia || undefined,
163
+ account: values.conta || undefined,
164
+ type: values.tipo,
165
+ description: values.descricao?.trim() || undefined,
166
+ initial_balance: values.saldoInicial,
167
+ },
168
+ });
169
+
170
+ const createdBankAccountId = response?.data?.id;
171
+
172
+ await onCreated(
173
+ createdBankAccountId !== undefined
174
+ ? String(createdBankAccountId)
175
+ : undefined
176
+ );
177
+ onOpenChange(false);
178
+ showToastHandler?.(
179
+ 'success',
180
+ tBank.has('messages.createSuccess')
181
+ ? tBank('messages.createSuccess')
182
+ : 'Conta bancária cadastrada com sucesso'
183
+ );
184
+ } catch {
185
+ showToastHandler?.(
186
+ 'error',
187
+ tBank.has('messages.createError')
188
+ ? tBank('messages.createError')
189
+ : 'Erro ao cadastrar conta bancária'
190
+ );
191
+ }
192
+ };
193
+
194
+ return (
195
+ <Sheet open={open} onOpenChange={onOpenChange}>
196
+ <SheetContent className="w-full sm:max-w-lg overflow-y-auto">
197
+ <SheetHeader>
198
+ <SheetTitle>{tBank('newAccount.title')}</SheetTitle>
199
+ <SheetDescription>{tBank('newAccount.description')}</SheetDescription>
200
+ </SheetHeader>
201
+
202
+ <Form {...form}>
203
+ <form
204
+ className="space-y-4 px-4"
205
+ onSubmit={form.handleSubmit(handleSubmit)}
206
+ >
207
+ <FormField
208
+ control={form.control}
209
+ name="banco"
210
+ render={({ field }) => (
211
+ <FormItem>
212
+ <FormLabel>{tBank('fields.bank')}</FormLabel>
213
+ <FormControl>
214
+ <Input
215
+ placeholder={tBank('fields.bankPlaceholder')}
216
+ {...field}
217
+ />
218
+ </FormControl>
219
+ <FormMessage />
220
+ </FormItem>
221
+ )}
222
+ />
223
+
224
+ <div className="grid grid-cols-2 gap-4">
225
+ <FormField
226
+ control={form.control}
227
+ name="agencia"
228
+ render={({ field }) => (
229
+ <FormItem>
230
+ <FormLabel>{tBank('fields.branch')}</FormLabel>
231
+ <FormControl>
232
+ <Input
233
+ placeholder="0000"
234
+ {...field}
235
+ value={field.value || ''}
236
+ />
237
+ </FormControl>
238
+ <FormMessage />
239
+ </FormItem>
240
+ )}
241
+ />
242
+
243
+ <FormField
244
+ control={form.control}
245
+ name="conta"
246
+ render={({ field }) => (
247
+ <FormItem>
248
+ <FormLabel>{tBank('fields.account')}</FormLabel>
249
+ <FormControl>
250
+ <Input
251
+ placeholder="00000-0"
252
+ {...field}
253
+ value={field.value || ''}
254
+ />
255
+ </FormControl>
256
+ <FormMessage />
257
+ </FormItem>
258
+ )}
259
+ />
260
+ </div>
261
+
262
+ <div className="grid grid-cols-2 gap-4">
263
+ <FormField
264
+ control={form.control}
265
+ name="tipo"
266
+ render={({ field }) => (
267
+ <FormItem>
268
+ <FormLabel>{tBank('fields.type')}</FormLabel>
269
+ <Select value={field.value} onValueChange={field.onChange}>
270
+ <FormControl>
271
+ <SelectTrigger className="w-full">
272
+ <SelectValue placeholder={tBank('common.select')} />
273
+ </SelectTrigger>
274
+ </FormControl>
275
+ <SelectContent>
276
+ <SelectItem value="corrente">
277
+ {tBank('types.corrente')}
278
+ </SelectItem>
279
+ <SelectItem value="poupanca">
280
+ {tBank('types.poupanca')}
281
+ </SelectItem>
282
+ <SelectItem value="investimento">
283
+ {tBank('types.investimento')}
284
+ </SelectItem>
285
+ <SelectItem value="caixa">
286
+ {tBank('types.caixa')}
287
+ </SelectItem>
288
+ </SelectContent>
289
+ </Select>
290
+ <FormMessage />
291
+ </FormItem>
292
+ )}
293
+ />
294
+
295
+ <FormField
296
+ control={form.control}
297
+ name="saldoInicial"
298
+ render={({ field }) => (
299
+ <FormItem>
300
+ <FormLabel>{tBank('fields.initialBalance')}</FormLabel>
301
+ <FormControl>
302
+ <InputMoney
303
+ ref={field.ref}
304
+ name={field.name}
305
+ value={field.value}
306
+ onBlur={field.onBlur}
307
+ onValueChange={(value) => field.onChange(value ?? 0)}
308
+ placeholder="0,00"
309
+ />
310
+ </FormControl>
311
+ <FormMessage />
312
+ </FormItem>
313
+ )}
314
+ />
315
+ </div>
316
+
317
+ <FormField
318
+ control={form.control}
319
+ name="descricao"
320
+ render={({ field }) => (
321
+ <FormItem>
322
+ <FormLabel>{tBank('fields.description')}</FormLabel>
323
+ <FormControl>
324
+ <Input
325
+ placeholder={tBank('fields.descriptionPlaceholder')}
326
+ {...field}
327
+ value={field.value || ''}
328
+ />
329
+ </FormControl>
330
+ <FormMessage />
331
+ </FormItem>
332
+ )}
333
+ />
334
+
335
+ <div className="flex justify-end gap-2 pt-2">
336
+ <Button
337
+ type="button"
338
+ variant="outline"
339
+ onClick={() => onOpenChange(false)}
340
+ >
341
+ {tBank('common.cancel')}
342
+ </Button>
343
+ <Button type="submit" disabled={form.formState.isSubmitting}>
344
+ {tBank('common.save')}
345
+ </Button>
346
+ </div>
347
+ </form>
348
+ </Form>
349
+ </SheetContent>
350
+ </Sheet>
351
+ );
352
+ }
98
353
 
99
354
  function ImportarExtratoSheet({
100
355
  contasBancarias,
101
356
  t,
102
357
  defaultBankAccountId,
103
358
  onImported,
359
+ onBankAccountCreated,
104
360
  }: {
105
361
  contasBancarias: BankAccount[];
106
362
  t: ReturnType<typeof useTranslations>;
107
363
  defaultBankAccountId?: string;
108
364
  onImported: () => Promise<any> | void;
365
+ onBankAccountCreated: (createdBankAccountId?: string) => Promise<void> | void;
109
366
  }) {
110
367
  const { request, showToastHandler } = useApp();
111
368
  const [open, setOpen] = useState(false);
369
+ const [openNovaContaSheet, setOpenNovaContaSheet] = useState(false);
112
370
 
113
371
  const form = useForm<ImportStatementFormValues>({
114
372
  resolver: zodResolver(importStatementSchema),
@@ -172,27 +430,58 @@ function ImportarExtratoSheet({
172
430
  render={({ field }) => (
173
431
  <FormItem>
174
432
  <FormLabel>{t('importDialog.bankAccount')}</FormLabel>
175
- <Select value={field.value} onValueChange={field.onChange}>
176
- <FormControl>
177
- <SelectTrigger className="w-full">
178
- <SelectValue
179
- placeholder={t('importDialog.selectAccount')}
180
- />
181
- </SelectTrigger>
182
- </FormControl>
183
- <SelectContent>
184
- {contasBancarias.map((conta) => (
185
- <SelectItem key={conta.id} value={conta.id}>
186
- {conta.banco} - {conta.descricao}
187
- </SelectItem>
188
- ))}
189
- </SelectContent>
190
- </Select>
433
+ <div className="flex items-center gap-2">
434
+ <div className="flex-1">
435
+ <Select
436
+ value={field.value}
437
+ onValueChange={field.onChange}
438
+ >
439
+ <FormControl>
440
+ <SelectTrigger className="w-full">
441
+ <SelectValue
442
+ placeholder={t('importDialog.selectAccount')}
443
+ />
444
+ </SelectTrigger>
445
+ </FormControl>
446
+ <SelectContent>
447
+ {contasBancarias.map((conta) => (
448
+ <SelectItem key={conta.id} value={conta.id}>
449
+ {conta.banco} - {conta.descricao}
450
+ </SelectItem>
451
+ ))}
452
+ </SelectContent>
453
+ </Select>
454
+ </div>
455
+ <Button
456
+ type="button"
457
+ variant="outline"
458
+ size="icon"
459
+ onClick={() => setOpenNovaContaSheet(true)}
460
+ aria-label="Nova conta bancária"
461
+ >
462
+ <Plus className="h-4 w-4" />
463
+ </Button>
464
+ </div>
191
465
  <FormMessage />
192
466
  </FormItem>
193
467
  )}
194
468
  />
195
469
 
470
+ <NovaContaBancariaSheet
471
+ open={openNovaContaSheet}
472
+ onOpenChange={setOpenNovaContaSheet}
473
+ onCreated={async (createdBankAccountId) => {
474
+ await onBankAccountCreated(createdBankAccountId);
475
+
476
+ if (createdBankAccountId) {
477
+ form.setValue('bankAccountId', createdBankAccountId, {
478
+ shouldDirty: true,
479
+ shouldValidate: true,
480
+ });
481
+ }
482
+ }}
483
+ />
484
+
196
485
  <FormField
197
486
  control={form.control}
198
487
  name="file"
@@ -246,20 +535,46 @@ export default function ExtratosPage() {
246
535
 
247
536
  const [contaFilter, setContaFilter] = useState<string>('');
248
537
  const [search, setSearch] = useState('');
538
+ const [debouncedSearch, setDebouncedSearch] = useState('');
249
539
  const [extratoSelecionado, setExtratoSelecionado] =
250
540
  useState<Statement | null>(null);
251
541
 
252
- const { data: contasBancarias = [] } = useQuery<BankAccount[]>({
253
- queryKey: ['finance-bank-accounts'],
254
- queryFn: async () => {
255
- const response = await request({
256
- url: '/finance/bank-accounts',
257
- method: 'GET',
258
- });
542
+ useEffect(() => {
543
+ const timeoutId = window.setTimeout(() => {
544
+ setDebouncedSearch(search);
545
+ }, 300);
546
+
547
+ return () => {
548
+ window.clearTimeout(timeoutId);
549
+ };
550
+ }, [search]);
551
+
552
+ const { data: contasBancarias = [], refetch: refetchContasBancarias } =
553
+ useQuery<BankAccount[]>({
554
+ queryKey: ['finance-bank-accounts'],
555
+ queryFn: async () => {
556
+ const response = await request({
557
+ url: '/finance/bank-accounts',
558
+ method: 'GET',
559
+ });
560
+
561
+ return (response?.data || []) as BankAccount[];
562
+ },
563
+ });
259
564
 
260
- return (response?.data || []) as BankAccount[];
261
- },
262
- });
565
+ const handleBankAccountCreated = async (createdBankAccountId?: string) => {
566
+ await refetchContasBancarias();
567
+
568
+ if (!createdBankAccountId) {
569
+ return;
570
+ }
571
+
572
+ setContaFilter(createdBankAccountId);
573
+
574
+ const params = new URLSearchParams(searchParams.toString());
575
+ params.set('bank_account_id', createdBankAccountId);
576
+ router.replace(`${pathname}?${params.toString()}`);
577
+ };
263
578
 
264
579
  useEffect(() => {
265
580
  const firstAccount = contasBancarias[0];
@@ -297,14 +612,22 @@ export default function ExtratosPage() {
297
612
  const { data: extratos = [], refetch: refetchExtratos } = useQuery<
298
613
  Statement[]
299
614
  >({
300
- queryKey: ['finance-statements', contaFilter],
615
+ queryKey: ['finance-statements', contaFilter, debouncedSearch],
301
616
  queryFn: async () => {
302
617
  if (!contaFilter) {
303
618
  return [];
304
619
  }
305
620
 
621
+ const params = new URLSearchParams();
622
+ params.set('bank_account_id', contaFilter);
623
+
624
+ const trimmedSearch = debouncedSearch.trim();
625
+ if (trimmedSearch) {
626
+ params.set('search', trimmedSearch);
627
+ }
628
+
306
629
  const response = await request({
307
- url: `/finance/statements?bank_account_id=${contaFilter}`,
630
+ url: `/finance/statements?${params.toString()}`,
308
631
  method: 'GET',
309
632
  });
310
633
 
@@ -312,24 +635,16 @@ export default function ExtratosPage() {
312
635
  },
313
636
  });
314
637
 
315
- const filteredExtratos = useMemo(
316
- () =>
317
- extratos.filter((extrato) =>
318
- extrato.descricao.toLowerCase().includes(search.toLowerCase())
319
- ),
320
- [extratos, search]
321
- );
322
-
323
638
  const conta = contasBancarias.find((item) => item.id === contaFilter);
324
639
  const contaExtratoSelecionado = extratoSelecionado
325
640
  ? contasBancarias.find(
326
641
  (item) => item.id === extratoSelecionado.contaBancariaId
327
642
  )
328
643
  : undefined;
329
- const totalEntradas = filteredExtratos
644
+ const totalEntradas = extratos
330
645
  .filter((e) => e.tipo === 'entrada')
331
646
  .reduce((acc, e) => acc + e.valor, 0);
332
- const totalSaidas = filteredExtratos
647
+ const totalSaidas = extratos
333
648
  .filter((e) => e.tipo === 'saida')
334
649
  .reduce((acc, e) => acc + e.valor, 0);
335
650
 
@@ -340,8 +655,16 @@ export default function ExtratosPage() {
340
655
  }
341
656
 
342
657
  try {
658
+ const params = new URLSearchParams();
659
+ params.set('bank_account_id', contaFilter);
660
+
661
+ const trimmedSearch = search.trim();
662
+ if (trimmedSearch) {
663
+ params.set('search', trimmedSearch);
664
+ }
665
+
343
666
  const response = await request<Blob>({
344
- url: `/finance/statements/export?bank_account_id=${contaFilter}`,
667
+ url: `/finance/statements/export?${params.toString()}`,
345
668
  method: 'GET',
346
669
  responseType: 'blob',
347
670
  });
@@ -395,6 +718,7 @@ export default function ExtratosPage() {
395
718
  t={t}
396
719
  defaultBankAccountId={contaFilter}
397
720
  onImported={refetchExtratos}
721
+ onBankAccountCreated={handleBankAccountCreated}
398
722
  />
399
723
  </div>
400
724
  }
@@ -477,7 +801,7 @@ export default function ExtratosPage() {
477
801
  <CardHeader>
478
802
  <CardTitle>{t('table.title')}</CardTitle>
479
803
  <CardDescription>
480
- {t('table.foundTransactions', { count: filteredExtratos.length })}
804
+ {t('table.foundTransactions', { count: extratos.length })}
481
805
  </CardDescription>
482
806
  </CardHeader>
483
807
  <CardContent>
@@ -501,7 +825,7 @@ export default function ExtratosPage() {
501
825
  </TableRow>
502
826
  </TableHeader>
503
827
  <TableBody>
504
- {filteredExtratos.map((extrato) => (
828
+ {extratos.map((extrato) => (
505
829
  <TableRow
506
830
  key={extrato.id}
507
831
  className="cursor-pointer"