@hed-hog/finance 0.0.238 → 0.0.239

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 (34) hide show
  1. package/dist/dto/reject-title.dto.d.ts +4 -0
  2. package/dist/dto/reject-title.dto.d.ts.map +1 -0
  3. package/dist/dto/reject-title.dto.js +22 -0
  4. package/dist/dto/reject-title.dto.js.map +1 -0
  5. package/dist/dto/reverse-settlement.dto.d.ts +4 -0
  6. package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
  7. package/dist/dto/reverse-settlement.dto.js +22 -0
  8. package/dist/dto/reverse-settlement.dto.js.map +1 -0
  9. package/dist/dto/settle-installment.dto.d.ts +12 -0
  10. package/dist/dto/settle-installment.dto.d.ts.map +1 -0
  11. package/dist/dto/settle-installment.dto.js +71 -0
  12. package/dist/dto/settle-installment.dto.js.map +1 -0
  13. package/dist/finance-data.controller.d.ts +13 -5
  14. package/dist/finance-data.controller.d.ts.map +1 -1
  15. package/dist/finance-installments.controller.d.ts +248 -12
  16. package/dist/finance-installments.controller.d.ts.map +1 -1
  17. package/dist/finance-installments.controller.js +92 -0
  18. package/dist/finance-installments.controller.js.map +1 -1
  19. package/dist/finance.service.d.ts +275 -17
  20. package/dist/finance.service.d.ts.map +1 -1
  21. package/dist/finance.service.js +666 -78
  22. package/dist/finance.service.js.map +1 -1
  23. package/hedhog/data/route.yaml +63 -0
  24. package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
  25. package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +355 -4
  26. package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +440 -16
  27. package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +396 -49
  28. package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +432 -14
  29. package/package.json +5 -5
  30. package/src/dto/reject-title.dto.ts +7 -0
  31. package/src/dto/reverse-settlement.dto.ts +7 -0
  32. package/src/dto/settle-installment.dto.ts +55 -0
  33. package/src/finance-installments.controller.ts +102 -0
  34. package/src/finance.service.ts +1007 -82
@@ -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,
@@ -43,15 +80,25 @@ import { useTranslations } from 'next-intl';
43
80
  import Link from 'next/link';
44
81
  import { useParams } 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();
52
99
  const params = useParams<{ id: string }>();
53
100
  const id = params?.id;
