@hed-hog/finance 0.0.252 → 0.0.256
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 +106 -4
- 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 +104 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +366 -121
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- package/hedhog/frontend/app/_components/finance-entity-field-with-create.tsx.ejs +572 -0
- package/hedhog/frontend/app/_components/finance-title-actions-menu.tsx.ejs +244 -0
- package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +143 -51
- package/hedhog/frontend/app/_lib/title-action-rules.ts.ejs +36 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +449 -293
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1189 -545
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +176 -133
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1459 -312
- package/hedhog/frontend/app/page.tsx.ejs +15 -4
- package/hedhog/frontend/messages/en.json +294 -5
- package/hedhog/frontend/messages/pt.json +294 -5
- 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 +7 -7
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +521 -146
|
@@ -1,17 +1,7 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import { FinanceTitleActionsMenu } from '@/app/(app)/(libraries)/finance/_components/finance-title-actions-menu';
|
|
3
4
|
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';
|
|
15
5
|
import { AuditTimeline } from '@/components/ui/audit-timeline';
|
|
16
6
|
import { Button } from '@/components/ui/button';
|
|
17
7
|
import {
|
|
@@ -21,13 +11,6 @@ import {
|
|
|
21
11
|
CardHeader,
|
|
22
12
|
CardTitle,
|
|
23
13
|
} from '@/components/ui/card';
|
|
24
|
-
import {
|
|
25
|
-
DropdownMenu,
|
|
26
|
-
DropdownMenuContent,
|
|
27
|
-
DropdownMenuItem,
|
|
28
|
-
DropdownMenuSeparator,
|
|
29
|
-
DropdownMenuTrigger,
|
|
30
|
-
} from '@/components/ui/dropdown-menu';
|
|
31
14
|
import {
|
|
32
15
|
Form,
|
|
33
16
|
FormControl,
|
|
@@ -38,6 +21,7 @@ import {
|
|
|
38
21
|
} from '@/components/ui/form';
|
|
39
22
|
import { Input } from '@/components/ui/input';
|
|
40
23
|
import { InputMoney } from '@/components/ui/input-money';
|
|
24
|
+
import { Label } from '@/components/ui/label';
|
|
41
25
|
import { Money } from '@/components/ui/money';
|
|
42
26
|
import {
|
|
43
27
|
Select,
|
|
@@ -64,24 +48,23 @@ import {
|
|
|
64
48
|
} from '@/components/ui/table';
|
|
65
49
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
66
50
|
import { TagSelectorSheet } from '@/components/ui/tag-selector-sheet';
|
|
67
|
-
import { useApp } from '@hed-hog/next-app-provider';
|
|
51
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
68
52
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
69
|
-
import {
|
|
70
|
-
CheckCircle,
|
|
71
|
-
Download,
|
|
72
|
-
Edit,
|
|
73
|
-
FileText,
|
|
74
|
-
MoreHorizontal,
|
|
75
|
-
Undo,
|
|
76
|
-
XCircle,
|
|
77
|
-
} from 'lucide-react';
|
|
53
|
+
import { ChevronDown, ChevronUp, FileText, Loader2, Undo } from 'lucide-react';
|
|
78
54
|
import { useTranslations } from 'next-intl';
|
|
79
55
|
import Link from 'next/link';
|
|
80
56
|
import { useParams, useRouter, useSearchParams } from 'next/navigation';
|
|
81
|
-
import { useEffect, useMemo, useState } from 'react';
|
|
57
|
+
import { Fragment, useEffect, useMemo, useState } from 'react';
|
|
82
58
|
import { useForm } from 'react-hook-form';
|
|
83
59
|
import { z } from 'zod';
|
|
84
60
|
import { formatarData } from '../../../_lib/formatters';
|
|
61
|
+
import {
|
|
62
|
+
canApproveTitle,
|
|
63
|
+
canCancelTitle,
|
|
64
|
+
canEditTitle,
|
|
65
|
+
canReverseTitle,
|
|
66
|
+
canSettleTitle,
|
|
67
|
+
} from '../../../_lib/title-action-rules';
|
|
85
68
|
import { useFinanceData } from '../../../_lib/use-finance-data';
|
|
86
69
|
|
|
87
70
|
type SettleFormValues = {
|
|
@@ -90,6 +73,40 @@ type SettleFormValues = {
|
|
|
90
73
|
description?: string;
|
|
91
74
|
};
|
|
92
75
|
|
|
76
|
+
type SettlementHistoryAllocation = {
|
|
77
|
+
installmentId: string;
|
|
78
|
+
installmentSeq: number;
|
|
79
|
+
amountCents: number;
|
|
80
|
+
};
|
|
81
|
+
|
|
82
|
+
type SettlementHistoryItem = {
|
|
83
|
+
normal: {
|
|
84
|
+
id: string;
|
|
85
|
+
paidAt: string | null;
|
|
86
|
+
amountCents: number;
|
|
87
|
+
type: 'NORMAL';
|
|
88
|
+
method?: string | null;
|
|
89
|
+
account?: string | null;
|
|
90
|
+
accountId?: string | null;
|
|
91
|
+
createdAt?: string | null;
|
|
92
|
+
createdBy?: string | null;
|
|
93
|
+
memo?: string | null;
|
|
94
|
+
reconciled: boolean;
|
|
95
|
+
reconciliationId?: string | null;
|
|
96
|
+
};
|
|
97
|
+
reversal?: {
|
|
98
|
+
id: string;
|
|
99
|
+
paidAt: string | null;
|
|
100
|
+
amountCents: number;
|
|
101
|
+
type: 'REVERSAL';
|
|
102
|
+
createdAt?: string | null;
|
|
103
|
+
createdBy?: string | null;
|
|
104
|
+
memo?: string | null;
|
|
105
|
+
} | null;
|
|
106
|
+
allocations: SettlementHistoryAllocation[];
|
|
107
|
+
statusLabel: 'ATIVO' | 'ESTORNADO';
|
|
108
|
+
};
|
|
109
|
+
|
|
93
110
|
export default function TituloDetalhePage() {
|
|
94
111
|
const t = useTranslations('finance.PayableInstallmentDetailPage');
|
|
95
112
|
const settleSchema = useMemo(
|
|
@@ -112,15 +129,31 @@ export default function TituloDetalhePage() {
|
|
|
112
129
|
pessoas,
|
|
113
130
|
categorias,
|
|
114
131
|
centrosCusto,
|
|
115
|
-
contasBancarias,
|
|
116
132
|
logsAuditoria,
|
|
117
133
|
tags,
|
|
118
134
|
} = data;
|
|
119
135
|
|
|
120
136
|
const titulo = titulosPagar.find((t) => t.id === id);
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
137
|
+
|
|
138
|
+
const {
|
|
139
|
+
data: settlementsHistory = [],
|
|
140
|
+
refetch: refetchSettlementsHistory,
|
|
141
|
+
isFetching: isFetchingSettlementsHistory,
|
|
142
|
+
} = useQuery<SettlementHistoryItem[]>({
|
|
143
|
+
queryKey: ['finance-title-settlements-history', id],
|
|
144
|
+
enabled: !!id,
|
|
145
|
+
queryFn: async () => {
|
|
146
|
+
const response = await request<SettlementHistoryItem[]>({
|
|
147
|
+
url: `/finance/titles/${id}/settlements-history`,
|
|
148
|
+
method: 'GET',
|
|
149
|
+
});
|
|
150
|
+
|
|
151
|
+
return (response.data || []) as SettlementHistoryItem[];
|
|
152
|
+
},
|
|
153
|
+
placeholderData: (old) => old,
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
const canSettle = canSettleTitle(titulo?.status);
|
|
124
157
|
|
|
125
158
|
const settleCandidates = (titulo?.parcelas || []).filter(
|
|
126
159
|
(parcela: any) =>
|
|
@@ -145,13 +178,17 @@ export default function TituloDetalhePage() {
|
|
|
145
178
|
const [isSettling, setIsSettling] = useState(false);
|
|
146
179
|
const [isCanceling, setIsCanceling] = useState(false);
|
|
147
180
|
const [isSettleDialogOpen, setIsSettleDialogOpen] = useState(false);
|
|
148
|
-
const [isCancelDialogOpen, setIsCancelDialogOpen] = useState(false);
|
|
149
181
|
const [isReverseDialogOpen, setIsReverseDialogOpen] = useState(false);
|
|
150
182
|
const [selectedSettlementIdToReverse, setSelectedSettlementIdToReverse] =
|
|
151
183
|
useState<string | null>(null);
|
|
184
|
+
const [reverseReason, setReverseReason] = useState('');
|
|
152
185
|
const [reversingSettlementId, setReversingSettlementId] = useState<
|
|
153
186
|
string | null
|
|
154
187
|
>(null);
|
|
188
|
+
const [unreconcilingId, setUnreconcilingId] = useState<string | null>(null);
|
|
189
|
+
const [expandedSettlementRows, setExpandedSettlementRows] = useState<
|
|
190
|
+
Record<string, boolean>
|
|
191
|
+
>({});
|
|
155
192
|
|
|
156
193
|
useEffect(() => {
|
|
157
194
|
setAvailableTags(tags || []);
|
|
@@ -225,8 +262,6 @@ export default function TituloDetalhePage() {
|
|
|
225
262
|
categorias.find((c) => c.id === categoryId);
|
|
226
263
|
const getCentroCustoById = (costCenterId?: string) =>
|
|
227
264
|
centrosCusto.find((c) => c.id === costCenterId);
|
|
228
|
-
const getContaBancariaById = (bankId?: string) =>
|
|
229
|
-
contasBancarias.find((c) => c.id === bankId);
|
|
230
265
|
|
|
231
266
|
const fornecedor = getPessoaById(titulo.fornecedorId);
|
|
232
267
|
const categoria = getCategoriaById(titulo.categoriaId);
|
|
@@ -282,11 +317,6 @@ export default function TituloDetalhePage() {
|
|
|
282
317
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
283
318
|
};
|
|
284
319
|
|
|
285
|
-
const tTagSelector = (key: string, fallback: string) => {
|
|
286
|
-
const fullKey = `tagSelector.${key}`;
|
|
287
|
-
return t.has(fullKey) ? t(fullKey) : fallback;
|
|
288
|
-
};
|
|
289
|
-
|
|
290
320
|
const toTagSlug = (value: string) => {
|
|
291
321
|
return value
|
|
292
322
|
.normalize('NFD')
|
|
@@ -334,10 +364,7 @@ export default function TituloDetalhePage() {
|
|
|
334
364
|
return [...current, newTag];
|
|
335
365
|
});
|
|
336
366
|
|
|
337
|
-
showToastHandler?.(
|
|
338
|
-
'success',
|
|
339
|
-
tTagSelector('messages.createSuccess', 'Tag criada com sucesso')
|
|
340
|
-
);
|
|
367
|
+
showToastHandler?.('success', t('tagSelector.messages.createSuccess'));
|
|
341
368
|
|
|
342
369
|
return {
|
|
343
370
|
id: newTag.id,
|
|
@@ -345,10 +372,7 @@ export default function TituloDetalhePage() {
|
|
|
345
372
|
color: newTag.cor,
|
|
346
373
|
};
|
|
347
374
|
} catch {
|
|
348
|
-
showToastHandler?.(
|
|
349
|
-
'error',
|
|
350
|
-
tTagSelector('messages.createError', 'Não foi possível criar a tag')
|
|
351
|
-
);
|
|
375
|
+
showToastHandler?.('error', t('tagSelector.messages.createError'));
|
|
352
376
|
return null;
|
|
353
377
|
} finally {
|
|
354
378
|
setIsCreatingTag(false);
|
|
@@ -375,29 +399,21 @@ export default function TituloDetalhePage() {
|
|
|
375
399
|
setSelectedTagIds(nextTagIds);
|
|
376
400
|
}
|
|
377
401
|
} catch {
|
|
378
|
-
showToastHandler?.(
|
|
379
|
-
'error',
|
|
380
|
-
tTagSelector(
|
|
381
|
-
'messages.updateError',
|
|
382
|
-
'Não foi possível atualizar as tags'
|
|
383
|
-
)
|
|
384
|
-
);
|
|
402
|
+
showToastHandler?.('error', t('tagSelector.messages.updateError'));
|
|
385
403
|
}
|
|
386
404
|
};
|
|
387
405
|
|
|
388
|
-
const canApprove = titulo.status
|
|
389
|
-
const canEdit = titulo.status
|
|
390
|
-
const canCancel =
|
|
391
|
-
const reversibleSettlements =
|
|
392
|
-
|
|
393
|
-
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
});
|
|
400
|
-
const canReverse = reversibleSettlements.length > 0;
|
|
406
|
+
const canApprove = canApproveTitle(titulo.status);
|
|
407
|
+
const canEdit = canEditTitle(titulo.status);
|
|
408
|
+
const canCancel = canCancelTitle(titulo.status);
|
|
409
|
+
const reversibleSettlements = settlementsHistory.filter(
|
|
410
|
+
(group) =>
|
|
411
|
+
group.normal?.id &&
|
|
412
|
+
group.statusLabel === 'ATIVO' &&
|
|
413
|
+
!group.normal?.reconciled
|
|
414
|
+
);
|
|
415
|
+
const canReverse =
|
|
416
|
+
canReverseTitle(titulo.status) && reversibleSettlements.length > 0;
|
|
401
417
|
|
|
402
418
|
const getErrorMessage = (error: any, fallback: string) => {
|
|
403
419
|
const message = error?.response?.data?.message;
|
|
@@ -454,7 +470,7 @@ export default function TituloDetalhePage() {
|
|
|
454
470
|
},
|
|
455
471
|
});
|
|
456
472
|
|
|
457
|
-
await refetch();
|
|
473
|
+
await Promise.all([refetch(), refetchSettlementsHistory()]);
|
|
458
474
|
setIsSettleDialogOpen(false);
|
|
459
475
|
showToastHandler?.('success', t('messages.settleSuccess'));
|
|
460
476
|
} catch (error) {
|
|
@@ -467,7 +483,10 @@ export default function TituloDetalhePage() {
|
|
|
467
483
|
}
|
|
468
484
|
};
|
|
469
485
|
|
|
470
|
-
const handleReverseSettlement = async (
|
|
486
|
+
const handleReverseSettlement = async (
|
|
487
|
+
settlementId: string,
|
|
488
|
+
reasonOverride?: string
|
|
489
|
+
) => {
|
|
471
490
|
if (!settlementId || reversingSettlementId) {
|
|
472
491
|
return;
|
|
473
492
|
}
|
|
@@ -475,12 +494,16 @@ export default function TituloDetalhePage() {
|
|
|
475
494
|
setReversingSettlementId(settlementId);
|
|
476
495
|
try {
|
|
477
496
|
await request({
|
|
478
|
-
url: `/finance/
|
|
479
|
-
method: '
|
|
480
|
-
data: {
|
|
497
|
+
url: `/finance/settlements/${settlementId}/reverse`,
|
|
498
|
+
method: 'POST',
|
|
499
|
+
data: {
|
|
500
|
+
reason: reasonOverride?.trim() || reverseReason?.trim() || undefined,
|
|
501
|
+
memo: reasonOverride?.trim() || reverseReason?.trim() || undefined,
|
|
502
|
+
},
|
|
481
503
|
});
|
|
482
504
|
|
|
483
|
-
await refetch();
|
|
505
|
+
await Promise.all([refetch(), refetchSettlementsHistory()]);
|
|
506
|
+
setReverseReason('');
|
|
484
507
|
showToastHandler?.('success', t('messages.reverseSuccess'));
|
|
485
508
|
} catch (error) {
|
|
486
509
|
showToastHandler?.(
|
|
@@ -492,6 +515,37 @@ export default function TituloDetalhePage() {
|
|
|
492
515
|
}
|
|
493
516
|
};
|
|
494
517
|
|
|
518
|
+
const handleUnreconcileSettlement = async (reconciliationId: string) => {
|
|
519
|
+
if (!reconciliationId || unreconcilingId) {
|
|
520
|
+
return;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
setUnreconcilingId(reconciliationId);
|
|
524
|
+
try {
|
|
525
|
+
await request({
|
|
526
|
+
url: `/finance/bank-reconciliations/${reconciliationId}`,
|
|
527
|
+
method: 'DELETE',
|
|
528
|
+
});
|
|
529
|
+
|
|
530
|
+
await Promise.all([refetch(), refetchSettlementsHistory()]);
|
|
531
|
+
showToastHandler?.('success', t('messages.unreconcileSuccess'));
|
|
532
|
+
} catch (error) {
|
|
533
|
+
showToastHandler?.(
|
|
534
|
+
'error',
|
|
535
|
+
getErrorMessage(error, t('messages.unreconcileError'))
|
|
536
|
+
);
|
|
537
|
+
} finally {
|
|
538
|
+
setUnreconcilingId(null);
|
|
539
|
+
}
|
|
540
|
+
};
|
|
541
|
+
|
|
542
|
+
const toggleSettlementExpanded = (settlementId: string) => {
|
|
543
|
+
setExpandedSettlementRows((current) => ({
|
|
544
|
+
...current,
|
|
545
|
+
[settlementId]: !current[settlementId],
|
|
546
|
+
}));
|
|
547
|
+
};
|
|
548
|
+
|
|
495
549
|
const handleCancel = async () => {
|
|
496
550
|
if (!canCancel || isCanceling) {
|
|
497
551
|
return;
|
|
@@ -506,7 +560,6 @@ export default function TituloDetalhePage() {
|
|
|
506
560
|
});
|
|
507
561
|
|
|
508
562
|
await refetch();
|
|
509
|
-
setIsCancelDialogOpen(false);
|
|
510
563
|
showToastHandler?.('success', t('messages.cancelSuccess'));
|
|
511
564
|
} catch (error) {
|
|
512
565
|
showToastHandler?.(
|
|
@@ -534,140 +587,129 @@ export default function TituloDetalhePage() {
|
|
|
534
587
|
]}
|
|
535
588
|
actions={
|
|
536
589
|
<div className="flex items-center gap-2">
|
|
537
|
-
<
|
|
538
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
546
|
-
|
|
547
|
-
|
|
548
|
-
|
|
549
|
-
|
|
550
|
-
|
|
551
|
-
|
|
552
|
-
|
|
553
|
-
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
567
|
-
|
|
568
|
-
|
|
569
|
-
|
|
570
|
-
|
|
571
|
-
|
|
572
|
-
|
|
573
|
-
|
|
574
|
-
|
|
575
|
-
|
|
576
|
-
|
|
577
|
-
|
|
578
|
-
|
|
579
|
-
|
|
580
|
-
|
|
581
|
-
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
onClick={() => setIsCancelDialogOpen(true)}
|
|
592
|
-
>
|
|
593
|
-
<XCircle className="mr-2 h-4 w-4" />
|
|
594
|
-
{t('actions.cancel')}
|
|
595
|
-
</DropdownMenuItem>
|
|
596
|
-
</DropdownMenuContent>
|
|
597
|
-
</DropdownMenu>
|
|
598
|
-
|
|
599
|
-
<AlertDialog
|
|
600
|
-
open={isCancelDialogOpen}
|
|
601
|
-
onOpenChange={setIsCancelDialogOpen}
|
|
602
|
-
>
|
|
603
|
-
<AlertDialogContent>
|
|
604
|
-
<AlertDialogHeader>
|
|
605
|
-
<AlertDialogTitle>
|
|
606
|
-
{t('dialogs.cancel.title')}
|
|
607
|
-
</AlertDialogTitle>
|
|
608
|
-
<AlertDialogDescription>
|
|
609
|
-
{t('dialogs.cancel.description')}
|
|
610
|
-
</AlertDialogDescription>
|
|
611
|
-
</AlertDialogHeader>
|
|
612
|
-
<AlertDialogFooter>
|
|
613
|
-
<AlertDialogCancel disabled={isCanceling}>
|
|
614
|
-
{t('dialogs.cancel.cancel')}
|
|
615
|
-
</AlertDialogCancel>
|
|
616
|
-
<AlertDialogAction
|
|
617
|
-
disabled={isCanceling}
|
|
618
|
-
onClick={() => void handleCancel()}
|
|
619
|
-
>
|
|
620
|
-
{t('dialogs.cancel.confirm')}
|
|
621
|
-
</AlertDialogAction>
|
|
622
|
-
</AlertDialogFooter>
|
|
623
|
-
</AlertDialogContent>
|
|
624
|
-
</AlertDialog>
|
|
590
|
+
<FinanceTitleActionsMenu
|
|
591
|
+
triggerVariant="outline"
|
|
592
|
+
canEdit={canEdit}
|
|
593
|
+
canApprove={canApprove}
|
|
594
|
+
canSettle={canSettle && settleCandidates.length > 0}
|
|
595
|
+
canReverse={canReverse}
|
|
596
|
+
canCancel={canCancel}
|
|
597
|
+
isApproving={isApproving}
|
|
598
|
+
isReversing={!!reversingSettlementId}
|
|
599
|
+
isCanceling={isCanceling}
|
|
600
|
+
labels={{
|
|
601
|
+
menu: t('actions.title'),
|
|
602
|
+
srActions: t('actions.title'),
|
|
603
|
+
edit: t('actions.edit'),
|
|
604
|
+
approve: t('actions.approve'),
|
|
605
|
+
settle: t('actions.settle'),
|
|
606
|
+
reverse: t('actions.reverse'),
|
|
607
|
+
cancel: t('actions.cancel'),
|
|
608
|
+
}}
|
|
609
|
+
dialogs={{
|
|
610
|
+
cancelTitle: t('dialogs.cancel.title'),
|
|
611
|
+
cancelDescription: t('dialogs.cancel.description'),
|
|
612
|
+
cancelButton: t('dialogs.cancel.cancel'),
|
|
613
|
+
confirmCancelButton: t('dialogs.cancel.confirm'),
|
|
614
|
+
reverseTitle: t('dialogs.reverse.title'),
|
|
615
|
+
reverseDescription: t('dialogs.reverse.description'),
|
|
616
|
+
reverseReasonLabel: t('dialogs.reverse.reasonLabel'),
|
|
617
|
+
reverseReasonPlaceholder: t(
|
|
618
|
+
'dialogs.reverse.reasonPlaceholder'
|
|
619
|
+
),
|
|
620
|
+
reverseButton: t('dialogs.reverse.cancel'),
|
|
621
|
+
confirmReverseButton: t('dialogs.reverse.confirm'),
|
|
622
|
+
}}
|
|
623
|
+
onEdit={() =>
|
|
624
|
+
router.push(
|
|
625
|
+
`/finance/accounts-payable/installments?editId=${titulo.id}`
|
|
626
|
+
)
|
|
627
|
+
}
|
|
628
|
+
onApprove={() => void handleApprove()}
|
|
629
|
+
onSettle={() => setIsSettleDialogOpen(true)}
|
|
630
|
+
onReverse={(reason) => {
|
|
631
|
+
const latestSettlementId = reversibleSettlements[0]?.normal?.id;
|
|
632
|
+
|
|
633
|
+
if (!latestSettlementId) {
|
|
634
|
+
return;
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
return handleReverseSettlement(
|
|
638
|
+
String(latestSettlementId),
|
|
639
|
+
reason
|
|
640
|
+
);
|
|
641
|
+
}}
|
|
642
|
+
onCancel={() => handleCancel()}
|
|
643
|
+
/>
|
|
625
644
|
|
|
626
|
-
<
|
|
645
|
+
<Sheet
|
|
627
646
|
open={isReverseDialogOpen}
|
|
628
647
|
onOpenChange={(open) => {
|
|
629
648
|
setIsReverseDialogOpen(open);
|
|
630
649
|
|
|
631
650
|
if (!open) {
|
|
632
651
|
setSelectedSettlementIdToReverse(null);
|
|
652
|
+
setReverseReason('');
|
|
633
653
|
}
|
|
634
654
|
}}
|
|
635
655
|
>
|
|
636
|
-
<
|
|
637
|
-
<
|
|
638
|
-
<
|
|
639
|
-
|
|
640
|
-
</AlertDialogTitle>
|
|
641
|
-
<AlertDialogDescription>
|
|
656
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
657
|
+
<SheetHeader>
|
|
658
|
+
<SheetTitle>{t('dialogs.reverse.title')}</SheetTitle>
|
|
659
|
+
<SheetDescription>
|
|
642
660
|
{t('dialogs.reverse.description')}
|
|
643
|
-
</
|
|
644
|
-
</
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
661
|
+
</SheetDescription>
|
|
662
|
+
</SheetHeader>
|
|
663
|
+
|
|
664
|
+
<div className="space-y-4 px-4">
|
|
665
|
+
<div className="space-y-2">
|
|
666
|
+
<Label>{t('dialogs.reverse.reasonLabel')}</Label>
|
|
667
|
+
<Input
|
|
668
|
+
value={reverseReason}
|
|
669
|
+
onChange={(event) => setReverseReason(event.target.value)}
|
|
670
|
+
placeholder={t('dialogs.reverse.reasonPlaceholder')}
|
|
671
|
+
maxLength={255}
|
|
672
|
+
disabled={!!reversingSettlementId}
|
|
673
|
+
/>
|
|
674
|
+
</div>
|
|
657
675
|
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
676
|
+
<div className="flex flex-col gap-2">
|
|
677
|
+
<Button
|
|
678
|
+
disabled={
|
|
679
|
+
!!reversingSettlementId ||
|
|
680
|
+
!selectedSettlementIdToReverse
|
|
681
|
+
}
|
|
682
|
+
onClick={() => {
|
|
683
|
+
if (!selectedSettlementIdToReverse) {
|
|
684
|
+
return;
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
void handleReverseSettlement(
|
|
688
|
+
selectedSettlementIdToReverse
|
|
689
|
+
).finally(() => {
|
|
690
|
+
setIsReverseDialogOpen(false);
|
|
691
|
+
setSelectedSettlementIdToReverse(null);
|
|
692
|
+
setReverseReason('');
|
|
693
|
+
});
|
|
694
|
+
}}
|
|
695
|
+
>
|
|
696
|
+
{t('dialogs.reverse.confirm')}
|
|
697
|
+
</Button>
|
|
698
|
+
<Button
|
|
699
|
+
variant="outline"
|
|
700
|
+
disabled={!!reversingSettlementId}
|
|
701
|
+
onClick={() => {
|
|
661
702
|
setIsReverseDialogOpen(false);
|
|
662
703
|
setSelectedSettlementIdToReverse(null);
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
670
|
-
|
|
704
|
+
setReverseReason('');
|
|
705
|
+
}}
|
|
706
|
+
>
|
|
707
|
+
{t('dialogs.reverse.cancel')}
|
|
708
|
+
</Button>
|
|
709
|
+
</div>
|
|
710
|
+
</div>
|
|
711
|
+
</SheetContent>
|
|
712
|
+
</Sheet>
|
|
671
713
|
|
|
672
714
|
<Sheet
|
|
673
715
|
open={isSettleDialogOpen}
|
|
@@ -869,35 +911,21 @@ export default function TituloDetalhePage() {
|
|
|
869
911
|
onChange={handleChangeTags}
|
|
870
912
|
onCreateTag={handleCreateTag}
|
|
871
913
|
disabled={isCreatingTag}
|
|
872
|
-
emptyText={
|
|
914
|
+
emptyText={t('tagSelector.noTags')}
|
|
873
915
|
labels={{
|
|
874
|
-
addTag:
|
|
875
|
-
sheetTitle:
|
|
876
|
-
sheetDescription:
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
),
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
883
|
-
|
|
884
|
-
),
|
|
885
|
-
createAction: tTagSelector('createAction', 'Criar tag'),
|
|
886
|
-
popularTitle: tTagSelector(
|
|
887
|
-
'popularTitle',
|
|
888
|
-
'Tags mais usadas'
|
|
889
|
-
),
|
|
890
|
-
selectedTitle: tTagSelector(
|
|
891
|
-
'selectedTitle',
|
|
892
|
-
'Tags selecionadas'
|
|
893
|
-
),
|
|
894
|
-
noTags: tTagSelector('noTags', 'Sem tags'),
|
|
895
|
-
cancel: tTagSelector('cancel', 'Cancelar'),
|
|
896
|
-
apply: tTagSelector('apply', 'Aplicar'),
|
|
916
|
+
addTag: t('tagSelector.addTag'),
|
|
917
|
+
sheetTitle: t('tagSelector.sheetTitle'),
|
|
918
|
+
sheetDescription: t('tagSelector.sheetDescription'),
|
|
919
|
+
createLabel: t('tagSelector.createLabel'),
|
|
920
|
+
createPlaceholder: t('tagSelector.createPlaceholder'),
|
|
921
|
+
createAction: t('tagSelector.createAction'),
|
|
922
|
+
popularTitle: t('tagSelector.popularTitle'),
|
|
923
|
+
selectedTitle: t('tagSelector.selectedTitle'),
|
|
924
|
+
noTags: t('tagSelector.noTags'),
|
|
925
|
+
cancel: t('tagSelector.cancel'),
|
|
926
|
+
apply: t('tagSelector.apply'),
|
|
897
927
|
removeTagAria: (tagName: string) =>
|
|
898
|
-
t
|
|
899
|
-
? t('tagSelector.removeTagAria', { tag: tagName })
|
|
900
|
-
: `Remover tag ${tagName}`,
|
|
928
|
+
t('tagSelector.removeTagAria', { tag: tagName }),
|
|
901
929
|
}}
|
|
902
930
|
/>
|
|
903
931
|
</dd>
|
|
@@ -985,105 +1013,233 @@ export default function TituloDetalhePage() {
|
|
|
985
1013
|
<TabsContent value="liquidacoes" className="mt-4">
|
|
986
1014
|
<Card>
|
|
987
1015
|
<CardContent className="pt-6">
|
|
988
|
-
{
|
|
1016
|
+
{isFetchingSettlementsHistory ? (
|
|
1017
|
+
<div className="flex items-center justify-center py-8 text-muted-foreground">
|
|
1018
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1019
|
+
{t('settlementsHistory.loading')}
|
|
1020
|
+
</div>
|
|
1021
|
+
) : settlementsHistory.length > 0 ? (
|
|
989
1022
|
<Table>
|
|
990
1023
|
<TableHeader>
|
|
991
1024
|
<TableRow>
|
|
992
|
-
<TableHead>
|
|
993
|
-
|
|
994
|
-
{t('settlementsTable.value')}
|
|
1025
|
+
<TableHead>
|
|
1026
|
+
{t('settlementsHistory.headers.date')}
|
|
995
1027
|
</TableHead>
|
|
996
|
-
<TableHead
|
|
997
|
-
{t('
|
|
1028
|
+
<TableHead>
|
|
1029
|
+
{t('settlementsHistory.headers.type')}
|
|
998
1030
|
</TableHead>
|
|
999
|
-
<TableHead
|
|
1000
|
-
{t('
|
|
1031
|
+
<TableHead>
|
|
1032
|
+
{t('settlementsHistory.headers.status')}
|
|
1001
1033
|
</TableHead>
|
|
1002
1034
|
<TableHead className="text-right">
|
|
1003
|
-
{t('
|
|
1035
|
+
{t('settlementsHistory.headers.netAmount')}
|
|
1036
|
+
</TableHead>
|
|
1037
|
+
<TableHead>
|
|
1038
|
+
{t('settlementsHistory.headers.method')}
|
|
1039
|
+
</TableHead>
|
|
1040
|
+
<TableHead>
|
|
1041
|
+
{t('settlementsHistory.headers.account')}
|
|
1042
|
+
</TableHead>
|
|
1043
|
+
<TableHead>
|
|
1044
|
+
{t('settlementsHistory.headers.reconciled')}
|
|
1004
1045
|
</TableHead>
|
|
1005
|
-
<TableHead>{t('settlementsTable.account')}</TableHead>
|
|
1006
|
-
<TableHead>{t('settlementsTable.method')}</TableHead>
|
|
1007
1046
|
<TableHead className="text-right">
|
|
1008
|
-
{t('
|
|
1047
|
+
{t('settlementsHistory.headers.actions')}
|
|
1009
1048
|
</TableHead>
|
|
1010
1049
|
</TableRow>
|
|
1011
1050
|
</TableHeader>
|
|
1012
1051
|
<TableBody>
|
|
1013
|
-
{
|
|
1014
|
-
|
|
1015
|
-
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1052
|
+
{settlementsHistory.map((group) => {
|
|
1053
|
+
const normalId = group.normal.id;
|
|
1054
|
+
const isExpanded = !!expandedSettlementRows[normalId];
|
|
1055
|
+
const canReverseNormal =
|
|
1056
|
+
group.statusLabel === 'ATIVO' &&
|
|
1057
|
+
!group.normal.reconciled &&
|
|
1058
|
+
!reversingSettlementId;
|
|
1059
|
+
|
|
1060
|
+
return (
|
|
1061
|
+
<Fragment key={normalId}>
|
|
1062
|
+
<TableRow>
|
|
1063
|
+
<TableCell>
|
|
1064
|
+
{formatarData(group.normal.paidAt)}
|
|
1021
1065
|
</TableCell>
|
|
1022
|
-
<TableCell
|
|
1023
|
-
<
|
|
1066
|
+
<TableCell>
|
|
1067
|
+
<span className="rounded-md border px-2 py-1 text-xs font-medium">
|
|
1068
|
+
{t('settlementsHistory.normalType')}
|
|
1069
|
+
</span>
|
|
1024
1070
|
</TableCell>
|
|
1025
|
-
<TableCell
|
|
1026
|
-
<
|
|
1071
|
+
<TableCell>
|
|
1072
|
+
<span
|
|
1073
|
+
className={`rounded-md px-2 py-1 text-xs font-medium ${
|
|
1074
|
+
group.statusLabel === 'ESTORNADO'
|
|
1075
|
+
? 'bg-muted text-muted-foreground'
|
|
1076
|
+
: 'bg-primary/10 text-primary'
|
|
1077
|
+
}`}
|
|
1078
|
+
>
|
|
1079
|
+
{group.statusLabel}
|
|
1080
|
+
</span>
|
|
1027
1081
|
</TableCell>
|
|
1028
|
-
<TableCell className="text-right">
|
|
1029
|
-
|
|
1082
|
+
<TableCell className="text-right font-medium">
|
|
1083
|
+
+
|
|
1084
|
+
<Money
|
|
1085
|
+
value={
|
|
1086
|
+
Math.abs(group.normal.amountCents || 0) / 100
|
|
1087
|
+
}
|
|
1088
|
+
/>
|
|
1030
1089
|
</TableCell>
|
|
1031
|
-
<TableCell>{conta?.descricao}</TableCell>
|
|
1032
1090
|
<TableCell className="capitalize">
|
|
1033
|
-
{
|
|
1091
|
+
{group.normal.method || '-'}
|
|
1092
|
+
</TableCell>
|
|
1093
|
+
<TableCell>{group.normal.account || '-'}</TableCell>
|
|
1094
|
+
<TableCell>
|
|
1095
|
+
<span
|
|
1096
|
+
className={`rounded-md px-2 py-1 text-xs font-medium ${
|
|
1097
|
+
group.normal.reconciled
|
|
1098
|
+
? 'bg-primary/10 text-primary'
|
|
1099
|
+
: 'bg-muted text-muted-foreground'
|
|
1100
|
+
}`}
|
|
1101
|
+
>
|
|
1102
|
+
{group.normal.reconciled
|
|
1103
|
+
? t('settlementsHistory.reconciled.yes')
|
|
1104
|
+
: t('settlementsHistory.reconciled.no')}
|
|
1105
|
+
</span>
|
|
1034
1106
|
</TableCell>
|
|
1035
1107
|
<TableCell className="text-right">
|
|
1036
|
-
<
|
|
1037
|
-
|
|
1108
|
+
<div className="flex items-center justify-end gap-2">
|
|
1109
|
+
{group.normal.reconciled &&
|
|
1110
|
+
group.normal.reconciliationId ? (
|
|
1038
1111
|
<Button
|
|
1039
1112
|
variant="outline"
|
|
1040
1113
|
size="sm"
|
|
1041
1114
|
disabled={
|
|
1042
|
-
|
|
1043
|
-
|
|
1044
|
-
|
|
1115
|
+
unreconcilingId ===
|
|
1116
|
+
group.normal.reconciliationId
|
|
1117
|
+
}
|
|
1118
|
+
onClick={() =>
|
|
1119
|
+
void handleUnreconcileSettlement(
|
|
1120
|
+
String(group.normal.reconciliationId)
|
|
1121
|
+
)
|
|
1045
1122
|
}
|
|
1046
1123
|
>
|
|
1047
|
-
|
|
1048
|
-
|
|
1049
|
-
|
|
1050
|
-
|
|
1051
|
-
<AlertDialogContent>
|
|
1052
|
-
<AlertDialogHeader>
|
|
1053
|
-
<AlertDialogTitle>
|
|
1054
|
-
{t('settlementsTable.reverseDialogTitle')}
|
|
1055
|
-
</AlertDialogTitle>
|
|
1056
|
-
<AlertDialogDescription>
|
|
1057
|
-
{t(
|
|
1058
|
-
'settlementsTable.reverseDialogDescription'
|
|
1059
|
-
)}
|
|
1060
|
-
</AlertDialogDescription>
|
|
1061
|
-
</AlertDialogHeader>
|
|
1062
|
-
<AlertDialogFooter>
|
|
1063
|
-
<AlertDialogCancel>
|
|
1064
|
-
{t(
|
|
1065
|
-
'settlementsTable.reverseDialogCancel'
|
|
1066
|
-
)}
|
|
1067
|
-
</AlertDialogCancel>
|
|
1068
|
-
<AlertDialogAction
|
|
1069
|
-
onClick={() =>
|
|
1070
|
-
void handleReverseSettlement(
|
|
1071
|
-
String(liq.settlementId)
|
|
1124
|
+
{unreconcilingId ===
|
|
1125
|
+
group.normal.reconciliationId
|
|
1126
|
+
? t(
|
|
1127
|
+
'settlementsHistory.unreconcileLoading'
|
|
1072
1128
|
)
|
|
1073
|
-
}
|
|
1074
|
-
|
|
1075
|
-
|
|
1076
|
-
|
|
1077
|
-
|
|
1078
|
-
|
|
1079
|
-
|
|
1080
|
-
|
|
1081
|
-
|
|
1129
|
+
: t('settlementsHistory.unreconcile')}
|
|
1130
|
+
</Button>
|
|
1131
|
+
) : null}
|
|
1132
|
+
|
|
1133
|
+
<Button
|
|
1134
|
+
variant="outline"
|
|
1135
|
+
size="sm"
|
|
1136
|
+
title={
|
|
1137
|
+
group.normal.reconciled
|
|
1138
|
+
? t('settlementsHistory.unreconcileFirst')
|
|
1139
|
+
: undefined
|
|
1140
|
+
}
|
|
1141
|
+
disabled={!canReverseNormal}
|
|
1142
|
+
onClick={() => {
|
|
1143
|
+
setSelectedSettlementIdToReverse(normalId);
|
|
1144
|
+
setReverseReason('');
|
|
1145
|
+
setIsReverseDialogOpen(true);
|
|
1146
|
+
}}
|
|
1147
|
+
>
|
|
1148
|
+
<Undo className="mr-2 h-4 w-4" />
|
|
1149
|
+
{t('settlementsHistory.reverse')}
|
|
1150
|
+
</Button>
|
|
1151
|
+
|
|
1152
|
+
<Button
|
|
1153
|
+
variant="ghost"
|
|
1154
|
+
size="sm"
|
|
1155
|
+
onClick={() =>
|
|
1156
|
+
toggleSettlementExpanded(normalId)
|
|
1157
|
+
}
|
|
1158
|
+
>
|
|
1159
|
+
{isExpanded ? (
|
|
1160
|
+
<ChevronUp className="h-4 w-4" />
|
|
1161
|
+
) : (
|
|
1162
|
+
<ChevronDown className="h-4 w-4" />
|
|
1163
|
+
)}
|
|
1164
|
+
</Button>
|
|
1165
|
+
</div>
|
|
1082
1166
|
</TableCell>
|
|
1083
1167
|
</TableRow>
|
|
1084
|
-
|
|
1085
|
-
|
|
1086
|
-
|
|
1168
|
+
|
|
1169
|
+
{group.reversal ? (
|
|
1170
|
+
<TableRow>
|
|
1171
|
+
<TableCell className="pl-8">
|
|
1172
|
+
{formatarData(group.reversal.paidAt)}
|
|
1173
|
+
</TableCell>
|
|
1174
|
+
<TableCell>
|
|
1175
|
+
<span className="rounded-md border px-2 py-1 text-xs font-medium">
|
|
1176
|
+
{t('settlementsHistory.reversalType')}
|
|
1177
|
+
</span>
|
|
1178
|
+
</TableCell>
|
|
1179
|
+
<TableCell>
|
|
1180
|
+
<span className="rounded-md bg-muted px-2 py-1 text-xs font-medium text-muted-foreground">
|
|
1181
|
+
{t('settlementsHistory.reversedStatus')}
|
|
1182
|
+
</span>
|
|
1183
|
+
</TableCell>
|
|
1184
|
+
<TableCell className="text-right font-medium text-destructive">
|
|
1185
|
+
-
|
|
1186
|
+
<Money
|
|
1187
|
+
value={
|
|
1188
|
+
Math.abs(group.reversal.amountCents || 0) /
|
|
1189
|
+
100
|
|
1190
|
+
}
|
|
1191
|
+
/>
|
|
1192
|
+
</TableCell>
|
|
1193
|
+
<TableCell>-</TableCell>
|
|
1194
|
+
<TableCell>-</TableCell>
|
|
1195
|
+
<TableCell>-</TableCell>
|
|
1196
|
+
<TableCell className="text-right text-xs text-muted-foreground">
|
|
1197
|
+
{group.reversal.memo || '-'}
|
|
1198
|
+
</TableCell>
|
|
1199
|
+
</TableRow>
|
|
1200
|
+
) : null}
|
|
1201
|
+
|
|
1202
|
+
{isExpanded ? (
|
|
1203
|
+
<TableRow>
|
|
1204
|
+
<TableCell colSpan={8}>
|
|
1205
|
+
<div className="rounded-md border bg-muted/20 p-3">
|
|
1206
|
+
<p className="mb-2 text-xs font-medium text-muted-foreground">
|
|
1207
|
+
{t('settlementsHistory.allocationsTitle')}
|
|
1208
|
+
</p>
|
|
1209
|
+
<div className="space-y-1">
|
|
1210
|
+
{group.allocations.map((allocation) => (
|
|
1211
|
+
<div
|
|
1212
|
+
key={`${normalId}-${allocation.installmentId}`}
|
|
1213
|
+
className="flex items-center justify-between text-sm"
|
|
1214
|
+
>
|
|
1215
|
+
<span>
|
|
1216
|
+
{t(
|
|
1217
|
+
'settlementsHistory.allocationInstallment'
|
|
1218
|
+
)}{' '}
|
|
1219
|
+
#{allocation.installmentSeq}
|
|
1220
|
+
</span>
|
|
1221
|
+
<span className="font-medium">
|
|
1222
|
+
+
|
|
1223
|
+
<Money
|
|
1224
|
+
value={
|
|
1225
|
+
Math.abs(
|
|
1226
|
+
Number(
|
|
1227
|
+
allocation.amountCents || 0
|
|
1228
|
+
)
|
|
1229
|
+
) / 100
|
|
1230
|
+
}
|
|
1231
|
+
/>
|
|
1232
|
+
</span>
|
|
1233
|
+
</div>
|
|
1234
|
+
))}
|
|
1235
|
+
</div>
|
|
1236
|
+
</div>
|
|
1237
|
+
</TableCell>
|
|
1238
|
+
</TableRow>
|
|
1239
|
+
) : null}
|
|
1240
|
+
</Fragment>
|
|
1241
|
+
);
|
|
1242
|
+
})}
|
|
1087
1243
|
</TableBody>
|
|
1088
1244
|
</Table>
|
|
1089
1245
|
) : (
|