@hed-hog/finance 0.0.238 → 0.0.240

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 (53) hide show
  1. package/README.md +1 -22
  2. package/dist/dto/reject-title.dto.d.ts +4 -0
  3. package/dist/dto/reject-title.dto.d.ts.map +1 -0
  4. package/dist/dto/reject-title.dto.js +22 -0
  5. package/dist/dto/reject-title.dto.js.map +1 -0
  6. package/dist/dto/reverse-settlement.dto.d.ts +4 -0
  7. package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
  8. package/dist/dto/reverse-settlement.dto.js +22 -0
  9. package/dist/dto/reverse-settlement.dto.js.map +1 -0
  10. package/dist/dto/settle-installment.dto.d.ts +12 -0
  11. package/dist/dto/settle-installment.dto.d.ts.map +1 -0
  12. package/dist/dto/settle-installment.dto.js +71 -0
  13. package/dist/dto/settle-installment.dto.js.map +1 -0
  14. package/dist/finance-data.controller.d.ts +13 -5
  15. package/dist/finance-data.controller.d.ts.map +1 -1
  16. package/dist/finance-installments.controller.d.ts +380 -12
  17. package/dist/finance-installments.controller.d.ts.map +1 -1
  18. package/dist/finance-installments.controller.js +144 -0
  19. package/dist/finance-installments.controller.js.map +1 -1
  20. package/dist/finance-statements.controller.d.ts +8 -0
  21. package/dist/finance-statements.controller.d.ts.map +1 -1
  22. package/dist/finance-statements.controller.js +40 -0
  23. package/dist/finance-statements.controller.js.map +1 -1
  24. package/dist/finance.module.d.ts.map +1 -1
  25. package/dist/finance.module.js +1 -0
  26. package/dist/finance.module.js.map +1 -1
  27. package/dist/finance.service.d.ts +435 -19
  28. package/dist/finance.service.d.ts.map +1 -1
  29. package/dist/finance.service.js +1286 -80
  30. package/dist/finance.service.js.map +1 -1
  31. package/hedhog/data/route.yaml +117 -0
  32. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
  33. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +434 -7
  34. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1172 -25
  35. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +396 -49
  36. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +430 -14
  37. package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +212 -60
  38. package/hedhog/frontend/messages/en.json +1 -0
  39. package/hedhog/frontend/messages/pt.json +1 -0
  40. package/hedhog/query/0_constraints.sql +2 -0
  41. package/hedhog/query/constraints.sql +86 -0
  42. package/hedhog/table/bank_account.yaml +0 -8
  43. package/hedhog/table/financial_title.yaml +1 -9
  44. package/hedhog/table/settlement.yaml +0 -8
  45. package/package.json +6 -6
  46. package/src/dto/reject-title.dto.ts +7 -0
  47. package/src/dto/reverse-settlement.dto.ts +7 -0
  48. package/src/dto/settle-installment.dto.ts +55 -0
  49. package/src/finance-installments.controller.ts +172 -10
  50. package/src/finance-statements.controller.ts +61 -2
  51. package/src/finance.module.ts +2 -1
  52. package/src/finance.service.ts +1887 -106
  53. package/hedhog/table/branch.yaml +0 -18
@@ -1,6 +1,17 @@
1
1
  'use client';
2
2
 
3
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
+ AlertDialogTrigger,
14
+ } from '@/components/ui/alert-dialog';
4
15
  import { AuditTimeline } from '@/components/ui/audit-timeline';
5
16
  import { Button } from '@/components/ui/button';
