@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,6 +1,17 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogAction,
|
|
7
|
+
AlertDialogCancel,
|
|
8
|
+
AlertDialogContent,
|
|
9
|
+
AlertDialogDescription,
|
|
10
|
+
AlertDialogFooter,
|
|
11
|
+
AlertDialogHeader,
|
|
12
|
+
AlertDialogTitle,
|
|
13
|
+
AlertDialogTrigger,
|
|
14
|
+
} from '@/components/ui/alert-dialog';
|
|
4
15
|
import { AuditTimeline } from '@/components/ui/audit-timeline';
|
|
5
16
|
import { Badge } from '@/components/ui/badge';
|
|
6
17
|
import { Button } from '@/components/ui/button';
|
|
@@ -11,13 +22,38 @@ import {
|
|
|
11
22
|
CardHeader,
|
|
12
23
|
CardTitle,
|
|
13
24
|
} from '@/components/ui/card';
|
|
25
|
+
import {
|
|
26
|
+
Dialog,
|
|
27
|
+
DialogContent,
|
|
28
|
+
DialogDescription,
|
|
29
|
+
DialogFooter,
|
|
30
|
+
DialogHeader,
|
|
31
|
+
DialogTitle,
|
|
32
|
+
} from '@/components/ui/dialog';
|
|
14
33
|
import {
|
|
15
34
|
DropdownMenu,
|
|
16
35
|
DropdownMenuContent,
|
|
17
36
|
DropdownMenuItem,
|
|
18
37
|
DropdownMenuTrigger,
|
|
19
38
|
} from '@/components/ui/dropdown-menu';
|
|
39
|
+
import {
|
|
40
|
+
Form,
|
|
41
|
+
FormControl,
|
|
42
|
+
FormField,
|
|
43
|
+
FormItem,
|
|
44
|
+
FormLabel,
|
|
45
|
+
FormMessage,
|
|
46
|
+
} from '@/components/ui/form';
|
|
47
|
+
import { Input } from '@/components/ui/input';
|
|
48
|
+
import { InputMoney } from '@/components/ui/input-money';
|
|
20
49
|
import { Money } from '@/components/ui/money';
|
|
50
|
+
import {
|
|
51
|
+
Select,
|
|
52
|
+
SelectContent,
|
|
53
|
+
SelectItem,
|
|
54
|
+
SelectTrigger,
|
|
55
|
+
SelectValue,
|
|
56
|
+
} from '@/components/ui/select';
|
|
21
57
|
import { StatusBadge } from '@/components/ui/status-badge';
|
|
22
58
|
import {
|
|
23
59
|
Table,
|
|
@@ -28,25 +64,41 @@ import {
|
|
|
28
64
|
TableRow,
|
|
29
65
|
} from '@/components/ui/table';
|
|
30
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';
|
|
31
70
|
import {
|
|
32
|
-
|
|
71
|
+
CheckCircle,
|
|
33
72
|
Download,
|
|
34
73
|
Edit,
|
|
35
74
|
FileText,
|
|
36
75
|
MoreHorizontal,
|
|
37
76
|
Send,
|
|
77
|
+
Undo,
|
|
38
78
|
} from 'lucide-react';
|
|
39
79
|
import { useTranslations } from 'next-intl';
|
|
40
80
|
import Link from 'next/link';
|
|
41
81
|
import { useParams } from 'next/navigation';
|
|
82
|
+
import { useEffect, useState } from 'react';
|
|
83
|
+
import { useForm } from 'react-hook-form';
|
|
84
|
+
import { z } from 'zod';
|
|
42
85
|
import { formatarData } from '../../../_lib/formatters';
|
|
43
86
|
import { useFinanceData } from '../../../_lib/use-finance-data';
|
|
44
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
|
+
|
|
45
96
|
export default function TituloReceberDetalhePage() {
|
|
46
97
|
const t = useTranslations('finance.ReceivableInstallmentDetailPage');
|
|
98
|
+
const { request, showToastHandler } = useApp();
|
|
47
99
|
const params = useParams<{ id: string }>();
|
|
48
100
|
const id = params?.id;
|
|
49
|
-
const { data } = useFinanceData();
|
|
101
|
+
const { data, refetch } = useFinanceData();
|
|
50
102
|
const {
|
|
51
103
|
titulosReceber,
|
|
52
104
|
pessoas,
|
|
@@ -59,6 +111,65 @@ export default function TituloReceberDetalhePage() {
|
|
|
59
111
|
|
|
60
112
|
const titulo = titulosReceber.find((t) => t.id === id);
|
|
61
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
|
+
}, [settleCandidates, settleForm]);
|
|
172
|
+
|
|
62
173
|
const canalBadge = {
|
|
63
174
|
boleto: {
|
|
64
175
|
label: t('channels.boleto'),
|
|
@@ -108,9 +219,21 @@ export default function TituloReceberDetalhePage() {
|
|
|
108
219
|
const cliente = getPessoaById(titulo.clienteId);
|
|
109
220
|
const categoria = getCategoriaById(titulo.categoriaId);
|
|
110
221
|
const centroCusto = getCentroCustoById(titulo.centroCustoId);
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
.
|
|
222
|
+
|
|
223
|
+
const tagOptions = availableTags.map((tag) => ({
|
|
224
|
+
id: String(tag.id),
|
|
225
|
+
name: String(tag.nome || ''),
|
|
226
|
+
color: tag.cor,
|
|
227
|
+
usageCount:
|
|
228
|
+
typeof tag.usageCount === 'number'
|
|
229
|
+
? tag.usageCount
|
|
230
|
+
: typeof tag.usoCount === 'number'
|
|
231
|
+
? tag.usoCount
|
|
232
|
+
: typeof tag.count === 'number'
|
|
233
|
+
? tag.count
|
|
234
|
+
: undefined,
|
|
235
|
+
}));
|
|
236
|
+
|
|
114
237
|
const canal =
|
|
115
238
|
canalBadge[titulo.canal as keyof typeof canalBadge] ||
|
|
116
239
|
canalBadge.transferencia;
|
|
@@ -129,55 +252,392 @@ export default function TituloReceberDetalhePage() {
|
|
|
129
252
|
depois: log.depois,
|
|
130
253
|
}));
|
|
131
254
|
|
|
255
|
+
const attachmentDetails = Array.isArray(titulo.anexosDetalhes)
|
|
256
|
+
? titulo.anexosDetalhes
|
|
257
|
+
: (titulo.anexos || []).map((nome: string) => ({ nome }));
|
|
258
|
+
|
|
259
|
+
const handleOpenAttachment = async (fileId?: string) => {
|
|
260
|
+
if (!fileId) {
|
|
261
|
+
return;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
const response = await request<{ url?: string }>({
|
|
265
|
+
url: `/file/open/${fileId}`,
|
|
266
|
+
method: 'PUT',
|
|
267
|
+
});
|
|
268
|
+
|
|
269
|
+
const url = response?.data?.url;
|
|
270
|
+
if (!url) {
|
|
271
|
+
return;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
275
|
+
};
|
|
276
|
+
|
|
277
|
+
const tTagSelector = (key: string, fallback: string) => {
|
|
278
|
+
const fullKey = `tagSelector.${key}`;
|
|
279
|
+
return t.has(fullKey) ? t(fullKey) : fallback;
|
|
280
|
+
};
|
|
281
|
+
|
|
282
|
+
const toTagSlug = (value: string) => {
|
|
283
|
+
return value
|
|
284
|
+
.normalize('NFD')
|
|
285
|
+
.replace(/[\u0300-\u036f]/g, '')
|
|
286
|
+
.toLowerCase()
|
|
287
|
+
.trim()
|
|
288
|
+
.replace(/[^a-z0-9]+/g, '-')
|
|
289
|
+
.replace(/(^-|-$)+/g, '');
|
|
290
|
+
};
|
|
291
|
+
|
|
292
|
+
const handleCreateTag = async (name: string) => {
|
|
293
|
+
const slug = toTagSlug(name);
|
|
294
|
+
|
|
295
|
+
if (!slug) {
|
|
296
|
+
return null;
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
setIsCreatingTag(true);
|
|
300
|
+
try {
|
|
301
|
+
const response = await request<{
|
|
302
|
+
id?: string | number;
|
|
303
|
+
nome?: string;
|
|
304
|
+
cor?: string;
|
|
305
|
+
}>({
|
|
306
|
+
url: '/finance/tags',
|
|
307
|
+
method: 'POST',
|
|
308
|
+
data: {
|
|
309
|
+
name: slug,
|
|
310
|
+
color: '#000000',
|
|
311
|
+
},
|
|
312
|
+
});
|
|
313
|
+
|
|
314
|
+
const created = response?.data;
|
|
315
|
+
const newTag = {
|
|
316
|
+
id: String(created?.id || `temp-${Date.now()}`),
|
|
317
|
+
nome: created?.nome || slug,
|
|
318
|
+
cor: created?.cor || '#000000',
|
|
319
|
+
};
|
|
320
|
+
|
|
321
|
+
setAvailableTags((current) => {
|
|
322
|
+
if (current.some((tag) => String(tag.id) === newTag.id)) {
|
|
323
|
+
return current;
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
return [...current, newTag];
|
|
327
|
+
});
|
|
328
|
+
|
|
329
|
+
showToastHandler?.(
|
|
330
|
+
'success',
|
|
331
|
+
tTagSelector('messages.createSuccess', 'Tag criada com sucesso')
|
|
332
|
+
);
|
|
333
|
+
|
|
334
|
+
return {
|
|
335
|
+
id: newTag.id,
|
|
336
|
+
name: newTag.nome,
|
|
337
|
+
color: newTag.cor,
|
|
338
|
+
};
|
|
339
|
+
} catch {
|
|
340
|
+
showToastHandler?.(
|
|
341
|
+
'error',
|
|
342
|
+
tTagSelector('messages.createError', 'Não foi possível criar a tag')
|
|
343
|
+
);
|
|
344
|
+
return null;
|
|
345
|
+
} finally {
|
|
346
|
+
setIsCreatingTag(false);
|
|
347
|
+
}
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const handleChangeTags = async (nextTagIds: string[]) => {
|
|
351
|
+
if (!titulo?.id) {
|
|
352
|
+
return;
|
|
353
|
+
}
|
|
354
|
+
|
|
355
|
+
try {
|
|
356
|
+
const response = await request<{ tags?: string[] }>({
|
|
357
|
+
url: `/finance/accounts-receivable/installments/${titulo.id}/tags`,
|
|
358
|
+
method: 'PATCH',
|
|
359
|
+
data: {
|
|
360
|
+
tag_ids: nextTagIds.map((tagId) => Number(tagId)),
|
|
361
|
+
},
|
|
362
|
+
});
|
|
363
|
+
|
|
364
|
+
if (Array.isArray(response?.data?.tags)) {
|
|
365
|
+
setSelectedTagIds(response.data.tags);
|
|
366
|
+
} else {
|
|
367
|
+
setSelectedTagIds(nextTagIds);
|
|
368
|
+
}
|
|
369
|
+
} catch {
|
|
370
|
+
showToastHandler?.(
|
|
371
|
+
'error',
|
|
372
|
+
tTagSelector(
|
|
373
|
+
'messages.updateError',
|
|
374
|
+
'Não foi possível atualizar as tags'
|
|
375
|
+
)
|
|
376
|
+
);
|
|
377
|
+
}
|
|
378
|
+
};
|
|
379
|
+
|
|
380
|
+
const canApprove = titulo.status === 'rascunho';
|
|
381
|
+
const canSettle = ['aprovado', 'aberto', 'parcial'].includes(titulo.status);
|
|
382
|
+
|
|
383
|
+
const getErrorMessage = (error: any, fallback: string) => {
|
|
384
|
+
const message = error?.response?.data?.message;
|
|
385
|
+
|
|
386
|
+
if (Array.isArray(message)) {
|
|
387
|
+
return message.join(', ');
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
if (typeof message === 'string' && message.trim()) {
|
|
391
|
+
return message;
|
|
392
|
+
}
|
|
393
|
+
|
|
394
|
+
return fallback;
|
|
395
|
+
};
|
|
396
|
+
|
|
397
|
+
const handleApprove = async () => {
|
|
398
|
+
if (!canApprove || isApproving) {
|
|
399
|
+
return;
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
setIsApproving(true);
|
|
403
|
+
try {
|
|
404
|
+
await request({
|
|
405
|
+
url: `/finance/accounts-receivable/installments/${titulo.id}/approve`,
|
|
406
|
+
method: 'PATCH',
|
|
407
|
+
});
|
|
408
|
+
|
|
409
|
+
await refetch();
|
|
410
|
+
showToastHandler?.('success', 'Título aprovado com sucesso');
|
|
411
|
+
} catch (error) {
|
|
412
|
+
showToastHandler?.(
|
|
413
|
+
'error',
|
|
414
|
+
getErrorMessage(error, 'Não foi possível aprovar o título')
|
|
415
|
+
);
|
|
416
|
+
} finally {
|
|
417
|
+
setIsApproving(false);
|
|
418
|
+
}
|
|
419
|
+
};
|
|
420
|
+
|
|
421
|
+
const handleSettle = async (values: SettleFormValues) => {
|
|
422
|
+
if (!canSettle || isSettling) {
|
|
423
|
+
return;
|
|
424
|
+
}
|
|
425
|
+
|
|
426
|
+
setIsSettling(true);
|
|
427
|
+
try {
|
|
428
|
+
await request({
|
|
429
|
+
url: `/finance/accounts-receivable/installments/${titulo.id}/settlements`,
|
|
430
|
+
method: 'POST',
|
|
431
|
+
data: {
|
|
432
|
+
installment_id: Number(values.installmentId),
|
|
433
|
+
amount: values.amount,
|
|
434
|
+
description: values.description?.trim() || undefined,
|
|
435
|
+
},
|
|
436
|
+
});
|
|
437
|
+
|
|
438
|
+
await refetch();
|
|
439
|
+
setIsSettleDialogOpen(false);
|
|
440
|
+
showToastHandler?.('success', 'Recebimento registrado com sucesso');
|
|
441
|
+
} catch (error) {
|
|
442
|
+
showToastHandler?.(
|
|
443
|
+
'error',
|
|
444
|
+
getErrorMessage(error, 'Não foi possível registrar o recebimento')
|
|
445
|
+
);
|
|
446
|
+
} finally {
|
|
447
|
+
setIsSettling(false);
|
|
448
|
+
}
|
|
449
|
+
};
|
|
450
|
+
|
|
451
|
+
const handleReverseSettlement = async (settlementId: string) => {
|
|
452
|
+
if (!settlementId || reversingSettlementId) {
|
|
453
|
+
return;
|
|
454
|
+
}
|
|
455
|
+
|
|
456
|
+
setReversingSettlementId(settlementId);
|
|
457
|
+
try {
|
|
458
|
+
await request({
|
|
459
|
+
url: `/finance/accounts-receivable/installments/${titulo.id}/settlements/${settlementId}/reverse`,
|
|
460
|
+
method: 'PATCH',
|
|
461
|
+
data: {},
|
|
462
|
+
});
|
|
463
|
+
|
|
464
|
+
await refetch();
|
|
465
|
+
showToastHandler?.('success', 'Estorno realizado com sucesso');
|
|
466
|
+
} catch (error) {
|
|
467
|
+
showToastHandler?.(
|
|
468
|
+
'error',
|
|
469
|
+
getErrorMessage(error, 'Não foi possível estornar o recebimento')
|
|
470
|
+
);
|
|
471
|
+
} finally {
|
|
472
|
+
setReversingSettlementId(null);
|
|
473
|
+
}
|
|
474
|
+
};
|
|
475
|
+
|
|
132
476
|
return (
|
|
133
477
|
<Page>
|
|
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
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
478
|
+
<PageHeader
|
|
479
|
+
title={titulo.documento}
|
|
480
|
+
description={titulo.descricao}
|
|
481
|
+
breadcrumbs={[
|
|
482
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
483
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
484
|
+
{
|
|
485
|
+
label: t('breadcrumbs.receivables'),
|
|
486
|
+
href: '/finance/accounts-receivable/installments',
|
|
487
|
+
},
|
|
488
|
+
{ label: titulo.documento },
|
|
489
|
+
]}
|
|
490
|
+
actions={
|
|
491
|
+
<div className="flex items-center gap-2">
|
|
492
|
+
<DropdownMenu>
|
|
493
|
+
<DropdownMenuTrigger asChild>
|
|
494
|
+
<Button variant="outline">
|
|
495
|
+
<MoreHorizontal className="mr-2 h-4 w-4" />
|
|
496
|
+
{t('actions.title')}
|
|
497
|
+
</Button>
|
|
498
|
+
</DropdownMenuTrigger>
|
|
499
|
+
<DropdownMenuContent align="end">
|
|
500
|
+
<DropdownMenuItem>
|
|
501
|
+
<Edit className="mr-2 h-4 w-4" />
|
|
502
|
+
{t('actions.edit')}
|
|
503
|
+
</DropdownMenuItem>
|
|
504
|
+
<DropdownMenuItem
|
|
505
|
+
disabled={!canApprove || isApproving}
|
|
506
|
+
onClick={() => void handleApprove()}
|
|
507
|
+
>
|
|
508
|
+
<CheckCircle className="mr-2 h-4 w-4" />
|
|
509
|
+
Aprovar
|
|
510
|
+
</DropdownMenuItem>
|
|
511
|
+
<DropdownMenuItem
|
|
512
|
+
disabled={!canSettle || settleCandidates.length === 0}
|
|
513
|
+
onClick={() => setIsSettleDialogOpen(true)}
|
|
514
|
+
>
|
|
515
|
+
<Download className="mr-2 h-4 w-4" />
|
|
516
|
+
{t('actions.registerReceipt')}
|
|
517
|
+
</DropdownMenuItem>
|
|
518
|
+
<DropdownMenuItem>
|
|
519
|
+
<Send className="mr-2 h-4 w-4" />
|
|
520
|
+
{t('actions.sendCollection')}
|
|
521
|
+
</DropdownMenuItem>
|
|
522
|
+
</DropdownMenuContent>
|
|
523
|
+
</DropdownMenu>
|
|
524
|
+
|
|
525
|
+
<Dialog
|
|
526
|
+
open={isSettleDialogOpen}
|
|
527
|
+
onOpenChange={setIsSettleDialogOpen}
|
|
528
|
+
>
|
|
529
|
+
<DialogContent>
|
|
530
|
+
<DialogHeader>
|
|
531
|
+
<DialogTitle>Registrar recebimento</DialogTitle>
|
|
532
|
+
<DialogDescription>
|
|
533
|
+
Informe a parcela e o valor para baixa parcial ou total.
|
|
534
|
+
</DialogDescription>
|
|
535
|
+
</DialogHeader>
|
|
536
|
+
|
|
537
|
+
<Form {...settleForm}>
|
|
538
|
+
<form
|
|
539
|
+
className="space-y-4"
|
|
540
|
+
onSubmit={settleForm.handleSubmit(handleSettle)}
|
|
541
|
+
>
|
|
542
|
+
<FormField
|
|
543
|
+
control={settleForm.control}
|
|
544
|
+
name="installmentId"
|
|
545
|
+
render={({ field }) => (
|
|
546
|
+
<FormItem>
|
|
547
|
+
<FormLabel>Parcela</FormLabel>
|
|
548
|
+
<Select
|
|
549
|
+
value={field.value}
|
|
550
|
+
onValueChange={(value) => {
|
|
551
|
+
field.onChange(value);
|
|
552
|
+
|
|
553
|
+
const selected = settleCandidates.find(
|
|
554
|
+
(parcela: any) => parcela.id === value
|
|
555
|
+
);
|
|
556
|
+
|
|
557
|
+
if (selected) {
|
|
558
|
+
settleForm.setValue(
|
|
559
|
+
'amount',
|
|
560
|
+
Number(selected.valorAberto || 0),
|
|
561
|
+
{ shouldValidate: true }
|
|
562
|
+
);
|
|
563
|
+
}
|
|
564
|
+
}}
|
|
565
|
+
>
|
|
566
|
+
<FormControl>
|
|
567
|
+
<SelectTrigger>
|
|
568
|
+
<SelectValue placeholder="Selecione" />
|
|
569
|
+
</SelectTrigger>
|
|
570
|
+
</FormControl>
|
|
571
|
+
<SelectContent>
|
|
572
|
+
{settleCandidates.map((parcela: any) => (
|
|
573
|
+
<SelectItem key={parcela.id} value={parcela.id}>
|
|
574
|
+
Parcela {parcela.numero} - em aberto:{' '}
|
|
575
|
+
{new Intl.NumberFormat('pt-BR', {
|
|
576
|
+
style: 'currency',
|
|
577
|
+
currency: 'BRL',
|
|
578
|
+
}).format(Number(parcela.valorAberto || 0))}
|
|
579
|
+
</SelectItem>
|
|
580
|
+
))}
|
|
581
|
+
</SelectContent>
|
|
582
|
+
</Select>
|
|
583
|
+
<FormMessage />
|
|
584
|
+
</FormItem>
|
|
585
|
+
)}
|
|
586
|
+
/>
|
|
587
|
+
|
|
588
|
+
<FormField
|
|
589
|
+
control={settleForm.control}
|
|
590
|
+
name="amount"
|
|
591
|
+
render={({ field }) => (
|
|
592
|
+
<FormItem>
|
|
593
|
+
<FormLabel>Valor</FormLabel>
|
|
594
|
+
<FormControl>
|
|
595
|
+
<InputMoney
|
|
596
|
+
value={Number(field.value || 0)}
|
|
597
|
+
onValueChange={(value) => {
|
|
598
|
+
field.onChange(Number(value || 0));
|
|
599
|
+
}}
|
|
600
|
+
/>
|
|
601
|
+
</FormControl>
|
|
602
|
+
<FormMessage />
|
|
603
|
+
</FormItem>
|
|
604
|
+
)}
|
|
605
|
+
/>
|
|
606
|
+
|
|
607
|
+
<FormField
|
|
608
|
+
control={settleForm.control}
|
|
609
|
+
name="description"
|
|
610
|
+
render={({ field }) => (
|
|
611
|
+
<FormItem>
|
|
612
|
+
<FormLabel>Descrição (opcional)</FormLabel>
|
|
613
|
+
<FormControl>
|
|
614
|
+
<Input {...field} value={field.value || ''} />
|
|
615
|
+
</FormControl>
|
|
616
|
+
<FormMessage />
|
|
617
|
+
</FormItem>
|
|
618
|
+
)}
|
|
619
|
+
/>
|
|
620
|
+
|
|
621
|
+
<DialogFooter>
|
|
622
|
+
<Button
|
|
623
|
+
type="button"
|
|
624
|
+
variant="outline"
|
|
625
|
+
disabled={isSettling}
|
|
626
|
+
onClick={() => setIsSettleDialogOpen(false)}
|
|
627
|
+
>
|
|
628
|
+
Cancelar
|
|
629
|
+
</Button>
|
|
630
|
+
<Button type="submit" disabled={isSettling}>
|
|
631
|
+
Confirmar recebimento
|
|
632
|
+
</Button>
|
|
633
|
+
</DialogFooter>
|
|
634
|
+
</form>
|
|
635
|
+
</Form>
|
|
636
|
+
</DialogContent>
|
|
637
|
+
</Dialog>
|
|
638
|
+
</div>
|
|
639
|
+
}
|
|
640
|
+
/>
|
|
181
641
|
|
|
182
642
|
<div className="grid gap-6 lg:grid-cols-3">
|
|
183
643
|
<Card className="lg:col-span-2">
|
|
@@ -245,20 +705,44 @@ export default function TituloReceberDetalhePage() {
|
|
|
245
705
|
<dt className="text-sm font-medium text-muted-foreground">
|
|
246
706
|
{t('documentData.tags')}
|
|
247
707
|
</dt>
|
|
248
|
-
<dd className="mt-1
|
|
249
|
-
|
|
250
|
-
|
|
251
|
-
|
|
252
|
-
|
|
253
|
-
|
|
254
|
-
|
|
255
|
-
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
708
|
+
<dd className="mt-1">
|
|
709
|
+
<TagSelectorSheet
|
|
710
|
+
selectedTagIds={selectedTagIds}
|
|
711
|
+
tags={tagOptions}
|
|
712
|
+
onChange={handleChangeTags}
|
|
713
|
+
onCreateTag={handleCreateTag}
|
|
714
|
+
disabled={isCreatingTag}
|
|
715
|
+
emptyText={tTagSelector('noTags', 'Sem tags')}
|
|
716
|
+
labels={{
|
|
717
|
+
addTag: tTagSelector('addTag', 'Adicionar tag'),
|
|
718
|
+
sheetTitle: tTagSelector('sheetTitle', 'Gerenciar tags'),
|
|
719
|
+
sheetDescription: tTagSelector(
|
|
720
|
+
'sheetDescription',
|
|
721
|
+
'Selecione tags existentes ou crie uma nova.'
|
|
722
|
+
),
|
|
723
|
+
createLabel: tTagSelector('createLabel', 'Nova tag'),
|
|
724
|
+
createPlaceholder: tTagSelector(
|
|
725
|
+
'createPlaceholder',
|
|
726
|
+
'Digite o nome da tag'
|
|
727
|
+
),
|
|
728
|
+
createAction: tTagSelector('createAction', 'Criar tag'),
|
|
729
|
+
popularTitle: tTagSelector(
|
|
730
|
+
'popularTitle',
|
|
731
|
+
'Tags mais usadas'
|
|
732
|
+
),
|
|
733
|
+
selectedTitle: tTagSelector(
|
|
734
|
+
'selectedTitle',
|
|
735
|
+
'Tags selecionadas'
|
|
736
|
+
),
|
|
737
|
+
noTags: tTagSelector('noTags', 'Sem tags'),
|
|
738
|
+
cancel: tTagSelector('cancel', 'Cancelar'),
|
|
739
|
+
apply: tTagSelector('apply', 'Aplicar'),
|
|
740
|
+
removeTagAria: (tagName: string) =>
|
|
741
|
+
t.has('tagSelector.removeTagAria')
|
|
742
|
+
? t('tagSelector.removeTagAria', { tag: tagName })
|
|
743
|
+
: `Remover tag ${tagName}`,
|
|
744
|
+
}}
|
|
745
|
+
/>
|
|
262
746
|
</dd>
|
|
263
747
|
</div>
|
|
264
748
|
</dl>
|
|
@@ -271,16 +755,17 @@ export default function TituloReceberDetalhePage() {
|
|
|
271
755
|
<CardDescription>{t('attachments.description')}</CardDescription>
|
|
272
756
|
</CardHeader>
|
|
273
757
|
<CardContent>
|
|
274
|
-
{
|
|
758
|
+
{attachmentDetails.length > 0 ? (
|
|
275
759
|
<ul className="space-y-2">
|
|
276
|
-
{
|
|
760
|
+
{attachmentDetails.map((anexo: any, i: number) => (
|
|
277
761
|
<li key={i}>
|
|
278
762
|
<Button
|
|
279
763
|
variant="ghost"
|
|
280
764
|
className="h-auto w-full justify-start p-2"
|
|
765
|
+
onClick={() => void handleOpenAttachment(anexo?.id)}
|
|
281
766
|
>
|
|
282
767
|
<FileText className="mr-2 h-4 w-4" />
|
|
283
|
-
{anexo}
|
|
768
|
+
{anexo?.nome}
|
|
284
769
|
</Button>
|
|
285
770
|
</li>
|
|
286
771
|
))}
|
|
@@ -355,6 +840,7 @@ export default function TituloReceberDetalhePage() {
|
|
|
355
840
|
</TableHead>
|
|
356
841
|
<TableHead>{t('receiptsTable.account')}</TableHead>
|
|
357
842
|
<TableHead>{t('receiptsTable.method')}</TableHead>
|
|
843
|
+
<TableHead className="text-right">Ações</TableHead>
|
|
358
844
|
</TableRow>
|
|
359
845
|
</TableHeader>
|
|
360
846
|
<TableBody>
|
|
@@ -377,6 +863,49 @@ export default function TituloReceberDetalhePage() {
|
|
|
377
863
|
<TableCell className="capitalize">
|
|
378
864
|
{liq.metodo}
|
|
379
865
|
</TableCell>
|
|
866
|
+
<TableCell className="text-right">
|
|
867
|
+
<AlertDialog>
|
|
868
|
+
<AlertDialogTrigger asChild>
|
|
869
|
+
<Button
|
|
870
|
+
variant="outline"
|
|
871
|
+
size="sm"
|
|
872
|
+
disabled={
|
|
873
|
+
!liq.settlementId ||
|
|
874
|
+
liq.status === 'reversed' ||
|
|
875
|
+
!!reversingSettlementId
|
|
876
|
+
}
|
|
877
|
+
>
|
|
878
|
+
<Undo className="mr-2 h-4 w-4" />
|
|
879
|
+
Estornar
|
|
880
|
+
</Button>
|
|
881
|
+
</AlertDialogTrigger>
|
|
882
|
+
<AlertDialogContent>
|
|
883
|
+
<AlertDialogHeader>
|
|
884
|
+
<AlertDialogTitle>
|
|
885
|
+
Confirmar estorno
|
|
886
|
+
</AlertDialogTitle>
|
|
887
|
+
<AlertDialogDescription>
|
|
888
|
+
Esta ação estorna o recebimento e
|
|
889
|
+
recalcula saldos e status.
|
|
890
|
+
</AlertDialogDescription>
|
|
891
|
+
</AlertDialogHeader>
|
|
892
|
+
<AlertDialogFooter>
|
|
893
|
+
<AlertDialogCancel>
|
|
894
|
+
Cancelar
|
|
895
|
+
</AlertDialogCancel>
|
|
896
|
+
<AlertDialogAction
|
|
897
|
+
onClick={() =>
|
|
898
|
+
void handleReverseSettlement(
|
|
899
|
+
String(liq.settlementId)
|
|
900
|
+
)
|
|
901
|
+
}
|
|
902
|
+
>
|
|
903
|
+
Confirmar estorno
|
|
904
|
+
</AlertDialogAction>
|
|
905
|
+
</AlertDialogFooter>
|
|
906
|
+
</AlertDialogContent>
|
|
907
|
+
</AlertDialog>
|
|
908
|
+
</TableCell>
|
|
380
909
|
</TableRow>
|
|
381
910
|
);
|
|
382
911
|
})
|