@hed-hog/finance 0.0.237 → 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.
- package/dist/dto/create-finance-tag.dto.d.ts +5 -0
- package/dist/dto/create-finance-tag.dto.d.ts.map +1 -0
- package/dist/dto/create-finance-tag.dto.js +29 -0
- package/dist/dto/create-finance-tag.dto.js.map +1 -0
- package/dist/dto/reject-title.dto.d.ts +4 -0
- package/dist/dto/reject-title.dto.d.ts.map +1 -0
- package/dist/dto/reject-title.dto.js +22 -0
- package/dist/dto/reject-title.dto.js.map +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts +4 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -0
- package/dist/dto/reverse-settlement.dto.js +22 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -0
- package/dist/dto/settle-installment.dto.d.ts +12 -0
- package/dist/dto/settle-installment.dto.d.ts.map +1 -0
- package/dist/dto/settle-installment.dto.js +71 -0
- package/dist/dto/settle-installment.dto.js.map +1 -0
- package/dist/dto/update-installment-tags.dto.d.ts +4 -0
- package/dist/dto/update-installment-tags.dto.d.ts.map +1 -0
- package/dist/dto/update-installment-tags.dto.js +27 -0
- package/dist/dto/update-installment-tags.dto.js.map +1 -0
- package/dist/finance-data.controller.d.ts +17 -5
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +325 -8
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +128 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +357 -13
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +835 -64
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +90 -0
- package/hedhog/frontend/app/_lib/use-finance-data.ts.ejs +2 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +601 -79
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +481 -19
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +598 -69
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +472 -15
- package/hedhog/frontend/messages/en.json +38 -0
- package/hedhog/frontend/messages/pt.json +38 -0
- package/package.json +5 -5
- package/src/dto/create-finance-tag.dto.ts +15 -0
- package/src/dto/reject-title.dto.ts +7 -0
- package/src/dto/reverse-settlement.dto.ts +7 -0
- package/src/dto/settle-installment.dto.ts +55 -0
- package/src/dto/update-installment-tags.dto.ts +12 -0
- package/src/finance-installments.controller.ts +145 -9
- package/src/finance.service.ts +1333 -165
|
@@ -1,8 +1,18 @@
|
|
|
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
|
-
import { Badge } from '@/components/ui/badge';
|
|
6
16
|
import { Button } from '@/components/ui/button';
|
|
7
17
|
import {
|
|
8
18
|
Card,
|
|
@@ -11,6 +21,14 @@ import {
|
|
|
11
21
|
CardHeader,
|
|
12
22
|
CardTitle,
|
|
13
23
|
} from '@/components/ui/card';
|
|
24
|
+
import {
|
|
25
|
+
Dialog,
|
|
26
|
+
DialogContent,
|
|
27
|
+
DialogDescription,
|
|
28
|
+
DialogFooter,
|
|
29
|
+
DialogHeader,
|
|
30
|
+
DialogTitle,
|
|
31
|
+
} from '@/components/ui/dialog';
|
|
14
32
|
import {
|
|
15
33
|
DropdownMenu,
|
|
16
34
|
DropdownMenuContent,
|
|
@@ -18,7 +36,24 @@ import {
|
|
|
18
36
|
DropdownMenuSeparator,
|
|
19
37
|
DropdownMenuTrigger,
|
|
20
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';
|
|
21
49
|
import { Money } from '@/components/ui/money';
|
|
50
|
+
import {
|
|
51
|
+
Select,
|
|
52
|
+
SelectContent,
|
|
53
|
+
SelectItem,
|
|
54
|
+
SelectTrigger,
|
|
55
|
+
SelectValue,
|
|
56
|
+
} from '@/components/ui/select';
|
|
22
57
|
import { StatusBadge } from '@/components/ui/status-badge';
|
|
23
58
|
import {
|
|
24
59
|
Table,
|
|
@@ -29,8 +64,10 @@ import {
|
|
|
29
64
|
TableRow,
|
|
30
65
|
} from '@/components/ui/table';
|
|
31
66
|
import { Tabs, TabsContent, TabsList, TabsTrigger } from '@/components/ui/tabs';
|
|
67
|
+
import { TagSelectorSheet } from '@/components/ui/tag-selector-sheet';
|
|
68
|
+
import { useApp } from '@hed-hog/next-app-provider';
|
|
69
|
+
import { zodResolver } from '@hookform/resolvers/zod';
|
|
32
70
|
import {
|
|
33
|
-
ArrowLeft,
|
|
34
71
|
CheckCircle,
|
|
35
72
|
Download,
|
|
36
73
|
Edit,
|
|
@@ -42,14 +79,26 @@ import {
|
|
|
42
79
|
import { useTranslations } from 'next-intl';
|
|
43
80
|
import Link from 'next/link';
|
|
44
81
|
import { useParams } from 'next/navigation';
|
|
82
|
+
import { useEffect, useState } from 'react';
|
|
83
|
+
import { useForm } from 'react-hook-form';
|
|
84
|
+
import { z } from 'zod';
|
|
45
85
|
import { formatarData } from '../../../_lib/formatters';
|
|
46
86
|
import { useFinanceData } from '../../../_lib/use-finance-data';
|
|
47
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
|
+
|
|
48
96
|
export default function TituloDetalhePage() {
|
|
49
97
|
const t = useTranslations('finance.PayableInstallmentDetailPage');
|
|
98
|
+
const { request, showToastHandler } = useApp();
|
|
50
99
|
const params = useParams<{ id: string }>();
|
|
51
100
|
const id = params?.id;
|
|
52
|
-
const { data } = useFinanceData();
|
|
101
|
+
const { data, refetch } = useFinanceData();
|
|
53
102
|
const {
|
|
54
103
|
titulosPagar,
|
|
55
104
|
pessoas,
|
|
@@ -62,6 +111,65 @@ export default function TituloDetalhePage() {
|
|
|
62
111
|
|
|
63
112
|
const titulo = titulosPagar.find((t) => t.id === id);
|
|
64
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
|
+
|
|
130
|
+
const [availableTags, setAvailableTags] = useState<any[]>([]);
|
|
131
|
+
const [selectedTagIds, setSelectedTagIds] = useState<string[]>([]);
|
|
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);
|
|
139
|
+
|
|
140
|
+
useEffect(() => {
|
|
141
|
+
setAvailableTags(tags || []);
|
|
142
|
+
}, [tags]);
|
|
143
|
+
|
|
144
|
+
useEffect(() => {
|
|
145
|
+
if (!titulo) {
|
|
146
|
+
return;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
setSelectedTagIds(Array.isArray(titulo.tags) ? titulo.tags : []);
|
|
150
|
+
}, [titulo]);
|
|
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
|
+
|
|
65
173
|
if (!titulo) {
|
|
66
174
|
return (
|
|
67
175
|
<div className="space-y-6">
|
|
@@ -95,9 +203,20 @@ export default function TituloDetalhePage() {
|
|
|
95
203
|
const fornecedor = getPessoaById(titulo.fornecedorId);
|
|
96
204
|
const categoria = getCategoriaById(titulo.categoriaId);
|
|
97
205
|
const centroCusto = getCentroCustoById(titulo.centroCustoId);
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
.
|
|
206
|
+
|
|
207
|
+
const tagOptions = availableTags.map((tag) => ({
|
|
208
|
+
id: String(tag.id),
|
|
209
|
+
name: String(tag.nome || ''),
|
|
210
|
+
color: tag.cor,
|
|
211
|
+
usageCount:
|
|
212
|
+
typeof tag.usageCount === 'number'
|
|
213
|
+
? tag.usageCount
|
|
214
|
+
: typeof tag.usoCount === 'number'
|
|
215
|
+
? tag.usoCount
|
|
216
|
+
: typeof tag.count === 'number'
|
|
217
|
+
? tag.count
|
|
218
|
+
: undefined,
|
|
219
|
+
}));
|
|
101
220
|
|
|
102
221
|
const auditEvents = logsAuditoria
|
|
103
222
|
.filter(
|
|
@@ -113,64 +232,398 @@ export default function TituloDetalhePage() {
|
|
|
113
232
|
depois: log.depois,
|
|
114
233
|
}));
|
|
115
234
|
|
|
235
|
+
const attachmentDetails = Array.isArray(titulo.anexosDetalhes)
|
|
236
|
+
? titulo.anexosDetalhes
|
|
237
|
+
: (titulo.anexos || []).map((nome: string) => ({ nome }));
|
|
238
|
+
|
|
239
|
+
const handleOpenAttachment = async (fileId?: string) => {
|
|
240
|
+
if (!fileId) {
|
|
241
|
+
return;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const response = await request<{ url?: string }>({
|
|
245
|
+
url: `/file/open/${fileId}`,
|
|
246
|
+
method: 'PUT',
|
|
247
|
+
});
|
|
248
|
+
|
|
249
|
+
const url = response?.data?.url;
|
|
250
|
+
if (!url) {
|
|
251
|
+
return;
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
255
|
+
};
|
|
256
|
+
|
|
257
|
+
const tTagSelector = (key: string, fallback: string) => {
|
|
258
|
+
const fullKey = `tagSelector.${key}`;
|
|
259
|
+
return t.has(fullKey) ? t(fullKey) : fallback;
|
|
260
|
+
};
|
|
261
|
+
|
|
262
|
+
const toTagSlug = (value: string) => {
|
|
263
|
+
return value
|
|
264
|
+
.normalize('NFD')
|
|
265
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
266
|
+
.toLowerCase()
|
|
267
|
+
.trim()
|
|
268
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
269
|
+
.replace(/(^-|-$)+/g, '');
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const handleCreateTag = async (name: string) => {
|
|
273
|
+
const slug = toTagSlug(name);
|
|
274
|
+
|
|
275
|
+
if (!slug) {
|
|
276
|
+
return null;
|
|
277
|
+
}
|
|
278
|
+
|
|
279
|
+
setIsCreatingTag(true);
|
|
280
|
+
try {
|
|
281
|
+
const response = await request<{
|
|
282
|
+
id?: string | number;
|
|
283
|
+
nome?: string;
|
|
284
|
+
cor?: string;
|
|
285
|
+
}>({
|
|
286
|
+
url: '/finance/tags',
|
|
287
|
+
method: 'POST',
|
|
288
|
+
data: {
|
|
289
|
+
name: slug,
|
|
290
|
+
color: '#000000',
|
|
291
|
+
},
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const created = response?.data;
|
|
295
|
+
const newTag = {
|
|
296
|
+
id: String(created?.id || `temp-${Date.now()}`),
|
|
297
|
+
nome: created?.nome || slug,
|
|
298
|
+
cor: created?.cor || '#000000',
|
|
299
|
+
};
|
|
300
|
+
|
|
301
|
+
setAvailableTags((current) => {
|
|
302
|
+
if (current.some((tag) => String(tag.id) === newTag.id)) {
|
|
303
|
+
return current;
|
|
304
|
+
}
|
|
305
|
+
|
|
306
|
+
return [...current, newTag];
|
|
307
|
+
});
|
|
308
|
+
|
|
309
|
+
showToastHandler?.(
|
|
310
|
+
'success',
|
|
311
|
+
tTagSelector('messages.createSuccess', 'Tag criada com sucesso')
|
|
312
|
+
);
|
|
313
|
+
|
|
314
|
+
return {
|
|
315
|
+
id: newTag.id,
|
|
316
|
+
name: newTag.nome,
|
|
317
|
+
color: newTag.cor,
|
|
318
|
+
};
|
|
319
|
+
} catch {
|
|
320
|
+
showToastHandler?.(
|
|
321
|
+
'error',
|
|
322
|
+
tTagSelector('messages.createError', 'Não foi possível criar a tag')
|
|
323
|
+
);
|
|
324
|
+
return null;
|
|
325
|
+
} finally {
|
|
326
|
+
setIsCreatingTag(false);
|
|
327
|
+
}
|
|
328
|
+
};
|
|
329
|
+
|
|
330
|
+
const handleChangeTags = async (nextTagIds: string[]) => {
|
|
331
|
+
if (!titulo?.id) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
try {
|
|
336
|
+
const response = await request<{ tags?: string[] }>({
|
|
337
|
+
url: `/finance/accounts-payable/installments/${titulo.id}/tags`,
|
|
338
|
+
method: 'PATCH',
|
|
339
|
+
data: {
|
|
340
|
+
tag_ids: nextTagIds.map((tagId) => Number(tagId)),
|
|
341
|
+
},
|
|
342
|
+
});
|
|
343
|
+
|
|
344
|
+
if (Array.isArray(response?.data?.tags)) {
|
|
345
|
+
setSelectedTagIds(response.data.tags);
|
|
346
|
+
} else {
|
|
347
|
+
setSelectedTagIds(nextTagIds);
|
|
348
|
+
}
|
|
349
|
+
} catch {
|
|
350
|
+
showToastHandler?.(
|
|
351
|
+
'error',
|
|
352
|
+
tTagSelector(
|
|
353
|
+
'messages.updateError',
|
|
354
|
+
'Não foi possível atualizar as tags'
|
|
355
|
+
)
|
|
356
|
+
);
|
|
357
|
+
}
|
|
358
|
+
};
|
|
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
|
+
|
|
116
456
|
return (
|
|
117
457
|
<Page>
|
|
118
|
-
<
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
|
|
139
|
-
<
|
|
140
|
-
<
|
|
141
|
-
<
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
<
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
<
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
<
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
458
|
+
<PageHeader
|
|
459
|
+
title={titulo.documento}
|
|
460
|
+
description={titulo.descricao}
|
|
461
|
+
breadcrumbs={[
|
|
462
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
463
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
464
|
+
{
|
|
465
|
+
label: t('breadcrumbs.payables'),
|
|
466
|
+
href: '/finance/accounts-payable/installments',
|
|
467
|
+
},
|
|
468
|
+
{ label: titulo.documento },
|
|
469
|
+
]}
|
|
470
|
+
actions={
|
|
471
|
+
<div className="flex items-center gap-2">
|
|
472
|
+
<DropdownMenu>
|
|
473
|
+
<DropdownMenuTrigger asChild>
|
|
474
|
+
<Button variant="outline">
|
|
475
|
+
<MoreHorizontal className="mr-2 h-4 w-4" />
|
|
476
|
+
{t('actions.title')}
|
|
477
|
+
</Button>
|
|
478
|
+
</DropdownMenuTrigger>
|
|
479
|
+
<DropdownMenuContent align="end">
|
|
480
|
+
<DropdownMenuItem>
|
|
481
|
+
<Edit className="mr-2 h-4 w-4" />
|
|
482
|
+
{t('actions.edit')}
|
|
483
|
+
</DropdownMenuItem>
|
|
484
|
+
<DropdownMenuItem
|
|
485
|
+
disabled={!canApprove || isApproving}
|
|
486
|
+
onClick={() => void handleApprove()}
|
|
487
|
+
>
|
|
488
|
+
<CheckCircle className="mr-2 h-4 w-4" />
|
|
489
|
+
{t('actions.approve')}
|
|
490
|
+
</DropdownMenuItem>
|
|
491
|
+
<DropdownMenuItem
|
|
492
|
+
disabled={!canSettle || settleCandidates.length === 0}
|
|
493
|
+
onClick={() => setIsSettleDialogOpen(true)}
|
|
494
|
+
>
|
|
495
|
+
<Download className="mr-2 h-4 w-4" />
|
|
496
|
+
{t('actions.settle')}
|
|
497
|
+
</DropdownMenuItem>
|
|
498
|
+
<DropdownMenuItem disabled>
|
|
499
|
+
<Undo className="mr-2 h-4 w-4" />
|
|
500
|
+
{t('actions.reverse')}
|
|
501
|
+
</DropdownMenuItem>
|
|
502
|
+
<DropdownMenuSeparator />
|
|
503
|
+
<DropdownMenuItem className="text-destructive">
|
|
504
|
+
<XCircle className="mr-2 h-4 w-4" />
|
|
505
|
+
{t('actions.cancel')}
|
|
506
|
+
</DropdownMenuItem>
|
|
507
|
+
</DropdownMenuContent>
|
|
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>
|
|
624
|
+
</div>
|
|
625
|
+
}
|
|
626
|
+
/>
|
|
174
627
|
|
|
175
628
|
<div className="grid gap-6 lg:grid-cols-3">
|
|
176
629
|
<Card className="lg:col-span-2">
|
|
@@ -234,20 +687,44 @@ export default function TituloDetalhePage() {
|
|
|
234
687
|
<dt className="text-sm font-medium text-muted-foreground">
|
|
235
688
|
{t('documentData.tags')}
|
|
236
689
|
</dt>
|
|
237
|
-
<dd className="mt-1
|
|
238
|
-
|
|
239
|
-
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
|
|
246
|
-
|
|
247
|
-
|
|
248
|
-
|
|
249
|
-
|
|
250
|
-
|
|
690
|
+
<dd className="mt-1">
|
|
691
|
+
<TagSelectorSheet
|
|
692
|
+
selectedTagIds={selectedTagIds}
|
|
693
|
+
tags={tagOptions}
|
|
694
|
+
onChange={handleChangeTags}
|
|
695
|
+
onCreateTag={handleCreateTag}
|
|
696
|
+
disabled={isCreatingTag}
|
|
697
|
+
emptyText={tTagSelector('noTags', 'Sem tags')}
|
|
698
|
+
labels={{
|
|
699
|
+
addTag: tTagSelector('addTag', 'Adicionar tag'),
|
|
700
|
+
sheetTitle: tTagSelector('sheetTitle', 'Gerenciar tags'),
|
|
701
|
+
sheetDescription: tTagSelector(
|
|
702
|
+
'sheetDescription',
|
|
703
|
+
'Selecione tags existentes ou crie uma nova.'
|
|
704
|
+
),
|
|
705
|
+
createLabel: tTagSelector('createLabel', 'Nova tag'),
|
|
706
|
+
createPlaceholder: tTagSelector(
|
|
707
|
+
'createPlaceholder',
|
|
708
|
+
'Digite o nome da tag'
|
|
709
|
+
),
|
|
710
|
+
createAction: tTagSelector('createAction', 'Criar tag'),
|
|
711
|
+
popularTitle: tTagSelector(
|
|
712
|
+
'popularTitle',
|
|
713
|
+
'Tags mais usadas'
|
|
714
|
+
),
|
|
715
|
+
selectedTitle: tTagSelector(
|
|
716
|
+
'selectedTitle',
|
|
717
|
+
'Tags selecionadas'
|
|
718
|
+
),
|
|
719
|
+
noTags: tTagSelector('noTags', 'Sem tags'),
|
|
720
|
+
cancel: tTagSelector('cancel', 'Cancelar'),
|
|
721
|
+
apply: tTagSelector('apply', 'Aplicar'),
|
|
722
|
+
removeTagAria: (tagName: string) =>
|
|
723
|
+
t.has('tagSelector.removeTagAria')
|
|
724
|
+
? t('tagSelector.removeTagAria', { tag: tagName })
|
|
725
|
+
: `Remover tag ${tagName}`,
|
|
726
|
+
}}
|
|
727
|
+
/>
|
|
251
728
|
</dd>
|
|
252
729
|
</div>
|
|
253
730
|
</dl>
|
|
@@ -260,16 +737,17 @@ export default function TituloDetalhePage() {
|
|
|
260
737
|
<CardDescription>{t('attachments.description')}</CardDescription>
|
|
261
738
|
</CardHeader>
|
|
262
739
|
<CardContent>
|
|
263
|
-
{
|
|
740
|
+
{attachmentDetails.length > 0 ? (
|
|
264
741
|
<ul className="space-y-2">
|
|
265
|
-
{
|
|
742
|
+
{attachmentDetails.map((anexo: any, i: number) => (
|
|
266
743
|
<li key={i}>
|
|
267
744
|
<Button
|
|
268
745
|
variant="ghost"
|
|
269
746
|
className="h-auto w-full justify-start p-2"
|
|
747
|
+
onClick={() => void handleOpenAttachment(anexo?.id)}
|
|
270
748
|
>
|
|
271
749
|
<FileText className="mr-2 h-4 w-4" />
|
|
272
|
-
{anexo}
|
|
750
|
+
{anexo?.nome}
|
|
273
751
|
</Button>
|
|
274
752
|
</li>
|
|
275
753
|
))}
|
|
@@ -351,6 +829,7 @@ export default function TituloDetalhePage() {
|
|
|
351
829
|
</TableHead>
|
|
352
830
|
<TableHead>{t('settlementsTable.account')}</TableHead>
|
|
353
831
|
<TableHead>{t('settlementsTable.method')}</TableHead>
|
|
832
|
+
<TableHead className="text-right">Ações</TableHead>
|
|
354
833
|
</TableRow>
|
|
355
834
|
</TableHeader>
|
|
356
835
|
<TableBody>
|
|
@@ -376,6 +855,49 @@ export default function TituloDetalhePage() {
|
|
|
376
855
|
<TableCell className="capitalize">
|
|
377
856
|
{liq.metodo}
|
|
378
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>
|
|
379
901
|
</TableRow>
|
|
380
902
|
);
|
|
381
903
|
})
|