54
- const { data } = useFinanceData();
101
+ const { data, refetch } = useFinanceData();
55
102
  const {
56
103
  titulosPagar,
57
104
  pessoas,
@@ -64,9 +111,31 @@ export default function TituloDetalhePage() {
64
111
 
65
112
  const titulo = titulosPagar.find((t) => t.id === id);
66
113
 
114
+ const settleCandidates = (titulo?.parcelas || []).filter(
115
+ (parcela: any) =>
116
+ parcela.status === 'aberto' ||
117
+ parcela.status === 'parcial' ||
118
+ parcela.status === 'vencido'
119
+ );
120
+
121
+ const settleForm = useForm<SettleFormValues>({
122
+ resolver: zodResolver(settleSchema),
123
+ defaultValues: {
124
+ installmentId: settleCandidates[0]?.id || '',
125
+ amount: Number(settleCandidates[0]?.valorAberto || 0),
126
+ description: '',
127
+ },
128
+ });
129
+
67
130
  const [availableTags, setAvailableTags] = useState<any[]>([]);
68
131
  const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
69
132
  const [isCreatingTag, setIsCreatingTag] = useState(false);
133
+ const [isApproving, setIsApproving] = useState(false);
134
+ const [isSettling, setIsSettling] = useState(false);
135
+ const [isSettleDialogOpen, setIsSettleDialogOpen] = useState(false);
136
+ const [reversingSettlementId, setReversingSettlementId] = useState<
137
+ string | null
138
+ >(null);
70
139
 
71
140
  useEffect(() => {
72
141
  setAvailableTags(tags || []);
@@ -80,6 +149,27 @@ export default function TituloDetalhePage() {
80
149
  setSelectedTagIds(Array.isArray(titulo.tags) ? titulo.tags : []);
81
150
  }, [titulo]);
82
151
 
152
+ useEffect(() => {
153
+ const firstCandidate = settleCandidates[0];
154
+ const nextInstallmentId = firstCandidate?.id || '';
155
+ const nextAmount = Number(firstCandidate?.valorAberto || 0);
156
+ const currentInstallmentId = settleForm.getValues('installmentId') || '';
157
+ const currentAmount = Number(settleForm.getValues('amount') || 0);
158
+
159
+ if (
160
+ currentInstallmentId === nextInstallmentId &&
161
+ currentAmount === nextAmount
162
+ ) {
163
+ return;
164
+ }
165
+
166
+ settleForm.reset({
167
+ installmentId: nextInstallmentId,
168
+ amount: nextAmount,
169
+ description: '',
170
+ });
171
+ }, [settleForm, settleCandidates]);
172
+
83
173
  if (!titulo) {
84
174
  return (
85
175
  <div className="space-y-6">
@@ -267,6 +357,102 @@ export default function TituloDetalhePage() {
267
357
  }
268
358
  };
269
359
 
360
+ const canApprove = titulo.status === 'rascunho';
361
+ const canSettle = ['aprovado', 'aberto', 'parcial'].includes(titulo.status);
362
+
363
+ const getErrorMessage = (error: any, fallback: string) => {
364
+ const message = error?.response?.data?.message;
365
+
366
+ if (Array.isArray(message)) {
367
+ return message.join(', ');
368
+ }
369
+
370
+ if (typeof message === 'string' && message.trim()) {
371
+ return message;
372
+ }
373
+
374
+ return fallback;
375
+ };
376
+
377
+ const handleApprove = async () => {
378
+ if (!canApprove || isApproving) {
379
+ return;
380
+ }
381
+
382
+ setIsApproving(true);
383
+ try {
384
+ await request({
385
+ url: `/finance/accounts-payable/installments/${titulo.id}/approve`,
386
+ method: 'PATCH',
387
+ });
388
+
389
+ await refetch();
390
+ showToastHandler?.('success', 'Título aprovado com sucesso');
391
+ } catch (error) {
392
+ showToastHandler?.(
393
+ 'error',
394
+ getErrorMessage(error, 'Não foi possível aprovar o título')
395
+ );
396
+ } finally {
397
+ setIsApproving(false);
398
+ }
399
+ };
400
+
401
+ const handleSettle = async (values: SettleFormValues) => {
402
+ if (!canSettle || isSettling) {
403
+ return;
404
+ }
405
+
406
+ setIsSettling(true);
407
+ try {
408
+ await request({
409
+ url: `/finance/accounts-payable/installments/${titulo.id}/settlements`,
410
+ method: 'POST',
411
+ data: {
412
+ installment_id: Number(values.installmentId),
413
+ amount: values.amount,
414
+ description: values.description?.trim() || undefined,
415
+ },
416
+ });
417
+
418
+ await refetch();
419
+ setIsSettleDialogOpen(false);
420
+ showToastHandler?.('success', 'Baixa registrada com sucesso');
421
+ } catch (error) {
422
+ showToastHandler?.(
423
+ 'error',
424
+ getErrorMessage(error, 'Não foi possível registrar a baixa')
425
+ );
426
+ } finally {
427
+ setIsSettling(false);
428
+ }
429
+ };
430
+
431
+ const handleReverseSettlement = async (settlementId: string) => {
432
+ if (!settlementId || reversingSettlementId) {
433
+ return;
434
+ }
435
+
436
+ setReversingSettlementId(settlementId);
437
+ try {
438
+ await request({
439
+ url: `/finance/accounts-payable/installments/${titulo.id}/settlements/${settlementId}/reverse`,
440
+ method: 'PATCH',
441
+ data: {},
442
+ });
443
+
444
+ await refetch();
445
+ showToastHandler?.('success', 'Estorno realizado com sucesso');
446
+ } catch (error) {
447
+ showToastHandler?.(
448
+ 'error',
449
+ getErrorMessage(error, 'Não foi possível estornar a liquidação')
450
+ );
451
+ } finally {
452
+ setReversingSettlementId(null);
453
+ }
454
+ };
455
+
270
456
  return (
271
457
  <Page>
272
458
  <PageHeader
@@ -295,15 +481,21 @@ export default function TituloDetalhePage() {
295
481
  <Edit className="mr-2 h-4 w-4" />
296
482
  {t('actions.edit')}
297
483
  </DropdownMenuItem>
298
- <DropdownMenuItem>
484
+ <DropdownMenuItem
485
+ disabled={!canApprove || isApproving}
486
+ onClick={() => void handleApprove()}
487
+ >
299
488
  <CheckCircle className="mr-2 h-4 w-4" />
300
489
  {t('actions.approve')}
301
490
  </DropdownMenuItem>
302
- <DropdownMenuItem>
491
+ <DropdownMenuItem
492
+ disabled={!canSettle || settleCandidates.length === 0}
493
+ onClick={() => setIsSettleDialogOpen(true)}
494
+ >
303
495
  <Download className="mr-2 h-4 w-4" />
304
496
  {t('actions.settle')}
305
497
  </DropdownMenuItem>
306
- <DropdownMenuItem>
498
+ <DropdownMenuItem disabled>
307
499
  <Undo className="mr-2 h-4 w-4" />
308
500
  {t('actions.reverse')}
309
501
  </DropdownMenuItem>
@@ -314,6 +506,121 @@ export default function TituloDetalhePage() {
314
506
  </DropdownMenuItem>
315
507
  </DropdownMenuContent>
316
508
  </DropdownMenu>
509
+
510
+ <Dialog
511
+ open={isSettleDialogOpen}
512
+ onOpenChange={setIsSettleDialogOpen}
513
+ >
514
+ <DialogContent>
515
+ <DialogHeader>
516
+ <DialogTitle>Registrar baixa</DialogTitle>
517
+ <DialogDescription>
518
+ Informe a parcela e o valor da baixa. O backend valida os
519
+ estados e limites de valor automaticamente.
520
+ </DialogDescription>
521
+ </DialogHeader>
522
+
523
+ <Form {...settleForm}>
524
+ <form
525
+ className="space-y-4"
526
+ onSubmit={settleForm.handleSubmit(handleSettle)}
527
+ >
528
+ <FormField
529
+ control={settleForm.control}
530
+ name="installmentId"
531
+ render={({ field }) => (
532
+ <FormItem>
533
+ <FormLabel>Parcela</FormLabel>
534
+ <Select
535
+ value={field.value}
536
+ onValueChange={(value) => {
537
+ field.onChange(value);
538
+
539
+ const selected = settleCandidates.find(
540
+ (parcela: any) => parcela.id === value
541
+ );
542
+
543
+ if (selected) {
544
+ settleForm.setValue(
545
+ 'amount',
546
+ Number(selected.valorAberto || 0),
547
+ { shouldValidate: true }
548
+ );
549
+ }
550
+ }}
551
+ >
552
+ <FormControl>
553
+ <SelectTrigger>
554
+ <SelectValue placeholder="Selecione" />
555
+ </SelectTrigger>
556
+ </FormControl>
557
+ <SelectContent>
558
+ {settleCandidates.map((parcela: any) => (
559
+ <SelectItem key={parcela.id} value={parcela.id}>
560
+ Parcela {parcela.numero} - em aberto:{' '}
561
+ {new Intl.NumberFormat('pt-BR', {
562
+ style: 'currency',
563
+ currency: 'BRL',
564
+ }).format(Number(parcela.valorAberto || 0))}
565
+ </SelectItem>
566
+ ))}
567
+ </SelectContent>
568
+ </Select>
569
+ <FormMessage />
570
+ </FormItem>
571
+ )}
572
+ />
573
+
574
+ <FormField
575
+ control={settleForm.control}
576
+ name="amount"
577
+ render={({ field }) => (
578
+ <FormItem>
579
+ <FormLabel>Valor</FormLabel>
580
+ <FormControl>
581
+ <InputMoney
582
+ value={Number(field.value || 0)}
583
+ onValueChange={(value) => {
584
+ field.onChange(Number(value || 0));
585
+ }}
586
+ />
587
+ </FormControl>
588
+ <FormMessage />
589
+ </FormItem>
590
+ )}
591
+ />
592
+
593
+ <FormField
594
+ control={settleForm.control}
595
+ name="description"
596
+ render={({ field }) => (
597
+ <FormItem>
598
+ <FormLabel>Descrição (opcional)</FormLabel>
599
+ <FormControl>
600
+ <Input {...field} value={field.value || ''} />
601
+ </FormControl>
602
+ <FormMessage />
603
+ </FormItem>
604
+ )}
605
+ />
606
+
607
+ <DialogFooter>
608
+ <Button
609
+ type="button"
610
+ variant="outline"
611
+ disabled={isSettling}
612
+ onClick={() => setIsSettleDialogOpen(false)}
613
+ >
614
+ Cancelar
615
+ </Button>
616
+ <Button type="submit" disabled={isSettling}>
617
+ Confirmar baixa
618
+ </Button>
619
+ </DialogFooter>
620
+ </form>
621
+ </Form>
622
+ </DialogContent>
623
+ </Dialog>
317
624
  </div>
318
625
  }
319
626
  />
@@ -522,6 +829,7 @@ export default function TituloDetalhePage() {
522
829
  </TableHead>
523
830
  <TableHead>{t('settlementsTable.account')}</TableHead>
524
831
  <TableHead>{t('settlementsTable.method')}</TableHead>
832
+ <TableHead className="text-right">Ações</TableHead>
525
833
  </TableRow>
526
834
  </TableHeader>
527
835
  <TableBody>
@@ -547,6 +855,49 @@ export default function TituloDetalhePage() {
547
855
  <TableCell className="capitalize">
548
856
  {liq.metodo}
549
857
  </TableCell>
858
+ <TableCell className="text-right">
859
+ <AlertDialog>
860
+ <AlertDialogTrigger asChild>
861
+ <Button
862
+ variant="outline"
863
+ size="sm"
864
+ disabled={
865
+ !liq.settlementId ||
866
+ liq.status === 'reversed' ||
867
+ !!reversingSettlementId
868
+ }
869
+ >
870
+ <Undo className="mr-2 h-4 w-4" />
871
+ Estornar
872
+ </Button>
873
+ </AlertDialogTrigger>
874
+ <AlertDialogContent>
875
+ <AlertDialogHeader>
876
+ <AlertDialogTitle>
877
+ Confirmar estorno
878
+ </AlertDialogTitle>
879
+ <AlertDialogDescription>
880
+ Esta ação cria o estorno da liquidação e
881
+ recalcula saldos e status.
882
+ </AlertDialogDescription>
883
+ </AlertDialogHeader>
884
+ <AlertDialogFooter>
885
+ <AlertDialogCancel>
886
+ Cancelar
887
+ </AlertDialogCancel>
888
+ <AlertDialogAction
889
+ onClick={() =>
890
+ void handleReverseSettlement(
891
+ String(liq.settlementId)
892
+ )
893
+ }
894
+ >
895
+ Confirmar estorno
896
+ </AlertDialogAction>
897
+ </AlertDialogFooter>
898
+ </AlertDialogContent>
899
+ </AlertDialog>
900
+ </TableCell>
550
901
  </TableRow>
551
902
  );
552
903
  })