@hed-hog/finance 0.0.251 → 0.0.253
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.
- package/dist/dto/reverse-settlement.dto.d.ts +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -1
- package/dist/dto/reverse-settlement.dto.js +5 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -1
- package/dist/finance-installments.controller.d.ts +40 -2
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +38 -2
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +39 -0
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +476 -219
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +457 -142
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +100 -5
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +19 -20
- package/hedhog/frontend/app/page.tsx.ejs +11 -4
- package/hedhog/frontend/messages/en.json +44 -0
- package/hedhog/frontend/messages/pt.json +44 -0
- package/hedhog/query/constraints.sql +3 -1
- package/hedhog/query/settlement-auditability.sql +175 -0
- package/hedhog/table/bank_reconciliation.yaml +11 -0
- package/hedhog/table/settlement.yaml +17 -1
- package/hedhog/table/settlement_allocation.yaml +3 -0
- package/package.json +2 -2
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +498 -114
|
@@ -10,7 +10,6 @@ import {
|
|
|
10
10
|
AlertDialogFooter,
|
|
11
11
|
AlertDialogHeader,
|
|
12
12
|
AlertDialogTitle,
|
|
13
|
-
AlertDialogTrigger,
|
|
14
13
|
} from '@/components/ui/alert-dialog';
|
|
15
14
|
import { AuditTimeline } from '@/components/ui/audit-timeline';
|
|
16
15
|
import { Button } from '@/components/ui/button';
|
|
@@ -21,14 +20,6 @@ import {
|
|
|
21
20
|
CardHeader,
|
|
22
21
|
CardTitle,
|
|
23
22
|
} from '@/components/ui/card';
|
|
24
|
-
import {
|
|
25
|
-
Dialog,
|
|
26
|
-
DialogContent,
|
|
27
|
-
DialogDescription,
|
|
28
|
-
DialogFooter,
|
|
29
|
-
DialogHeader,
|
|
30
|
-
DialogTitle,
|
|
31
|
-
} from '@/components/ui/dialog';
|
|
32
23
|
import {
|
|
33
24
|
DropdownMenu,
|
|
34
25
|
DropdownMenuContent,
|
|
@@ -54,6 +45,13 @@ import {
|
|
|
54
45
|
SelectTrigger,
|
|
55
46
|
SelectValue,
|
|
56
47
|
} from '@/components/ui/select';
|
|
48
|
+
import {
|
|
49
|
+
Sheet,
|
|
50
|
+
SheetContent,
|
|
51
|
+
SheetDescription,
|
|
52
|
+
SheetHeader,
|
|
53
|
+
SheetTitle,
|
|
54
|
+
} from '@/components/ui/sheet';
|
|
57
55
|
import { StatusBadge } from '@/components/ui/status-badge';
|
|
58
56
|
import {
|
|
59
57
|
Table,
|
|
@@ -65,38 +63,83 @@ import {
|
|
|
65
63
|
} from '@/components/ui/table';
|
|
66
64
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
67
65
|
import { TagSelectorSheet } from '@/components/ui/tag-selector-sheet';
|
|
68
|
-
import { useApp } from '@hed-hog/next-app-provider';
|
|
66
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
69
67
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
70
68
|
import {
|
|
71
69
|
CheckCircle,
|
|
70
|
+
ChevronDown,
|
|
71
|
+
ChevronUp,
|
|
72
72
|
Download,
|
|
73
73
|
Edit,
|
|
74
74
|
FileText,
|
|
75
|
+
Loader2,
|
|
75
76
|
MoreHorizontal,
|
|
76
77
|
Undo,
|
|
77
78
|
XCircle,
|
|
78
79
|
} from 'lucide-react';
|
|
79
80
|
import { useTranslations } from 'next-intl';
|
|
80
81
|
import Link from 'next/link';
|
|
81
|
-
import { useParams, useRouter } from 'next/navigation';
|
|
82
|
-
import { useEffect, useState } from 'react';
|
|
82
|
+
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
|
83
|
+
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
83
84
|
import { useForm } from 'react-hook-form';
|
|
84
85
|
import { z } from 'zod';
|
|
85
86
|
import { formatarData } from '../../../_lib/formatters';
|
|
86
87
|
import { useFinanceData } from '../../../_lib/use-finance-data';
|
|
87
88
|
|
|
88
|
-
|
|
89
|
-
installmentId:
|
|
90
|
-
amount:
|
|
91
|
-
description
|
|
92
|
-
}
|
|
93
|
-
|
|
94
|
-
type
|
|
89
|
+
type SettleFormValues = {
|
|
90
|
+
installmentId: string;
|
|
91
|
+
amount: number;
|
|
92
|
+
description?: string;
|
|
93
|
+
};
|
|
94
|
+
|
|
95
|
+
type SettlementHistoryAllocation = {
|
|
96
|
+
installmentId: string;
|
|
97
|
+
installmentSeq: number;
|
|
98
|
+
amountCents: number;
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
type SettlementHistoryItem = {
|
|
102
|
+
normal: {
|
|
103
|
+
id: string;
|
|
104
|
+
paidAt: string | null;
|
|
105
|
+
amountCents: number;
|
|
106
|
+
type: 'NORMAL';
|
|
107
|
+
method?: string | null;
|
|
108
|
+
account?: string | null;
|
|
109
|
+
accountId?: string | null;
|
|
110
|
+
createdAt?: string | null;
|
|
111
|
+
createdBy?: string | null;
|
|
112
|
+
memo?: string | null;
|
|
113
|
+
reconciled: boolean;
|
|
114
|
+
reconciliationId?: string | null;
|
|
115
|
+
};
|
|
116
|
+
reversal?: {
|
|
117
|
+
id: string;
|
|
118
|
+
paidAt: string | null;
|
|
119
|
+
amountCents: number;
|
|
120
|
+
type: 'REVERSAL';
|
|
121
|
+
createdAt?: string | null;
|
|
122
|
+
createdBy?: string | null;
|
|
123
|
+
memo?: string | null;
|
|
124
|
+
} | null;
|
|
125
|
+
allocations: SettlementHistoryAllocation[];
|
|
126
|
+
statusLabel: 'ATIVO' | 'ESTORNADO';
|
|
127
|
+
};
|
|
95
128
|
|
|
96
129
|
export default function TituloDetalhePage() {
|
|
97
130
|
const t = useTranslations('finance.PayableInstallmentDetailPage');
|
|
131
|
+
const settleSchema = useMemo(
|
|
132
|
+
() =>
|
|
133
|
+
z.object({
|
|
134
|
+
installmentId: z.string().min(1, t('validation.installmentRequired')),
|
|
135
|
+
amount: z.number().min(0.01, t('validation.amountGreaterThanZero')),
|
|
136
|
+
description: z.string().optional(),
|
|
137
|
+
}),
|
|
138
|
+
[t]
|
|
139
|
+
);
|
|
98
140
|
const { request, showToastHandler } = useApp();
|
|
99
141
|
const router = useRouter();
|
|
142
|
+
const searchParams = useSearchParams();
|
|
100
143
|
const params = useParams<{ id: string }>();
|
|
101
144
|
const id = params?.id;
|
|
102
145
|
const { data, refetch } = useFinanceData();
|
|
@@ -105,13 +148,34 @@ export default function TituloDetalhePage() {
|
|
|
105
148
|
pessoas,
|
|
106
149
|
categorias,
|
|
107
150
|
centrosCusto,
|
|
108
|
-
contasBancarias,
|
|
109
151
|
logsAuditoria,
|
|
110
152
|
tags,
|
|
111
153
|
} = data;
|
|
112
154
|
|
|
113
155
|
const titulo = titulosPagar.find((t) => t.id === id);
|
|
114
156
|
|
|
157
|
+
const {
|
|
158
|
+
data: settlementsHistory = [],
|
|
159
|
+
refetch: refetchSettlementsHistory,
|
|
160
|
+
isFetching: isFetchingSettlementsHistory,
|
|
161
|
+
} = useQuery<SettlementHistoryItem[]>({
|
|
162
|
+
queryKey: ['finance-title-settlements-history', id],
|
|
163
|
+
enabled: !!id,
|
|
164
|
+
queryFn: async () => {
|
|
165
|
+
const response = await request<SettlementHistoryItem[]>({
|
|
166
|
+
url: `/finance/titles/${id}/settlements-history`,
|
|
167
|
+
method: 'GET',
|
|
168
|
+
});
|
|
169
|
+
|
|
170
|
+
return (response.data || []) as SettlementHistoryItem[];
|
|
171
|
+
},
|
|
172
|
+
placeholderData: (old) => old,
|
|
173
|
+
});
|
|
174
|
+
|
|
175
|
+
const canSettle = ['aberto', 'parcial', 'vencido'].includes(
|
|
176
|
+
titulo?.status || ''
|
|
177
|
+
);
|
|
178
|
+
|
|
115
179
|
const settleCandidates = (titulo?.parcelas || []).filter(
|
|
116
180
|
(parcela: any) =>
|
|
117
181
|
parcela.status === 'aberto' ||
|
|
@@ -136,9 +200,17 @@ export default function TituloDetalhePage() {
|
|
|
136
200
|
const [isCanceling, setIsCanceling] = useState(false);
|
|
137
201
|
const [isSettleDialogOpen, setIsSettleDialogOpen] = useState(false);
|
|
138
202
|
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
|
|
203
|
+
const [isReverseDialogOpen, setIsReverseDialogOpen] = useState(false);
|
|
204
|
+
const [selectedSettlementIdToReverse, setSelectedSettlementIdToReverse] =
|
|
205
|
+
useState<string | null>(null);
|
|
206
|
+
const [reverseReason, setReverseReason] = useState('');
|
|
139
207
|
const [reversingSettlementId, setReversingSettlementId] = useState<
|
|
140
208
|
string | null
|
|
141
209
|
>(null);
|
|
210
|
+
const [unreconcilingId, setUnreconcilingId] = useState<string | null>(null);
|
|
211
|
+
const [expandedSettlementRows, setExpandedSettlementRows] = useState<
|
|
212
|
+
Record<string, boolean>
|
|
213
|
+
>({});
|
|
142
214
|
|
|
143
215
|
useEffect(() => {
|
|
144
216
|
setAvailableTags(tags || []);
|
|
@@ -173,6 +245,18 @@ export default function TituloDetalhePage() {
|
|
|
173
245
|
});
|
|
174
246
|
}, [settleForm, settleCandidates]);
|
|
175
247
|
|
|
248
|
+
useEffect(() => {
|
|
249
|
+
const action = searchParams.get('action');
|
|
250
|
+
|
|
251
|
+
if (action !== 'settle') {
|
|
252
|
+
return;
|
|
253
|
+
}
|
|
254
|
+
|
|
255
|
+
if (canSettle && settleCandidates.length > 0) {
|
|
256
|
+
setIsSettleDialogOpen(true);
|
|
257
|
+
}
|
|
258
|
+
}, [canSettle, searchParams, settleCandidates.length]);
|
|
259
|
+
|
|
176
260
|
if (!titulo) {
|
|
177
261
|
return (
|
|
178
262
|
<div className="space-y-6">
|
|
@@ -200,8 +284,6 @@ export default function TituloDetalhePage() {
|
|
|
200
284
|
categorias.find((c) => c.id === categoryId);
|
|
201
285
|
const getCentroCustoById = (costCenterId?: string) =>
|
|
202
286
|
centrosCusto.find((c) => c.id === costCenterId);
|
|
203
|
-
const getContaBancariaById = (bankId?: string) =>
|
|
204
|
-
contasBancarias.find((c) => c.id === bankId);
|
|
205
287
|
|
|
206
288
|
const fornecedor = getPessoaById(titulo.fornecedorId);
|
|
207
289
|
const categoria = getCategoriaById(titulo.categoriaId);
|
|
@@ -362,8 +444,14 @@ export default function TituloDetalhePage() {
|
|
|
362
444
|
|
|
363
445
|
const canApprove = titulo.status === 'rascunho';
|
|
364
446
|
const canEdit = titulo.status === 'rascunho';
|
|
365
|
-
const canSettle = ['aberto', 'parcial', 'vencido'].includes(titulo.status);
|
|
366
447
|
const canCancel = !['cancelado', 'liquidado'].includes(titulo.status);
|
|
448
|
+
const reversibleSettlements = settlementsHistory.filter(
|
|
449
|
+
(group) =>
|
|
450
|
+
group.normal?.id &&
|
|
451
|
+
group.statusLabel === 'ATIVO' &&
|
|
452
|
+
!group.normal?.reconciled
|
|
453
|
+
);
|
|
454
|
+
const canReverse = reversibleSettlements.length > 0;
|
|
367
455
|
|
|
368
456
|
const getErrorMessage = (error: any, fallback: string) => {
|
|
369
457
|
const message = error?.response?.data?.message;
|
|
@@ -392,11 +480,11 @@ export default function TituloDetalhePage() {
|
|
|
392
480
|
});
|
|
393
481
|
|
|
394
482
|
await refetch();
|
|
395
|
-
showToastHandler?.('success', '
|
|
483
|
+
showToastHandler?.('success', t('messages.approveSuccess'));
|
|
396
484
|
} catch (error) {
|
|
397
485
|
showToastHandler?.(
|
|
398
486
|
'error',
|
|
399
|
-
getErrorMessage(error, '
|
|
487
|
+
getErrorMessage(error, t('messages.approveError'))
|
|
400
488
|
);
|
|
401
489
|
} finally {
|
|
402
490
|
setIsApproving(false);
|
|
@@ -422,11 +510,11 @@ export default function TituloDetalhePage() {
|
|
|
422
510
|
|
|
423
511
|
await refetch();
|
|
424
512
|
setIsSettleDialogOpen(false);
|
|
425
|
-
showToastHandler?.('success', '
|
|
513
|
+
showToastHandler?.('success', t('messages.settleSuccess'));
|
|
426
514
|
} catch (error) {
|
|
427
515
|
showToastHandler?.(
|
|
428
516
|
'error',
|
|
429
|
-
getErrorMessage(error, '
|
|
517
|
+
getErrorMessage(error, t('messages.settleError'))
|
|
430
518
|
);
|
|
431
519
|
} finally {
|
|
432
520
|
setIsSettling(false);
|
|
@@ -441,23 +529,58 @@ export default function TituloDetalhePage() {
|
|
|
441
529
|
setReversingSettlementId(settlementId);
|
|
442
530
|
try {
|
|
443
531
|
await request({
|
|
444
|
-
url: `/finance/
|
|
445
|
-
method: '
|
|
446
|
-
data: {
|
|
532
|
+
url: `/finance/settlements/${settlementId}/reverse`,
|
|
533
|
+
method: 'POST',
|
|
534
|
+
data: {
|
|
535
|
+
reason: reverseReason?.trim() || undefined,
|
|
536
|
+
memo: reverseReason?.trim() || undefined,
|
|
537
|
+
},
|
|
447
538
|
});
|
|
448
539
|
|
|
449
|
-
await refetch();
|
|
450
|
-
|
|
540
|
+
await Promise.all([refetch(), refetchSettlementsHistory()]);
|
|
541
|
+
setReverseReason('');
|
|
542
|
+
showToastHandler?.('success', t('messages.reverseSuccess'));
|
|
451
543
|
} catch (error) {
|
|
452
544
|
showToastHandler?.(
|
|
453
545
|
'error',
|
|
454
|
-
getErrorMessage(error, '
|
|
546
|
+
getErrorMessage(error, t('messages.reverseError'))
|
|
455
547
|
);
|
|
456
548
|
} finally {
|
|
457
549
|
setReversingSettlementId(null);
|
|
458
550
|
}
|
|
459
551
|
};
|
|
460
552
|
|
|
553
|
+
const handleUnreconcileSettlement = async (reconciliationId: string) => {
|
|
554
|
+
if (!reconciliationId || unreconcilingId) {
|
|
555
|
+
return;
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
setUnreconcilingId(reconciliationId);
|
|
559
|
+
try {
|
|
560
|
+
await request({
|
|
561
|
+
url: `/finance/bank-reconciliations/${reconciliationId}`,
|
|
562
|
+
method: 'DELETE',
|
|
563
|
+
});
|
|
564
|
+
|
|
565
|
+
await Promise.all([refetch(), refetchSettlementsHistory()]);
|
|
566
|
+
showToastHandler?.('success', 'Conciliação removida com sucesso');
|
|
567
|
+
} catch (error) {
|
|
568
|
+
showToastHandler?.(
|
|
569
|
+
'error',
|
|
570
|
+
getErrorMessage(error, 'Não foi possível desconciliar a liquidação')
|
|
571
|
+
);
|
|
572
|
+
} finally {
|
|
573
|
+
setUnreconcilingId(null);
|
|
574
|
+
}
|
|
575
|
+
};
|
|
576
|
+
|
|
577
|
+
const toggleSettlementExpanded = (settlementId: string) => {
|
|
578
|
+
setExpandedSettlementRows((current) => ({
|
|
579
|
+
...current,
|
|
580
|
+
[settlementId]: !current[settlementId],
|
|
581
|
+
}));
|
|
582
|
+
};
|
|
583
|
+
|
|
461
584
|
const handleCancel = async () => {
|
|
462
585
|
if (!canCancel || isCanceling) {
|
|
463
586
|
return;
|
|
@@ -473,11 +596,11 @@ export default function TituloDetalhePage() {
|
|
|
473
596
|
|
|
474
597
|
await refetch();
|
|
475
598
|
setIsCancelDialogOpen(false);
|
|
476
|
-
showToastHandler?.('success', '
|
|
599
|
+
showToastHandler?.('success', t('messages.cancelSuccess'));
|
|
477
600
|
} catch (error) {
|
|
478
601
|
showToastHandler?.(
|
|
479
602
|
'error',
|
|
480
|
-
getErrorMessage(error, '
|
|
603
|
+
getErrorMessage(error, t('messages.cancelError'))
|
|
481
604
|
);
|
|
482
605
|
} finally {
|
|
483
606
|
setIsCanceling(false);
|
|
@@ -533,7 +656,21 @@ export default function TituloDetalhePage() {
|
|
|
533
656
|
<Download className="mr-2 h-4 w-4" />
|
|
534
657
|
{t('actions.settle')}
|
|
535
658
|
</DropdownMenuItem>
|
|
536
|
-
<DropdownMenuItem
|
|
659
|
+
<DropdownMenuItem
|
|
660
|
+
disabled={!canReverse || !!reversingSettlementId}
|
|
661
|
+
onClick={() => {
|
|
662
|
+
const latestSettlement = reversibleSettlements[0];
|
|
663
|
+
if (!latestSettlement?.normal?.id) {
|
|
664
|
+
return;
|
|
665
|
+
}
|
|
666
|
+
|
|
667
|
+
setSelectedSettlementIdToReverse(
|
|
668
|
+
String(latestSettlement.normal.id)
|
|
669
|
+
);
|
|
670
|
+
setReverseReason('');
|
|
671
|
+
setIsReverseDialogOpen(true);
|
|
672
|
+
}}
|
|
673
|
+
>
|
|
537
674
|
<Undo className="mr-2 h-4 w-4" />
|
|
538
675
|
{t('actions.reverse')}
|
|
539
676
|
</DropdownMenuItem>
|
|
@@ -555,42 +692,100 @@ export default function TituloDetalhePage() {
|
|
|
555
692
|
>
|
|
556
693
|
<AlertDialogContent>
|
|
557
694
|
<AlertDialogHeader>
|
|
558
|
-
<AlertDialogTitle>
|
|
695
|
+
<AlertDialogTitle>
|
|
696
|
+
{t('dialogs.cancel.title')}
|
|
697
|
+
</AlertDialogTitle>
|
|
559
698
|
<AlertDialogDescription>
|
|
560
|
-
|
|
561
|
-
registros de auditoria.
|
|
699
|
+
{t('dialogs.cancel.description')}
|
|
562
700
|
</AlertDialogDescription>
|
|
563
701
|
</AlertDialogHeader>
|
|
564
702
|
<AlertDialogFooter>
|
|
565
703
|
<AlertDialogCancel disabled={isCanceling}>
|
|
566
|
-
|
|
704
|
+
{t('dialogs.cancel.cancel')}
|
|
567
705
|
</AlertDialogCancel>
|
|
568
706
|
<AlertDialogAction
|
|
569
707
|
disabled={isCanceling}
|
|
570
708
|
onClick={() => void handleCancel()}
|
|
571
709
|
>
|
|
572
|
-
|
|
710
|
+
{t('dialogs.cancel.confirm')}
|
|
711
|
+
</AlertDialogAction>
|
|
712
|
+
</AlertDialogFooter>
|
|
713
|
+
</AlertDialogContent>
|
|
714
|
+
</AlertDialog>
|
|
715
|
+
|
|
716
|
+
<AlertDialog
|
|
717
|
+
open={isReverseDialogOpen}
|
|
718
|
+
onOpenChange={(open) => {
|
|
719
|
+
setIsReverseDialogOpen(open);
|
|
720
|
+
|
|
721
|
+
if (!open) {
|
|
722
|
+
setSelectedSettlementIdToReverse(null);
|
|
723
|
+
setReverseReason('');
|
|
724
|
+
}
|
|
725
|
+
}}
|
|
726
|
+
>
|
|
727
|
+
<AlertDialogContent>
|
|
728
|
+
<AlertDialogHeader>
|
|
729
|
+
<AlertDialogTitle>
|
|
730
|
+
{t('dialogs.reverse.title')}
|
|
731
|
+
</AlertDialogTitle>
|
|
732
|
+
<AlertDialogDescription>
|
|
733
|
+
{t('dialogs.reverse.description')}
|
|
734
|
+
</AlertDialogDescription>
|
|
735
|
+
</AlertDialogHeader>
|
|
736
|
+
<div className="space-y-2">
|
|
737
|
+
<FormLabel>Motivo do estorno</FormLabel>
|
|
738
|
+
<Input
|
|
739
|
+
value={reverseReason}
|
|
740
|
+
onChange={(event) => setReverseReason(event.target.value)}
|
|
741
|
+
placeholder="Ex.: baixa duplicada"
|
|
742
|
+
maxLength={255}
|
|
743
|
+
disabled={!!reversingSettlementId}
|
|
744
|
+
/>
|
|
745
|
+
</div>
|
|
746
|
+
<AlertDialogFooter>
|
|
747
|
+
<AlertDialogCancel disabled={!!reversingSettlementId}>
|
|
748
|
+
{t('dialogs.reverse.cancel')}
|
|
749
|
+
</AlertDialogCancel>
|
|
750
|
+
<AlertDialogAction
|
|
751
|
+
disabled={
|
|
752
|
+
!!reversingSettlementId || !selectedSettlementIdToReverse
|
|
753
|
+
}
|
|
754
|
+
onClick={() => {
|
|
755
|
+
if (!selectedSettlementIdToReverse) {
|
|
756
|
+
return;
|
|
757
|
+
}
|
|
758
|
+
|
|
759
|
+
void handleReverseSettlement(
|
|
760
|
+
selectedSettlementIdToReverse
|
|
761
|
+
).finally(() => {
|
|
762
|
+
setIsReverseDialogOpen(false);
|
|
763
|
+
setSelectedSettlementIdToReverse(null);
|
|
764
|
+
setReverseReason('');
|
|
765
|
+
});
|
|
766
|
+
}}
|
|
767
|
+
>
|
|
768
|
+
{t('dialogs.reverse.confirm')}
|
|
573
769
|
</AlertDialogAction>
|
|
574
770
|
</AlertDialogFooter>
|
|
575
771
|
</AlertDialogContent>
|
|
576
772
|
</AlertDialog>
|
|
577
773
|
|
|
578
|
-
<
|
|
774
|
+
<Sheet
|
|
579
775
|
open={isSettleDialogOpen}
|
|
580
776
|
onOpenChange={setIsSettleDialogOpen}
|
|
581
777
|
>
|
|
582
|
-
<
|
|
583
|
-
<
|
|
584
|
-
<
|
|
585
|
-
<
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
</DialogHeader>
|
|
778
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
779
|
+
<SheetHeader>
|
|
780
|
+
<SheetTitle>{t('settleSheet.title')}</SheetTitle>
|
|
781
|
+
<SheetDescription>
|
|
782
|
+
{t('settleSheet.description')}
|
|
783
|
+
</SheetDescription>
|
|
784
|
+
</SheetHeader>
|
|
590
785
|
|
|
591
786
|
<Form {...settleForm}>
|
|
592
787
|
<form
|
|
593
|
-
className="space-y-4"
|
|
788
|
+
className="space-y-4 px-4"
|
|
594
789
|
onSubmit={settleForm.handleSubmit(handleSettle)}
|
|
595
790
|
>
|
|
596
791
|
<FormField
|
|
@@ -598,7 +793,9 @@ export default function TituloDetalhePage() {
|
|
|
598
793
|
name="installmentId"
|
|
599
794
|
render={({ field }) => (
|
|
600
795
|
<FormItem>
|
|
601
|
-
<FormLabel>
|
|
796
|
+
<FormLabel>
|
|
797
|
+
{t('settleSheet.installmentLabel')}
|
|
798
|
+
</FormLabel>
|
|
602
799
|
<Select
|
|
603
800
|
value={field.value}
|
|
604
801
|
onValueChange={(value) => {
|
|
@@ -618,18 +815,24 @@ export default function TituloDetalhePage() {
|
|
|
618
815
|
}}
|
|
619
816
|
>
|
|
620
817
|
<FormControl>
|
|
621
|
-
<SelectTrigger>
|
|
622
|
-
<SelectValue
|
|
818
|
+
<SelectTrigger className="w-full">
|
|
819
|
+
<SelectValue
|
|
820
|
+
placeholder={t(
|
|
821
|
+
'settleSheet.installmentPlaceholder'
|
|
822
|
+
)}
|
|
823
|
+
/>
|
|
623
824
|
</SelectTrigger>
|
|
624
825
|
</FormControl>
|
|
625
826
|
<SelectContent>
|
|
626
827
|
{settleCandidates.map((parcela: any) => (
|
|
627
828
|
<SelectItem key={parcela.id} value={parcela.id}>
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
|
|
631
|
-
|
|
632
|
-
|
|
829
|
+
{t('settleSheet.installmentOption', {
|
|
830
|
+
number: parcela.numero,
|
|
831
|
+
amount: new Intl.NumberFormat('pt-BR', {
|
|
832
|
+
style: 'currency',
|
|
833
|
+
currency: 'BRL',
|
|
834
|
+
}).format(Number(parcela.valorAberto || 0)),
|
|
835
|
+
})}
|
|
633
836
|
</SelectItem>
|
|
634
837
|
))}
|
|
635
838
|
</SelectContent>
|
|
@@ -644,7 +847,7 @@ export default function TituloDetalhePage() {
|
|
|
644
847
|
name="amount"
|
|
645
848
|
render={({ field }) => (
|
|
646
849
|
<FormItem>
|
|
647
|
-
<FormLabel>
|
|
850
|
+
<FormLabel>{t('settleSheet.amountLabel')}</FormLabel>
|
|
648
851
|
<FormControl>
|
|
649
852
|
<InputMoney
|
|
650
853
|
value={Number(field.value || 0)}
|
|
@@ -663,7 +866,9 @@ export default function TituloDetalhePage() {
|
|
|
663
866
|
name="description"
|
|
664
867
|
render={({ field }) => (
|
|
665
868
|
<FormItem>
|
|
666
|
-
<FormLabel>
|
|
869
|
+
<FormLabel>
|
|
870
|
+
{t('settleSheet.descriptionLabel')}
|
|
871
|
+
</FormLabel>
|
|
667
872
|
<FormControl>
|
|
668
873
|
<Input {...field} value={field.value || ''} />
|
|
669
874
|
</FormControl>
|
|
@@ -672,23 +877,19 @@ export default function TituloDetalhePage() {
|
|
|
672
877
|
)}
|
|
673
878
|
/>
|
|
674
879
|
|
|
675
|
-
<
|
|
880
|
+
<div className="flex flex-col gap-4">
|
|
676
881
|
<Button
|
|
677
|
-
|
|
678
|
-
|
|
882
|
+
className="w-full"
|
|
883
|
+
type="submit"
|
|
679
884
|
disabled={isSettling}
|
|
680
|
-
onClick={() => setIsSettleDialogOpen(false)}
|
|
681
885
|
>
|
|
682
|
-
|
|
683
|
-
</Button>
|
|
684
|
-
<Button type="submit" disabled={isSettling}>
|
|
685
|
-
Confirmar baixa
|
|
886
|
+
{t('settleSheet.confirm')}
|
|
686
887
|
</Button>
|
|
687
|
-
</
|
|
888
|
+
</div>
|
|
688
889
|
</form>
|
|
689
890
|
</Form>
|
|
690
|
-
</
|
|
691
|
-
</
|
|
891
|
+
</SheetContent>
|
|
892
|
+
</Sheet>
|
|
692
893
|
</div>
|
|
693
894
|
}
|
|
694
895
|
/>
|
|
@@ -886,98 +1087,212 @@ export default function TituloDetalhePage() {
|
|
|
886
1087
|
<TabsContent value="liquidacoes" className="mt-4">
|
|
887
1088
|
<Card>
|
|
888
1089
|
<CardContent className="pt-6">
|
|
889
|
-
{
|
|
1090
|
+
{isFetchingSettlementsHistory ? (
|
|
1091
|
+
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
|
1092
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1093
|
+
Carregando histórico de liquidações...
|
|
1094
|
+
</div>
|
|
1095
|
+
) : settlementsHistory.length > 0 ? (
|
|
890
1096
|
<Table>
|
|
891
1097
|
<TableHeader>
|
|
892
1098
|
<TableRow>
|
|
893
|
-
<TableHead>
|
|
894
|
-
<TableHead
|
|
895
|
-
|
|
896
|
-
</TableHead>
|
|
897
|
-
<TableHead
|
|
898
|
-
|
|
899
|
-
</TableHead>
|
|
900
|
-
<TableHead className="text-right">
|
|
901
|
-
{t('settlementsTable.discount')}
|
|
902
|
-
</TableHead>
|
|
903
|
-
<TableHead className="text-right">
|
|
904
|
-
{t('settlementsTable.fine')}
|
|
905
|
-
</TableHead>
|
|
906
|
-
<TableHead>{t('settlementsTable.account')}</TableHead>
|
|
907
|
-
<TableHead>{t('settlementsTable.method')}</TableHead>
|
|
908
|
-
<TableHead className="text-right">Ações</TableHead>
|
|
1099
|
+
<TableHead>Date</TableHead>
|
|
1100
|
+
<TableHead>Type</TableHead>
|
|
1101
|
+
<TableHead>Status</TableHead>
|
|
1102
|
+
<TableHead className="text-right">Net Amount</TableHead>
|
|
1103
|
+
<TableHead>Method</TableHead>
|
|
1104
|
+
<TableHead>Account</TableHead>
|
|
1105
|
+
<TableHead>Reconciled</TableHead>
|
|
1106
|
+
<TableHead className="text-right">Actions</TableHead>
|
|
909
1107
|
</TableRow>
|
|
910
1108
|
</TableHeader>
|
|
911
1109
|
<TableBody>
|
|
912
|
-
{
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
918
|
-
|
|
919
|
-
|
|
1110
|
+
{settlementsHistory.map((group) => {
|
|
1111
|
+
const normalId = group.normal.id;
|
|
1112
|
+
const isExpanded = !!expandedSettlementRows[normalId];
|
|
1113
|
+
const canReverseNormal =
|
|
1114
|
+
group.statusLabel === 'ATIVO' &&
|
|
1115
|
+
!group.normal.reconciled &&
|
|
1116
|
+
!reversingSettlementId;
|
|
1117
|
+
|
|
1118
|
+
return (
|
|
1119
|
+
<Fragment key={normalId}>
|
|
1120
|
+
<TableRow>
|
|
1121
|
+
<TableCell>
|
|
1122
|
+
{formatarData(group.normal.paidAt)}
|
|
920
1123
|
</TableCell>
|
|
921
|
-
<TableCell
|
|
922
|
-
<
|
|
1124
|
+
<TableCell>
|
|
1125
|
+
<span className="rounded-md border px-2 py-1 text-xs font-medium">
|
|
1126
|
+
NORMAL
|
|
1127
|
+
</span>
|
|
923
1128
|
</TableCell>
|
|
924
|
-
<TableCell
|
|
925
|
-
<
|
|
1129
|
+
<TableCell>
|
|
1130
|
+
<span
|
|
1131
|
+
className={`rounded-md px-2 py-1 text-xs font-medium ${
|
|
1132
|
+
group.statusLabel === 'ESTORNADO'
|
|
1133
|
+
? 'bg-muted text-muted-foreground'
|
|
1134
|
+
: 'bg-primary/10 text-primary'
|
|
1135
|
+
}`}
|
|
1136
|
+
>
|
|
1137
|
+
{group.statusLabel}
|
|
1138
|
+
</span>
|
|
926
1139
|
</TableCell>
|
|
927
|
-
<TableCell className="text-right">
|
|
928
|
-
|
|
1140
|
+
<TableCell className="text-right font-medium">
|
|
1141
|
+
+
|
|
1142
|
+
<Money
|
|
1143
|
+
value={
|
|
1144
|
+
Math.abs(group.normal.amountCents || 0) / 100
|
|
1145
|
+
}
|
|
1146
|
+
/>
|
|
929
1147
|
</TableCell>
|
|
930
|
-
<TableCell>{conta?.descricao}</TableCell>
|
|
931
1148
|
<TableCell className="capitalize">
|
|
932
|
-
{
|
|
1149
|
+
{group.normal.method || '-'}
|
|
1150
|
+
</TableCell>
|
|
1151
|
+
<TableCell>{group.normal.account || '-'}</TableCell>
|
|
1152
|
+
<TableCell>
|
|
1153
|
+
<span
|
|
1154
|
+
className={`rounded-md px-2 py-1 text-xs font-medium ${
|
|
1155
|
+
group.normal.reconciled
|
|
1156
|
+
? 'bg-primary/10 text-primary'
|
|
1157
|
+
: 'bg-muted text-muted-foreground'
|
|
1158
|
+
}`}
|
|
1159
|
+
>
|
|
1160
|
+
{group.normal.reconciled
|
|
1161
|
+
? 'Conciliado'
|
|
1162
|
+
: 'Pendente'}
|
|
1163
|
+
</span>
|
|
933
1164
|
</TableCell>
|
|
934
1165
|
<TableCell className="text-right">
|
|
935
|
-
<
|
|
936
|
-
|
|
1166
|
+
<div className="flex items-center justify-end gap-2">
|
|
1167
|
+
{group.normal.reconciled &&
|
|
1168
|
+
group.normal.reconciliationId ? (
|
|
937
1169
|
<Button
|
|
938
1170
|
variant="outline"
|
|
939
1171
|
size="sm"
|
|
940
1172
|
disabled={
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
1173
|
+
unreconcilingId ===
|
|
1174
|
+
group.normal.reconciliationId
|
|
1175
|
+
}
|
|
1176
|
+
onClick={() =>
|
|
1177
|
+
void handleUnreconcileSettlement(
|
|
1178
|
+
String(group.normal.reconciliationId)
|
|
1179
|
+
)
|
|
944
1180
|
}
|
|
945
1181
|
>
|
|
946
|
-
|
|
947
|
-
|
|
1182
|
+
{unreconcilingId ===
|
|
1183
|
+
group.normal.reconciliationId
|
|
1184
|
+
? 'Desconciliando...'
|
|
1185
|
+
: 'Desconciliar'}
|
|
948
1186
|
</Button>
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
|
|
958
|
-
|
|
959
|
-
|
|
960
|
-
|
|
961
|
-
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
|
|
974
|
-
|
|
975
|
-
|
|
1187
|
+
) : null}
|
|
1188
|
+
|
|
1189
|
+
<Button
|
|
1190
|
+
variant="outline"
|
|
1191
|
+
size="sm"
|
|
1192
|
+
title={
|
|
1193
|
+
group.normal.reconciled
|
|
1194
|
+
? 'Desconciliar primeiro'
|
|
1195
|
+
: undefined
|
|
1196
|
+
}
|
|
1197
|
+
disabled={!canReverseNormal}
|
|
1198
|
+
onClick={() => {
|
|
1199
|
+
setSelectedSettlementIdToReverse(normalId);
|
|
1200
|
+
setReverseReason('');
|
|
1201
|
+
setIsReverseDialogOpen(true);
|
|
1202
|
+
}}
|
|
1203
|
+
>
|
|
1204
|
+
<Undo className="mr-2 h-4 w-4" />
|
|
1205
|
+
Estornar
|
|
1206
|
+
</Button>
|
|
1207
|
+
|
|
1208
|
+
<Button
|
|
1209
|
+
variant="ghost"
|
|
1210
|
+
size="sm"
|
|
1211
|
+
onClick={() =>
|
|
1212
|
+
toggleSettlementExpanded(normalId)
|
|
1213
|
+
}
|
|
1214
|
+
>
|
|
1215
|
+
{isExpanded ? (
|
|
1216
|
+
<ChevronUp className="h-4 w-4" />
|
|
1217
|
+
) : (
|
|
1218
|
+
<ChevronDown className="h-4 w-4" />
|
|
1219
|
+
)}
|
|
1220
|
+
</Button>
|
|
1221
|
+
</div>
|
|
976
1222
|
</TableCell>
|
|
977
1223
|
</TableRow>
|
|
978
|
-
|
|
979
|
-
|
|
980
|
-
|
|
1224
|
+
|
|
1225
|
+
{group.reversal ? (
|
|
1226
|
+
<TableRow>
|
|
1227
|
+
<TableCell className="pl-8">
|
|
1228
|
+
{formatarData(group.reversal.paidAt)}
|
|
1229
|
+
</TableCell>
|
|
1230
|
+
<TableCell>
|
|
1231
|
+
<span className="rounded-md border px-2 py-1 text-xs font-medium">
|
|
1232
|
+
REVERSAL
|
|
1233
|
+
</span>
|
|
1234
|
+
</TableCell>
|
|
1235
|
+
<TableCell>
|
|
1236
|
+
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
|
1237
|
+
ESTORNO
|
|
1238
|
+
</span>
|
|
1239
|
+
</TableCell>
|
|
1240
|
+
<TableCell className="text-right font-medium text-destructive">
|
|
1241
|
+
-
|
|
1242
|
+
<Money
|
|
1243
|
+
value={
|
|
1244
|
+
Math.abs(group.reversal.amountCents || 0) /
|
|
1245
|
+
100
|
|
1246
|
+
}
|
|
1247
|
+
/>
|
|
1248
|
+
</TableCell>
|
|
1249
|
+
<TableCell>-</TableCell>
|
|
1250
|
+
<TableCell>-</TableCell>
|
|
1251
|
+
<TableCell>-</TableCell>
|
|
1252
|
+
<TableCell className="text-right text-xs text-muted-foreground">
|
|
1253
|
+
{group.reversal.memo || '-'}
|
|
1254
|
+
</TableCell>
|
|
1255
|
+
</TableRow>
|
|
1256
|
+
) : null}
|
|
1257
|
+
|
|
1258
|
+
{isExpanded ? (
|
|
1259
|
+
<TableRow>
|
|
1260
|
+
<TableCell colSpan={8}>
|
|
1261
|
+
<div className="rounded-md border bg-muted/20 p-3">
|
|
1262
|
+
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
|
1263
|
+
Allocations
|
|
1264
|
+
</p>
|
|
1265
|
+
<div className="space-y-1">
|
|
1266
|
+
{group.allocations.map((allocation) => (
|
|
1267
|
+
<div
|
|
1268
|
+
key={`${normalId}-${allocation.installmentId}`}
|
|
1269
|
+
className="flex items-center justify-between text-sm"
|
|
1270
|
+
>
|
|
1271
|
+
<span>
|
|
1272
|
+
Parcela #{allocation.installmentSeq}
|
|
1273
|
+
</span>
|
|
1274
|
+
<span className="font-medium">
|
|
1275
|
+
+
|
|
1276
|
+
<Money
|
|
1277
|
+
value={
|
|
1278
|
+
Math.abs(
|
|
1279
|
+
Number(
|
|
1280
|
+
allocation.amountCents || 0
|
|
1281
|
+
)
|
|
1282
|
+
) / 100
|
|
1283
|
+
}
|
|
1284
|
+
/>
|
|
1285
|
+
</span>
|
|
1286
|
+
</div>
|
|
1287
|
+
))}
|
|
1288
|
+
</div>
|
|
1289
|
+
</div>
|
|
1290
|
+
</TableCell>
|
|
1291
|
+
</TableRow>
|
|
1292
|
+
) : null}
|
|
1293
|
+
</Fragment>
|
|
1294
|
+
);
|
|
1295
|
+
})}
|
|
981
1296
|
</TableBody>
|
|
982
1297
|
</Table>
|
|
983
1298
|
) : (
|