@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
|
@@ -4,6 +4,7 @@ import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/finance/_componen
|
|
|
4
4
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
5
5
|
import { Badge } from '@/components/ui/badge';
|
|
6
6
|
import { Button } from '@/components/ui/button';
|
|
7
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
7
8
|
import {
|
|
8
9
|
DropdownMenu,
|
|
9
10
|
DropdownMenuContent,
|
|
@@ -22,6 +23,7 @@ import {
|
|
|
22
23
|
} from '@/components/ui/form';
|
|
23
24
|
import { Input } from '@/components/ui/input';
|
|
24
25
|
import { InputMoney } from '@/components/ui/input-money';
|
|
26
|
+
import { Label } from '@/components/ui/label';
|
|
25
27
|
import { Money } from '@/components/ui/money';
|
|
26
28
|
import { Progress } from '@/components/ui/progress';
|
|
27
29
|
import {
|
|
@@ -63,23 +65,159 @@ import {
|
|
|
63
65
|
} from 'lucide-react';
|
|
64
66
|
import { useTranslations } from 'next-intl';
|
|
65
67
|
import Link from 'next/link';
|
|
66
|
-
import { useState } from 'react';
|
|
67
|
-
import { useForm } from 'react-hook-form';
|
|
68
|
+
import { useEffect, useRef, useState } from 'react';
|
|
69
|
+
import { useFieldArray, useForm } from 'react-hook-form';
|
|
68
70
|
import { z } from 'zod';
|
|
69
71
|
import { formatarData } from '../../_lib/formatters';
|
|
70
72
|
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
71
73
|
|
|
72
|
-
const
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
}
|
|
74
|
+
const INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS = 300;
|
|
75
|
+
|
|
76
|
+
const addMonthsToDate = (date: string, monthsToAdd: number) => {
|
|
77
|
+
if (!date) {
|
|
78
|
+
return '';
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
const baseDate = new Date(`${date}T00:00:00`);
|
|
82
|
+
if (Number.isNaN(baseDate.getTime())) {
|
|
83
|
+
return date;
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
const nextDate = new Date(baseDate);
|
|
87
|
+
nextDate.setMonth(nextDate.getMonth() + monthsToAdd);
|
|
88
|
+
return nextDate.toISOString().slice(0, 10);
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const buildEqualInstallments = (
|
|
92
|
+
installmentsCount: number,
|
|
93
|
+
totalAmount: number,
|
|
94
|
+
firstDueDate: string
|
|
95
|
+
) => {
|
|
96
|
+
const safeInstallmentsCount = Math.max(1, Math.floor(installmentsCount || 1));
|
|
97
|
+
const totalCents = Math.max(0, Math.round((totalAmount || 0) * 100));
|
|
98
|
+
const baseCents = Math.floor(totalCents / safeInstallmentsCount);
|
|
99
|
+
const remainder = totalCents % safeInstallmentsCount;
|
|
100
|
+
|
|
101
|
+
return Array.from({ length: safeInstallmentsCount }, (_, index) => ({
|
|
102
|
+
dueDate: addMonthsToDate(firstDueDate, index),
|
|
103
|
+
amount: (baseCents + (index < remainder ? 1 : 0)) / 100,
|
|
104
|
+
}));
|
|
105
|
+
};
|
|
106
|
+
|
|
107
|
+
const redistributeRemainingInstallments = (
|
|
108
|
+
installments: Array<{ dueDate: string; amount: number }>,
|
|
109
|
+
editedIndex: number,
|
|
110
|
+
editedAmount: number,
|
|
111
|
+
totalAmount: number
|
|
112
|
+
) => {
|
|
113
|
+
if (installments.length === 0) {
|
|
114
|
+
return installments;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
const totalCents = Math.max(0, Math.round((totalAmount || 0) * 100));
|
|
118
|
+
|
|
119
|
+
if (installments.length === 1) {
|
|
120
|
+
return installments.map((installment, index) =>
|
|
121
|
+
index === editedIndex
|
|
122
|
+
? {
|
|
123
|
+
...installment,
|
|
124
|
+
amount: totalCents / 100,
|
|
125
|
+
}
|
|
126
|
+
: installment
|
|
127
|
+
);
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
const minimumPerInstallmentCents = totalCents >= installments.length ? 1 : 0;
|
|
131
|
+
const remainingInstallmentsCount = installments.length - 1;
|
|
132
|
+
|
|
133
|
+
let editedCents = Math.round((editedAmount || 0) * 100);
|
|
134
|
+
const minEditedCents = minimumPerInstallmentCents;
|
|
135
|
+
const maxEditedCents =
|
|
136
|
+
totalCents - remainingInstallmentsCount * minimumPerInstallmentCents;
|
|
137
|
+
|
|
138
|
+
if (maxEditedCents < minEditedCents) {
|
|
139
|
+
editedCents = Math.min(Math.max(editedCents, 0), totalCents);
|
|
140
|
+
} else {
|
|
141
|
+
editedCents = Math.min(
|
|
142
|
+
Math.max(editedCents, minEditedCents),
|
|
143
|
+
maxEditedCents
|
|
144
|
+
);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
const remainingCents = totalCents - editedCents;
|
|
148
|
+
const baseCents = Math.floor(remainingCents / remainingInstallmentsCount);
|
|
149
|
+
let remainderCents = remainingCents % remainingInstallmentsCount;
|
|
150
|
+
let processedInstallments = 0;
|
|
151
|
+
|
|
152
|
+
return installments.map((installment, index) => {
|
|
153
|
+
if (index === editedIndex) {
|
|
154
|
+
return {
|
|
155
|
+
...installment,
|
|
156
|
+
amount: editedCents / 100,
|
|
157
|
+
};
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const amountCents =
|
|
161
|
+
baseCents + (processedInstallments < remainderCents ? 1 : 0);
|
|
162
|
+
processedInstallments += 1;
|
|
163
|
+
|
|
164
|
+
return {
|
|
165
|
+
...installment,
|
|
166
|
+
amount: amountCents / 100,
|
|
167
|
+
};
|
|
168
|
+
});
|
|
169
|
+
};
|
|
170
|
+
|
|
171
|
+
const newTitleFormSchema = z
|
|
172
|
+
.object({
|
|
173
|
+
documento: z.string().trim().min(1, 'Documento é obrigatório'),
|
|
174
|
+
clienteId: z.string().min(1, 'Cliente é obrigatório'),
|
|
175
|
+
competencia: z.string().optional(),
|
|
176
|
+
vencimento: z.string().min(1, 'Vencimento é obrigatório'),
|
|
177
|
+
valor: z.number().min(0.01, 'Valor deve ser maior que zero'),
|
|
178
|
+
installmentsCount: z.coerce
|
|
179
|
+
.number({ invalid_type_error: 'Quantidade de parcelas inválida' })
|
|
180
|
+
.int('Quantidade de parcelas inválida')
|
|
181
|
+
.min(1, 'Mínimo de 1 parcela')
|
|
182
|
+
.max(120, 'Máximo de 120 parcelas'),
|
|
183
|
+
installments: z
|
|
184
|
+
.array(
|
|
185
|
+
z.object({
|
|
186
|
+
dueDate: z.string().min(1, 'Vencimento da parcela é obrigatório'),
|
|
187
|
+
amount: z
|
|
188
|
+
.number()
|
|
189
|
+
.min(0.01, 'Valor da parcela deve ser maior que zero'),
|
|
190
|
+
})
|
|
191
|
+
)
|
|
192
|
+
.min(1, 'Informe ao menos uma parcela'),
|
|
193
|
+
categoriaId: z.string().optional(),
|
|
194
|
+
centroCustoId: z.string().optional(),
|
|
195
|
+
canal: z.string().optional(),
|
|
196
|
+
descricao: z.string().optional(),
|
|
197
|
+
})
|
|
198
|
+
.superRefine((values, ctx) => {
|
|
199
|
+
if (values.installments.length !== values.installmentsCount) {
|
|
200
|
+
ctx.addIssue({
|
|
201
|
+
code: z.ZodIssueCode.custom,
|
|
202
|
+
path: ['installments'],
|
|
203
|
+
message: 'Quantidade de parcelas não confere com o detalhamento',
|
|
204
|
+
});
|
|
205
|
+
}
|
|
206
|
+
|
|
207
|
+
const installmentsTotalCents = values.installments.reduce(
|
|
208
|
+
(acc, installment) => acc + Math.round((installment.amount || 0) * 100),
|
|
209
|
+
0
|
|
210
|
+
);
|
|
211
|
+
const totalCents = Math.round((values.valor || 0) * 100);
|
|
212
|
+
|
|
213
|
+
if (installmentsTotalCents !== totalCents) {
|
|
214
|
+
ctx.addIssue({
|
|
215
|
+
code: z.ZodIssueCode.custom,
|
|
216
|
+
path: ['installments'],
|
|
217
|
+
message: 'A soma das parcelas deve ser igual ao valor total',
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
});
|
|
83
221
|
|
|
84
222
|
type NewTitleFormValues = z.infer<typeof newTitleFormSchema>;
|
|
85
223
|
|
|
@@ -100,6 +238,12 @@ function NovoTituloSheet({
|
|
|
100
238
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
101
239
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
102
240
|
const [isExtractingFileData, setIsExtractingFileData] = useState(false);
|
|
241
|
+
const [isInstallmentsEdited, setIsInstallmentsEdited] = useState(false);
|
|
242
|
+
const [autoRedistributeInstallments, setAutoRedistributeInstallments] =
|
|
243
|
+
useState(true);
|
|
244
|
+
const redistributionTimeoutRef = useRef<
|
|
245
|
+
Record<number, ReturnType<typeof setTimeout>>
|
|
246
|
+
>({});
|
|
103
247
|
const [extractionConfidence, setExtractionConfidence] = useState<
|
|
104
248
|
number | null
|
|
105
249
|
>(null);
|
|
@@ -132,6 +276,8 @@ function NovoTituloSheet({
|
|
|
132
276
|
competencia: '',
|
|
133
277
|
vencimento: '',
|
|
134
278
|
valor: 0,
|
|
279
|
+
installmentsCount: 1,
|
|
280
|
+
installments: [{ dueDate: '', amount: 0 }],
|
|
135
281
|
categoriaId: '',
|
|
136
282
|
centroCustoId: '',
|
|
137
283
|
canal: '',
|
|
@@ -139,6 +285,101 @@ function NovoTituloSheet({
|
|
|
139
285
|
},
|
|
140
286
|
});
|
|
141
287
|
|
|
288
|
+
const { fields: installmentFields, replace: replaceInstallments } =
|
|
289
|
+
useFieldArray({
|
|
290
|
+
control: form.control,
|
|
291
|
+
name: 'installments',
|
|
292
|
+
});
|
|
293
|
+
|
|
294
|
+
const watchedInstallmentsCount = form.watch('installmentsCount');
|
|
295
|
+
const watchedTotalValue = form.watch('valor');
|
|
296
|
+
const watchedDueDate = form.watch('vencimento');
|
|
297
|
+
const watchedInstallments = form.watch('installments');
|
|
298
|
+
|
|
299
|
+
useEffect(() => {
|
|
300
|
+
if (isInstallmentsEdited) {
|
|
301
|
+
return;
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
replaceInstallments(
|
|
305
|
+
buildEqualInstallments(
|
|
306
|
+
watchedInstallmentsCount,
|
|
307
|
+
watchedTotalValue,
|
|
308
|
+
watchedDueDate
|
|
309
|
+
)
|
|
310
|
+
);
|
|
311
|
+
}, [
|
|
312
|
+
isInstallmentsEdited,
|
|
313
|
+
replaceInstallments,
|
|
314
|
+
watchedDueDate,
|
|
315
|
+
watchedInstallmentsCount,
|
|
316
|
+
watchedTotalValue,
|
|
317
|
+
]);
|
|
318
|
+
|
|
319
|
+
const installmentsTotal = (watchedInstallments || []).reduce(
|
|
320
|
+
(acc, installment) => acc + Number(installment?.amount || 0),
|
|
321
|
+
0
|
|
322
|
+
);
|
|
323
|
+
const installmentsDiffCents = Math.abs(
|
|
324
|
+
Math.round(installmentsTotal * 100) -
|
|
325
|
+
Math.round((watchedTotalValue || 0) * 100)
|
|
326
|
+
);
|
|
327
|
+
|
|
328
|
+
const clearScheduledRedistribution = (index: number) => {
|
|
329
|
+
const timeout = redistributionTimeoutRef.current[index];
|
|
330
|
+
if (!timeout) {
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
clearTimeout(timeout);
|
|
335
|
+
delete redistributionTimeoutRef.current[index];
|
|
336
|
+
};
|
|
337
|
+
|
|
338
|
+
const runInstallmentRedistribution = (index: number) => {
|
|
339
|
+
if (!autoRedistributeInstallments) {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
const currentInstallments = form.getValues('installments');
|
|
344
|
+
const editedInstallmentAmount = Number(
|
|
345
|
+
currentInstallments[index]?.amount || 0
|
|
346
|
+
);
|
|
347
|
+
|
|
348
|
+
const redistributedInstallments = redistributeRemainingInstallments(
|
|
349
|
+
currentInstallments,
|
|
350
|
+
index,
|
|
351
|
+
editedInstallmentAmount,
|
|
352
|
+
form.getValues('valor')
|
|
353
|
+
);
|
|
354
|
+
|
|
355
|
+
replaceInstallments(redistributedInstallments);
|
|
356
|
+
};
|
|
357
|
+
|
|
358
|
+
const scheduleInstallmentRedistribution = (index: number) => {
|
|
359
|
+
clearScheduledRedistribution(index);
|
|
360
|
+
|
|
361
|
+
redistributionTimeoutRef.current[index] = setTimeout(() => {
|
|
362
|
+
runInstallmentRedistribution(index);
|
|
363
|
+
delete redistributionTimeoutRef.current[index];
|
|
364
|
+
}, INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS);
|
|
365
|
+
};
|
|
366
|
+
|
|
367
|
+
useEffect(() => {
|
|
368
|
+
if (autoRedistributeInstallments) {
|
|
369
|
+
return;
|
|
370
|
+
}
|
|
371
|
+
|
|
372
|
+
Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
|
|
373
|
+
redistributionTimeoutRef.current = {};
|
|
374
|
+
}, [autoRedistributeInstallments]);
|
|
375
|
+
|
|
376
|
+
useEffect(() => {
|
|
377
|
+
return () => {
|
|
378
|
+
Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
|
|
379
|
+
redistributionTimeoutRef.current = {};
|
|
380
|
+
};
|
|
381
|
+
}, []);
|
|
382
|
+
|
|
142
383
|
const handleSubmit = async (values: NewTitleFormValues) => {
|
|
143
384
|
try {
|
|
144
385
|
await request({
|
|
@@ -160,6 +401,11 @@ function NovoTituloSheet({
|
|
|
160
401
|
: undefined,
|
|
161
402
|
payment_channel: values.canal || undefined,
|
|
162
403
|
description: values.descricao?.trim() || undefined,
|
|
404
|
+
installments: values.installments.map((installment, index) => ({
|
|
405
|
+
installment_number: index + 1,
|
|
406
|
+
due_date: installment.dueDate || values.vencimento,
|
|
407
|
+
amount: installment.amount,
|
|
408
|
+
})),
|
|
163
409
|
attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
|
|
164
410
|
},
|
|
165
411
|
});
|
|
@@ -170,6 +416,8 @@ function NovoTituloSheet({
|
|
|
170
416
|
setUploadedFileName('');
|
|
171
417
|
setExtractionConfidence(null);
|
|
172
418
|
setExtractionWarnings([]);
|
|
419
|
+
setIsInstallmentsEdited(false);
|
|
420
|
+
setAutoRedistributeInstallments(true);
|
|
173
421
|
setOpen(false);
|
|
174
422
|
showToastHandler?.('success', 'Título criado com sucesso');
|
|
175
423
|
} catch {
|
|
@@ -184,6 +432,8 @@ function NovoTituloSheet({
|
|
|
184
432
|
setExtractionConfidence(null);
|
|
185
433
|
setExtractionWarnings([]);
|
|
186
434
|
setUploadProgress(0);
|
|
435
|
+
setIsInstallmentsEdited(false);
|
|
436
|
+
setAutoRedistributeInstallments(true);
|
|
187
437
|
setOpen(false);
|
|
188
438
|
};
|
|
189
439
|
|
|
@@ -506,6 +756,168 @@ function NovoTituloSheet({
|
|
|
506
756
|
)}
|
|
507
757
|
/>
|
|
508
758
|
|
|
759
|
+
<FormField
|
|
760
|
+
control={form.control}
|
|
761
|
+
name="installmentsCount"
|
|
762
|
+
render={({ field }) => (
|
|
763
|
+
<FormItem>
|
|
764
|
+
<FormLabel>Quantidade de Parcelas</FormLabel>
|
|
765
|
+
<FormControl>
|
|
766
|
+
<Input
|
|
767
|
+
type="number"
|
|
768
|
+
min={1}
|
|
769
|
+
max={120}
|
|
770
|
+
value={field.value}
|
|
771
|
+
onChange={(event) => {
|
|
772
|
+
const nextValue = Number(event.target.value || 1);
|
|
773
|
+
field.onChange(
|
|
774
|
+
Number.isNaN(nextValue) ? 1 : nextValue
|
|
775
|
+
);
|
|
776
|
+
}}
|
|
777
|
+
/>
|
|
778
|
+
</FormControl>
|
|
779
|
+
<FormMessage />
|
|
780
|
+
</FormItem>
|
|
781
|
+
)}
|
|
782
|
+
/>
|
|
783
|
+
|
|
784
|
+
<div className="space-y-3 rounded-md border p-3">
|
|
785
|
+
<div className="flex items-center justify-between gap-2">
|
|
786
|
+
<p className="text-sm font-medium">Parcelas</p>
|
|
787
|
+
<Button
|
|
788
|
+
type="button"
|
|
789
|
+
variant="outline"
|
|
790
|
+
size="sm"
|
|
791
|
+
onClick={() => {
|
|
792
|
+
setIsInstallmentsEdited(false);
|
|
793
|
+
replaceInstallments(
|
|
794
|
+
buildEqualInstallments(
|
|
795
|
+
form.getValues('installmentsCount'),
|
|
796
|
+
form.getValues('valor'),
|
|
797
|
+
form.getValues('vencimento')
|
|
798
|
+
)
|
|
799
|
+
);
|
|
800
|
+
}}
|
|
801
|
+
>
|
|
802
|
+
Recalcular automaticamente
|
|
803
|
+
</Button>
|
|
804
|
+
</div>
|
|
805
|
+
|
|
806
|
+
<div className="flex items-center gap-2">
|
|
807
|
+
<Checkbox
|
|
808
|
+
id="auto-redistribute-installments-receivable"
|
|
809
|
+
checked={autoRedistributeInstallments}
|
|
810
|
+
onCheckedChange={(checked) =>
|
|
811
|
+
setAutoRedistributeInstallments(checked === true)
|
|
812
|
+
}
|
|
813
|
+
/>
|
|
814
|
+
<Label
|
|
815
|
+
htmlFor="auto-redistribute-installments-receivable"
|
|
816
|
+
className="text-xs text-muted-foreground"
|
|
817
|
+
>
|
|
818
|
+
Redistribuir automaticamente o restante ao editar parcela
|
|
819
|
+
</Label>
|
|
820
|
+
</div>
|
|
821
|
+
{autoRedistributeInstallments && (
|
|
822
|
+
<p className="text-xs text-muted-foreground">
|
|
823
|
+
A redistribuição ocorre ao parar de digitar e ao sair do
|
|
824
|
+
campo.
|
|
825
|
+
</p>
|
|
826
|
+
)}
|
|
827
|
+
|
|
828
|
+
<div className="space-y-2">
|
|
829
|
+
{installmentFields.map((installment, index) => (
|
|
830
|
+
<div
|
|
831
|
+
key={installment.id}
|
|
832
|
+
className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
833
|
+
>
|
|
834
|
+
<div className="flex items-center text-sm text-muted-foreground">
|
|
835
|
+
#{index + 1}
|
|
836
|
+
</div>
|
|
837
|
+
|
|
838
|
+
<FormField
|
|
839
|
+
control={form.control}
|
|
840
|
+
name={`installments.${index}.dueDate` as const}
|
|
841
|
+
render={({ field }) => (
|
|
842
|
+
<FormItem>
|
|
843
|
+
<FormLabel className="text-xs">
|
|
844
|
+
Vencimento
|
|
845
|
+
</FormLabel>
|
|
846
|
+
<FormControl>
|
|
847
|
+
<Input
|
|
848
|
+
type="date"
|
|
849
|
+
{...field}
|
|
850
|
+
value={field.value || ''}
|
|
851
|
+
onChange={(event) => {
|
|
852
|
+
setIsInstallmentsEdited(true);
|
|
853
|
+
field.onChange(event);
|
|
854
|
+
}}
|
|
855
|
+
/>
|
|
856
|
+
</FormControl>
|
|
857
|
+
<FormMessage />
|
|
858
|
+
</FormItem>
|
|
859
|
+
)}
|
|
860
|
+
/>
|
|
861
|
+
|
|
862
|
+
<FormField
|
|
863
|
+
control={form.control}
|
|
864
|
+
name={`installments.${index}.amount` as const}
|
|
865
|
+
render={({ field }) => (
|
|
866
|
+
<FormItem>
|
|
867
|
+
<FormLabel className="text-xs">Valor</FormLabel>
|
|
868
|
+
<FormControl>
|
|
869
|
+
<InputMoney
|
|
870
|
+
ref={field.ref}
|
|
871
|
+
name={field.name}
|
|
872
|
+
value={field.value}
|
|
873
|
+
onBlur={() => {
|
|
874
|
+
field.onBlur();
|
|
875
|
+
|
|
876
|
+
if (!autoRedistributeInstallments) {
|
|
877
|
+
return;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
clearScheduledRedistribution(index);
|
|
881
|
+
runInstallmentRedistribution(index);
|
|
882
|
+
}}
|
|
883
|
+
onValueChange={(value) => {
|
|
884
|
+
setIsInstallmentsEdited(true);
|
|
885
|
+
field.onChange(value ?? 0);
|
|
886
|
+
|
|
887
|
+
if (!autoRedistributeInstallments) {
|
|
888
|
+
return;
|
|
889
|
+
}
|
|
890
|
+
|
|
891
|
+
scheduleInstallmentRedistribution(index);
|
|
892
|
+
}}
|
|
893
|
+
placeholder="0,00"
|
|
894
|
+
/>
|
|
895
|
+
</FormControl>
|
|
896
|
+
<FormMessage />
|
|
897
|
+
</FormItem>
|
|
898
|
+
)}
|
|
899
|
+
/>
|
|
900
|
+
</div>
|
|
901
|
+
))}
|
|
902
|
+
</div>
|
|
903
|
+
|
|
904
|
+
<p
|
|
905
|
+
className={`text-xs ${
|
|
906
|
+
installmentsDiffCents === 0
|
|
907
|
+
? 'text-muted-foreground'
|
|
908
|
+
: 'text-destructive'
|
|
909
|
+
}`}
|
|
910
|
+
>
|
|
911
|
+
Soma das parcelas: {installmentsTotal.toFixed(2)}
|
|
912
|
+
{installmentsDiffCents > 0 && ' (ajuste necessário)'}
|
|
913
|
+
</p>
|
|
914
|
+
{form.formState.errors.installments?.message && (
|
|
915
|
+
<p className="text-xs text-destructive">
|
|
916
|
+
{form.formState.errors.installments.message}
|
|
917
|
+
</p>
|
|
918
|
+
)}
|
|
919
|
+
</div>
|
|
920
|
+
|
|
509
921
|
<FormField
|
|
510
922
|
control={form.control}
|
|
511
923
|
name="categoriaId"
|
|
@@ -638,6 +1050,7 @@ function NovoTituloSheet({
|
|
|
638
1050
|
|
|
639
1051
|
export default function TitulosReceberPage() {
|
|
640
1052
|
const t = useTranslations('finance.ReceivableInstallmentsPage');
|
|
1053
|
+
const { request, showToastHandler } = useApp();
|
|
641
1054
|
const { data, refetch } = useFinanceData();
|
|
642
1055
|
const { titulosReceber, pessoas, categorias, centrosCusto } = data;
|
|
643
1056
|
|
|
@@ -674,6 +1087,29 @@ export default function TitulosReceberPage() {
|
|
|
674
1087
|
return matchesSearch && matchesStatus;
|
|
675
1088
|
});
|
|
676
1089
|
|
|
1090
|
+
const handleOpenAttachment = async (fileId?: string) => {
|
|
1091
|
+
if (!fileId) {
|
|
1092
|
+
return;
|
|
1093
|
+
}
|
|
1094
|
+
|
|
1095
|
+
try {
|
|
1096
|
+
const response = await request<{ url?: string }>({
|
|
1097
|
+
url: `/file/open/${fileId}`,
|
|
1098
|
+
method: 'PUT',
|
|
1099
|
+
});
|
|
1100
|
+
|
|
1101
|
+
const url = response?.data?.url;
|
|
1102
|
+
if (!url) {
|
|
1103
|
+
showToastHandler?.('error', 'Não foi possível abrir o anexo');
|
|
1104
|
+
return;
|
|
1105
|
+
}
|
|
1106
|
+
|
|
1107
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1108
|
+
} catch {
|
|
1109
|
+
showToastHandler?.('error', 'Não foi possível abrir o anexo');
|
|
1110
|
+
}
|
|
1111
|
+
};
|
|
1112
|
+
|
|
677
1113
|
return (
|
|
678
1114
|
<Page>
|
|
679
1115
|
<PageHeader
|
|
@@ -754,7 +1190,22 @@ export default function TitulosReceberPage() {
|
|
|
754
1190
|
{titulo.documento}
|
|
755
1191
|
</Link>
|
|
756
1192
|
{titulo.anexos.length > 0 && (
|
|
757
|
-
<
|
|
1193
|
+
<Button
|
|
1194
|
+
type="button"
|
|
1195
|
+
variant="ghost"
|
|
1196
|
+
size="icon"
|
|
1197
|
+
className="ml-1 inline-flex h-5 w-5 align-middle text-muted-foreground"
|
|
1198
|
+
onClick={(event) => {
|
|
1199
|
+
event.preventDefault();
|
|
1200
|
+
event.stopPropagation();
|
|
1201
|
+
const firstAttachmentId =
|
|
1202
|
+
titulo.anexosDetalhes?.[0]?.id;
|
|
1203
|
+
void handleOpenAttachment(firstAttachmentId);
|
|
1204
|
+
}}
|
|
1205
|
+
aria-label="Abrir anexo"
|
|
1206
|
+
>
|
|
1207
|
+
<Paperclip className="h-3 w-3" />
|
|
1208
|
+
</Button>
|
|
758
1209
|
)}
|
|
759
1210
|
</TableCell>
|
|
760
1211
|
<TableCell>{cliente?.nome}</TableCell>
|
|
@@ -799,7 +1250,13 @@ export default function TitulosReceberPage() {
|
|
|
799
1250
|
{t('table.actions.edit')}
|
|
800
1251
|
</DropdownMenuItem>
|
|
801
1252
|
<DropdownMenuSeparator />
|
|
802
|
-
<DropdownMenuItem
|
|
1253
|
+
<DropdownMenuItem
|
|
1254
|
+
disabled={
|
|
1255
|
+
!['aprovado', 'aberto', 'parcial'].includes(
|
|
1256
|
+
titulo.status
|
|
1257
|
+
)
|
|
1258
|
+
}
|
|
1259
|
+
>
|
|
803
1260
|
<Download className="mr-2 h-4 w-4" />
|
|
804
1261
|
{t('table.actions.registerReceipt')}
|
|
805
1262
|
</DropdownMenuItem>
|
|
@@ -209,6 +209,25 @@
|
|
|
209
209
|
"createdAt": "Created At",
|
|
210
210
|
"tags": "Tags"
|
|
211
211
|
},
|
|
212
|
+
"tagSelector": {
|
|
213
|
+
"addTag": "Add tag",
|
|
214
|
+
"sheetTitle": "Manage tags",
|
|
215
|
+
"sheetDescription": "Select existing tags or create a new one.",
|
|
216
|
+
"createLabel": "New tag",
|
|
217
|
+
"createPlaceholder": "Type the tag name",
|
|
218
|
+
"createAction": "Create tag",
|
|
219
|
+
"popularTitle": "Most used tags",
|
|
220
|
+
"selectedTitle": "Selected tags",
|
|
221
|
+
"noTags": "No tags",
|
|
222
|
+
"cancel": "Cancel",
|
|
223
|
+
"apply": "Apply",
|
|
224
|
+
"removeTagAria": "Remove tag {tag}",
|
|
225
|
+
"messages": {
|
|
226
|
+
"createSuccess": "Tag created successfully",
|
|
227
|
+
"createError": "Could not create the tag",
|
|
228
|
+
"updateError": "Could not update tags"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
212
231
|
"attachments": {
|
|
213
232
|
"title": "Attachments",
|
|
214
233
|
"description": "Related documents",
|
|
@@ -415,6 +434,25 @@
|
|
|
415
434
|
"channel": "Channel",
|
|
416
435
|
"tags": "Tags"
|
|
417
436
|
},
|
|
437
|
+
"tagSelector": {
|
|
438
|
+
"addTag": "Add tag",
|
|
439
|
+
"sheetTitle": "Manage tags",
|
|
440
|
+
"sheetDescription": "Select existing tags or create a new one.",
|
|
441
|
+
"createLabel": "New tag",
|
|
442
|
+
"createPlaceholder": "Type the tag name",
|
|
443
|
+
"createAction": "Create tag",
|
|
444
|
+
"popularTitle": "Most used tags",
|
|
445
|
+
"selectedTitle": "Selected tags",
|
|
446
|
+
"noTags": "No tags",
|
|
447
|
+
"cancel": "Cancel",
|
|
448
|
+
"apply": "Apply",
|
|
449
|
+
"removeTagAria": "Remove tag {tag}",
|
|
450
|
+
"messages": {
|
|
451
|
+
"createSuccess": "Tag created successfully",
|
|
452
|
+
"createError": "Could not create the tag",
|
|
453
|
+
"updateError": "Could not update tags"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
418
456
|
"attachments": {
|
|
419
457
|
"title": "Attachments",
|
|
420
458
|
"description": "Related documents",
|
|
@@ -209,6 +209,25 @@
|
|
|
209
209
|
"createdAt": "Data de Criação",
|
|
210
210
|
"tags": "Tags"
|
|
211
211
|
},
|
|
212
|
+
"tagSelector": {
|
|
213
|
+
"addTag": "Adicionar tag",
|
|
214
|
+
"sheetTitle": "Gerenciar tags",
|
|
215
|
+
"sheetDescription": "Selecione tags existentes ou crie uma nova.",
|
|
216
|
+
"createLabel": "Nova tag",
|
|
217
|
+
"createPlaceholder": "Digite o nome da tag",
|
|
218
|
+
"createAction": "Criar tag",
|
|
219
|
+
"popularTitle": "Tags mais usadas",
|
|
220
|
+
"selectedTitle": "Tags selecionadas",
|
|
221
|
+
"noTags": "Sem tags",
|
|
222
|
+
"cancel": "Cancelar",
|
|
223
|
+
"apply": "Aplicar",
|
|
224
|
+
"removeTagAria": "Remover tag {tag}",
|
|
225
|
+
"messages": {
|
|
226
|
+
"createSuccess": "Tag criada com sucesso",
|
|
227
|
+
"createError": "Não foi possível criar a tag",
|
|
228
|
+
"updateError": "Não foi possível atualizar as tags"
|
|
229
|
+
}
|
|
230
|
+
},
|
|
212
231
|
"attachments": {
|
|
213
232
|
"title": "Anexos",
|
|
214
233
|
"description": "Documentos relacionados",
|
|
@@ -415,6 +434,25 @@
|
|
|
415
434
|
"channel": "Canal",
|
|
416
435
|
"tags": "Tags"
|
|
417
436
|
},
|
|
437
|
+
"tagSelector": {
|
|
438
|
+
"addTag": "Adicionar tag",
|
|
439
|
+
"sheetTitle": "Gerenciar tags",
|
|
440
|
+
"sheetDescription": "Selecione tags existentes ou crie uma nova.",
|
|
441
|
+
"createLabel": "Nova tag",
|
|
442
|
+
"createPlaceholder": "Digite o nome da tag",
|
|
443
|
+
"createAction": "Criar tag",
|
|
444
|
+
"popularTitle": "Tags mais usadas",
|
|
445
|
+
"selectedTitle": "Tags selecionadas",
|
|
446
|
+
"noTags": "Sem tags",
|
|
447
|
+
"cancel": "Cancelar",
|
|
448
|
+
"apply": "Aplicar",
|
|
449
|
+
"removeTagAria": "Remover tag {tag}",
|
|
450
|
+
"messages": {
|
|
451
|
+
"createSuccess": "Tag criada com sucesso",
|
|
452
|
+
"createError": "Não foi possível criar a tag",
|
|
453
|
+
"updateError": "Não foi possível atualizar as tags"
|
|
454
|
+
}
|
|
455
|
+
},
|
|
418
456
|
"attachments": {
|
|
419
457
|
"title": "Anexos",
|
|
420
458
|
"description": "Documentos relacionados",
|