6
17
  import {
@@ -10,6 +21,14 @@ import {
10
21
  CardHeader,
11
22
  CardTitle,
12
23
  } from '@/components/ui/card';
24
+ import {
25
+ Dialog,
26
+ DialogContent,
27
+ DialogDescription,
28
+ DialogFooter,
29
+ DialogHeader,
30
+ DialogTitle,
31
+ } from '@/components/ui/dialog';
13
32
  import {
14
33
  DropdownMenu,
15
34
  DropdownMenuContent,
@@ -17,7 +36,24 @@ import {
17
36
  DropdownMenuSeparator,
18
37
  DropdownMenuTrigger,
19
38
  } from '@/components/ui/dropdown-menu';
39
+ import {
40
+ Form,
41
+ FormControl,
42
+ FormField,
43
+ FormItem,
44
+ FormLabel,
45
+ FormMessage,
46
+ } from '@/components/ui/form';
47
+ import { Input } from '@/components/ui/input';
48
+ import { InputMoney } from '@/components/ui/input-money';
20
49
  import { Money } from '@/components/ui/money';
50
+ import {
51
+ Select,
52
+ SelectContent,
53
+ SelectItem,
54
+ SelectTrigger,
55
+ SelectValue,
56
+ } from '@/components/ui/select';
21
57
  import { StatusBadge } from '@/components/ui/status-badge';
22
58
  import {
23
59
  Table,
@@ -30,6 +66,7 @@ import {
30
66
  import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
31
67
  import { TagSelectorSheet } from '@/components/ui/tag-selector-sheet';
32
68
  import { useApp } from '@hed-hog/next-app-provider';
69
+ import { zodResolver } from '@hookform/resolvers/zod';
33
70
  import {
34
71
  CheckCircle,
35
72
  Download,
@@ -41,17 +78,28 @@ import {
41
78
  } from 'lucide-react';
42
79
  import { useTranslations } from 'next-intl';
43
80
  import Link from 'next/link';
44
- import { useParams } from 'next/navigation';
81
+ import { useParams, useRouter } from 'next/navigation';
45
82
  import { useEffect, useState } from 'react';
83
+ import { useForm } from 'react-hook-form';
84
+ import { z } from 'zod';
46
85
  import { formatarData } from '../../../_lib/formatters';
47
86
  import { useFinanceData } from '../../../_lib/use-finance-data';
48
87
 
88
+ const settleSchema = z.object({
89
+ installmentId: z.string().min(1, 'Parcela obrigatória'),
90
+ amount: z.number().min(0.01, 'Valor deve ser maior que zero'),
91
+ description: z.string().optional(),
92
+ });
93
+
94
+ type SettleFormValues = z.infer<typeof settleSchema>;
95
+
49
96
  export default function TituloDetalhePage() {
50
97
  const t = useTranslations('finance.PayableInstallmentDetailPage');
51
98
  const { request, showToastHandler } = useApp();
99
+ const router = useRouter();
52
100
  const params = useParams<{ id: string }>();
53
101
  const id = params?.id;
54
- const { data } = useFinanceData();
102
+ const { data, refetch } = useFinanceData();
55
103
  const {
56
104
  titulosPagar,
57
105
  pessoas,
@@ -64,9 +112,33 @@ export default function TituloDetalhePage() {
64
112
 
65
113
  const titulo = titulosPagar.find((t) => t.id === id);
66
114
 
115
+ const settleCandidates = (titulo?.parcelas || []).filter(
116
+ (parcela: any) =>
117
+ parcela.status === 'aberto' ||
118
+ parcela.status === 'parcial' ||
119
+ parcela.status === 'vencido'
120
+ );
121
+
122
+ const settleForm = useForm<SettleFormValues>({
123
+ resolver: zodResolver(settleSchema),
124
+ defaultValues: {
125
+ installmentId: settleCandidates[0]?.id || '',
126
+ amount: Number(settleCandidates[0]?.valorAberto || 0),
127
+ description: '',
128
+ },
129
+ });
130
+
67
131
  const [availableTags, setAvailableTags] = useState<any[]>([]);
68
132
  const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
69
133
  const [isCreatingTag, setIsCreatingTag] = useState(false);
134
+ const [isApproving, setIsApproving] = useState(false);
135
+ const [isSettling, setIsSettling] = useState(false);
136
+ const [isCanceling, setIsCanceling] = useState(false);
137
+ const [isSettleDialogOpen, setIsSettleDialogOpen] = useState(false);
138
+ const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
139
+ const [reversingSettlementId, setReversingSettlementId] = useState<
140
+ string | null
141
+ >(null);
70
142
 
71
143
  useEffect(() => {
72
144
  setAvailableTags(tags || []);
@@ -80,6 +152,27 @@ export default function TituloDetalhePage() {
80
152
  setSelectedTagIds(Array.isArray(titulo.tags) ? titulo.tags : []);
81
153
  }, [titulo]);
82
154
 
155
+ useEffect(() => {
156
+ const firstCandidate = settleCandidates[0];
157
+ const nextInstallmentId = firstCandidate?.id || '';
158
+ const nextAmount = Number(firstCandidate?.valorAberto || 0);
159
+ const currentInstallmentId = settleForm.getValues('installmentId') || '';
160
+ const currentAmount = Number(settleForm.getValues('amount') || 0);
161
+
162
+ if (
163
+ currentInstallmentId === nextInstallmentId &&
164
+ currentAmount === nextAmount
165
+ ) {
166
+ return;
167
+ }
168
+
169
+ settleForm.reset({
170
+ installmentId: nextInstallmentId,
171
+ amount: nextAmount,
172
+ description: '',
173
+ });
174
+ }, [settleForm, settleCandidates]);
175
+
83
176
  if (!titulo) {
84
177
  return (
85
178
  <div className="space-y-6">
@@ -267,6 +360,130 @@ export default function TituloDetalhePage() {
267
360
  }
268
361
  };
269
362
 
363
+ const canApprove = titulo.status === 'rascunho';
364
+ const canEdit = titulo.status === 'rascunho';
365
+ const canSettle = ['aberto', 'parcial', 'vencido'].includes(titulo.status);
366
+ const canCancel = !['cancelado', 'liquidado'].includes(titulo.status);
367
+
368
+ const getErrorMessage = (error: any, fallback: string) => {
369
+ const message = error?.response?.data?.message;
370
+
371
+ if (Array.isArray(message)) {
372
+ return message.join(', ');
373
+ }
374
+
375
+ if (typeof message === 'string' && message.trim()) {
376
+ return message;
377
+ }
378
+
379
+ return fallback;
380
+ };
381
+
382
+ const handleApprove = async () => {
383
+ if (!canApprove || isApproving) {
384
+ return;
385
+ }
386
+
387
+ setIsApproving(true);
388
+ try {
389
+ await request({
390
+ url: `/finance/accounts-payable/installments/${titulo.id}/approve`,
391
+ method: 'PATCH',
392
+ });
393
+
394
+ await refetch();
395
+ showToastHandler?.('success', 'Título aprovado com sucesso');
396
+ } catch (error) {
397
+ showToastHandler?.(
398
+ 'error',
399
+ getErrorMessage(error, 'Não foi possível aprovar o título')
400
+ );
401
+ } finally {
402
+ setIsApproving(false);
403
+ }
404
+ };
405
+
406
+ const handleSettle = async (values: SettleFormValues) => {
407
+ if (!canSettle || isSettling) {
408
+ return;
409
+ }
410
+
411
+ setIsSettling(true);
412
+ try {
413
+ await request({
414
+ url: `/finance/accounts-payable/installments/${titulo.id}/settlements`,
415
+ method: 'POST',
416
+ data: {
417
+ installment_id: Number(values.installmentId),
418
+ amount: values.amount,
419
+ description: values.description?.trim() || undefined,
420
+ },
421
+ });
422
+
423
+ await refetch();
424
+ setIsSettleDialogOpen(false);
425
+ showToastHandler?.('success', 'Baixa registrada com sucesso');
426
+ } catch (error) {
427
+ showToastHandler?.(
428
+ 'error',
429
+ getErrorMessage(error, 'Não foi possível registrar a baixa')
430
+ );
431
+ } finally {
432
+ setIsSettling(false);
433
+ }
434
+ };
435
+
436
+ const handleReverseSettlement = async (settlementId: string) => {
437
+ if (!settlementId || reversingSettlementId) {
438
+ return;
439
+ }
440
+
441
+ setReversingSettlementId(settlementId);
442
+ try {
443
+ await request({
444
+ url: `/finance/accounts-payable/installments/${titulo.id}/settlements/${settlementId}/reverse`,
445
+ method: 'PATCH',
446
+ data: {},
447
+ });
448
+
449
+ await refetch();
450
+ showToastHandler?.('success', 'Estorno realizado com sucesso');
451
+ } catch (error) {
452
+ showToastHandler?.(
453
+ 'error',
454
+ getErrorMessage(error, 'Não foi possível estornar a liquidação')
455
+ );
456
+ } finally {
457
+ setReversingSettlementId(null);
458
+ }
459
+ };
460
+
461
+ const handleCancel = async () => {
462
+ if (!canCancel || isCanceling) {
463
+ return;
464
+ }
465
+
466
+ setIsCanceling(true);
467
+ try {
468
+ await request({
469
+ url: `/finance/accounts-payable/installments/${titulo.id}/cancel`,
470
+ method: 'PATCH',
471
+ data: {},
472
+ });
473
+
474
+ await refetch();
475
+ setIsCancelDialogOpen(false);
476
+ showToastHandler?.('success', 'Título cancelado com sucesso');
477
+ } catch (error) {
478
+ showToastHandler?.(
479
+ 'error',
480
+ getErrorMessage(error, 'Não foi possível cancelar o título')
481
+ );
482
+ } finally {
483
+ setIsCanceling(false);
484
+ }
485
+ };
486
+
270
487
  return (
271
488
  <Page>
272
489
  <PageHeader
@@ -291,29 +508,187 @@ export default function TituloDetalhePage() {
291
508
  </Button>
292
509
  </DropdownMenuTrigger>
293
510
  <DropdownMenuContent align="end">
294
- <DropdownMenuItem>
511
+ <DropdownMenuItem
512
+ disabled={!canEdit}
513
+ onClick={() =>
514
+ router.push(
515
+ `/finance/accounts-payable/installments?editId=${titulo.id}`
516
+ )
517
+ }
518
+ >
295
519
  <Edit className="mr-2 h-4 w-4" />
296
520
  {t('actions.edit')}
297
521
  </DropdownMenuItem>
298
- <DropdownMenuItem>
522
+ <DropdownMenuItem
523
+ disabled={!canApprove || isApproving}
524
+ onClick={() => void handleApprove()}
525
+ >
299
526
  <CheckCircle className="mr-2 h-4 w-4" />
300
527
  {t('actions.approve')}
301
528
  </DropdownMenuItem>
302
- <DropdownMenuItem>
529
+ <DropdownMenuItem
530
+ disabled={!canSettle || settleCandidates.length === 0}
531
+ onClick={() => setIsSettleDialogOpen(true)}
532
+ >
303
533
  <Download className="mr-2 h-4 w-4" />
304
534
  {t('actions.settle')}
305
535
  </DropdownMenuItem>
306
- <DropdownMenuItem>
536
+ <DropdownMenuItem disabled>
307
537
  <Undo className="mr-2 h-4 w-4" />
308
538
  {t('actions.reverse')}
309
539
  </DropdownMenuItem>
310
540
  <DropdownMenuSeparator />
311
- <DropdownMenuItem className="text-destructive">
541
+ <DropdownMenuItem
542
+ className="text-destructive"
543
+ disabled={!canCancel || isCanceling}
544
+ onClick={() => setIsCancelDialogOpen(true)}
545
+ >
312
546
  <XCircle className="mr-2 h-4 w-4" />
313
547
  {t('actions.cancel')}
314
548
  </DropdownMenuItem>
315
549
  </DropdownMenuContent>
316
550
  </DropdownMenu>
551
+
552
+ <AlertDialog
553
+ open={isCancelDialogOpen}
554
+ onOpenChange={setIsCancelDialogOpen}
555
+ >
556
+ <AlertDialogContent>
557
+ <AlertDialogHeader>
558
+ <AlertDialogTitle>Confirmar cancelamento</AlertDialogTitle>
559
+ <AlertDialogDescription>
560
+ Essa ação altera o título para cancelado e não remove os
561
+ registros de auditoria.
562
+ </AlertDialogDescription>
563
+ </AlertDialogHeader>
564
+ <AlertDialogFooter>
565
+ <AlertDialogCancel disabled={isCanceling}>
566
+ Cancelar
567
+ </AlertDialogCancel>
568
+ <AlertDialogAction
569
+ disabled={isCanceling}
570
+ onClick={() => void handleCancel()}
571
+ >
572
+ Confirmar cancelamento
573
+ </AlertDialogAction>
574
+ </AlertDialogFooter>
575
+ </AlertDialogContent>
576
+ </AlertDialog>
577
+
578
+ <Dialog
579
+ open={isSettleDialogOpen}
580
+ onOpenChange={setIsSettleDialogOpen}
581
+ >
582
+ <DialogContent>
583
+ <DialogHeader>
584
+ <DialogTitle>Registrar baixa</DialogTitle>
585
+ <DialogDescription>
586
+ Informe a parcela e o valor da baixa. O backend valida os
587
+ estados e limites de valor automaticamente.
588
+ </DialogDescription>
589
+ </DialogHeader>
590
+
591
+ <Form {...settleForm}>
592
+ <form
593
+ className="space-y-4"
594
+ onSubmit={settleForm.handleSubmit(handleSettle)}
595
+ >
596
+ <FormField
597
+ control={settleForm.control}
598
+ name="installmentId"
599
+ render={({ field }) => (
600
+ <FormItem>
601
+ <FormLabel>Parcela</FormLabel>
602
+ <Select
603
+ value={field.value}
604
+ onValueChange={(value) => {
605
+ field.onChange(value);
606
+
607
+ const selected = settleCandidates.find(
608
+ (parcela: any) => parcela.id === value
609
+ );
610
+
611
+ if (selected) {
612
+ settleForm.setValue(
613
+ 'amount',
614
+ Number(selected.valorAberto || 0),
615
+ { shouldValidate: true }
616
+ );
617
+ }
618
+ }}
619
+ >
620
+ <FormControl>
621
+ <SelectTrigger>
622
+ <SelectValue placeholder="Selecione" />
623
+ </SelectTrigger>
624
+ </FormControl>
625
+ <SelectContent>
626
+ {settleCandidates.map((parcela: any) => (
627
+ <SelectItem key={parcela.id} value={parcela.id}>
628
+ Parcela {parcela.numero} - em aberto:{' '}
629
+ {new Intl.NumberFormat('pt-BR', {
630
+ style: 'currency',
631
+ currency: 'BRL',
632
+ }).format(Number(parcela.valorAberto || 0))}
633
+ </SelectItem>
634
+ ))}
635
+ </SelectContent>
636
+ </Select>
637
+ <FormMessage />
638
+ </FormItem>
639
+ )}
640
+ />
641
+
642
+ <FormField
643
+ control={settleForm.control}
644
+ name="amount"
645
+ render={({ field }) => (
646
+ <FormItem>
647
+ <FormLabel>Valor</FormLabel>
648
+ <FormControl>
649
+ <InputMoney
650
+ value={Number(field.value || 0)}
651
+ onValueChange={(value) => {
652
+ field.onChange(Number(value || 0));
653
+ }}
654
+ />
655
+ </FormControl>
656
+ <FormMessage />
657
+ </FormItem>
658
+ )}
659
+ />
660
+
661
+ <FormField
662
+ control={settleForm.control}
663
+ name="description"
664
+ render={({ field }) => (
665
+ <FormItem>
666
+ <FormLabel>Descrição (opcional)</FormLabel>
667
+ <FormControl>
668
+ <Input {...field} value={field.value || ''} />
669
+ </FormControl>
670
+ <FormMessage />
671
+ </FormItem>
672
+ )}
673
+ />
674
+
675
+ <DialogFooter>
676
+ <Button
677
+ type="button"
678
+ variant="outline"
679
+ disabled={isSettling}
680
+ onClick={() => setIsSettleDialogOpen(false)}
681
+ >
682
+ Cancelar
683
+ </Button>
684
+ <Button type="submit" disabled={isSettling}>
685
+ Confirmar baixa
686
+ </Button>
687
+ </DialogFooter>
688
+ </form>
689
+ </Form>
690
+ </DialogContent>
691
+ </Dialog>
317
692
  </div>
318
693
  }
319
694
  />
@@ -358,6 +733,14 @@ export default function TituloDetalhePage() {
358
733
  <Money value={titulo.valorTotal} />
359
734
  </dd>
360
735
  </div>
736
+ <div>
737
+ <dt className="text-sm font-medium text-muted-foreground">
738
+ {t('documentData.status')}
739
+ </dt>
740
+ <dd className="mt-1">
741
+ <StatusBadge status={titulo.status} />
742
+ </dd>
743
+ </div>
361
744
  <div>
362
745
  <dt className="text-sm font-medium text-muted-foreground">
363
746
  {t('documentData.category')}
@@ -522,6 +905,7 @@ export default function TituloDetalhePage() {
522
905
  </TableHead>
523
906
  <TableHead>{t('settlementsTable.account')}</TableHead>
524
907
  <TableHead>{t('settlementsTable.method')}</TableHead>
908
+ <TableHead className="text-right">Ações</TableHead>
525
909
  </TableRow>
526
910
  </TableHeader>
527
911
  <TableBody>
@@ -547,6 +931,49 @@ export default function TituloDetalhePage() {
547
931
  <TableCell className="capitalize">
548
932
  {liq.metodo}
549
933
  </TableCell>
934
+ <TableCell className="text-right">
935
+ <AlertDialog>
936
+ <AlertDialogTrigger asChild>
937
+ <Button
938
+ variant="outline"
939
+ size="sm"
940
+ disabled={
941
+ !liq.settlementId ||
942
+ liq.status === 'reversed' ||
943
+ !!reversingSettlementId
944
+ }
945
+ >
946
+ <Undo className="mr-2 h-4 w-4" />
947
+ Estornar
948
+ </Button>
949
+ </AlertDialogTrigger>
950
+ <AlertDialogContent>
951
+ <AlertDialogHeader>
952
+ <AlertDialogTitle>
953
+ Confirmar estorno
954
+ </AlertDialogTitle>
955
+ <AlertDialogDescription>
956
+ Esta ação cria o estorno da liquidação e
957
+ recalcula saldos e status.
958
+ </AlertDialogDescription>
959
+ </AlertDialogHeader>
960
+ <AlertDialogFooter>
961
+ <AlertDialogCancel>
962
+ Cancelar
963
+ </AlertDialogCancel>
964
+ <AlertDialogAction
965
+ onClick={() =>
966
+ void handleReverseSettlement(
967
+ String(liq.settlementId)
968
+ )
969
+ }
970
+ >
971
+ Confirmar estorno
972
+ </AlertDialogAction>
973
+ </AlertDialogFooter>
974
+ </AlertDialogContent>
975
+ </AlertDialog>
976
+ </TableCell>
550
977
  </TableRow>
551
978
  );
552
979
  })