@hed-hog/finance 0.0.238 → 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/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/finance-data.controller.d.ts +13 -5
- package/dist/finance-data.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.d.ts +248 -12
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +92 -0
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +275 -17
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +666 -78
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +63 -0
- package/hedhog/frontend/app/accounts-payable/approvals/page.tsx.ejs +92 -12
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +355 -4
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +440 -16
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +396 -49
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +432 -14
- package/package.json +5 -5
- 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/finance-installments.controller.ts +102 -0
- package/src/finance.service.ts +1007 -82
|
@@ -3,6 +3,7 @@
|
|
|
3
3
|
import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/finance/_components/person-field-with-create';
|
|
4
4
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
5
5
|
import { Button } from '@/components/ui/button';
|
|
6
|
+
import { Checkbox } from '@/components/ui/checkbox';
|
|
6
7
|
import {
|
|
7
8
|
DropdownMenu,
|
|
8
9
|
DropdownMenuContent,
|
|
@@ -21,6 +22,7 @@ import {
|
|
|
21
22
|
} from '@/components/ui/form';
|
|
22
23
|
import { Input } from '@/components/ui/input';
|
|
23
24
|
import { InputMoney } from '@/components/ui/input-money';
|
|
25
|
+
import { Label } from '@/components/ui/label';
|
|
24
26
|
import { Money } from '@/components/ui/money';
|
|
25
27
|
import { Progress } from '@/components/ui/progress';
|
|
26
28
|
import {
|
|
@@ -64,23 +66,159 @@ import {
|
|
|
64
66
|
} from 'lucide-react';
|
|
65
67
|
import { useTranslations } from 'next-intl';
|
|
66
68
|
import Link from 'next/link';
|
|
67
|
-
import { useState } from 'react';
|
|
68
|
-
import { useForm } from 'react-hook-form';
|
|
69
|
+
import { useEffect, useRef, useState } from 'react';
|
|
70
|
+
import { useFieldArray, useForm } from 'react-hook-form';
|
|
69
71
|
import { z } from 'zod';
|
|
70
72
|
import { formatarData } from '../../_lib/formatters';
|
|
71
73
|
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
72
74
|
|
|
73
|
-
const
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
}
|
|
75
|
+
const INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS = 300;
|
|
76
|
+
|
|
77
|
+
const addMonthsToDate = (date: string, monthsToAdd: number) => {
|
|
78
|
+
if (!date) {
|
|
79
|
+
return '';
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const baseDate = new Date(`${date}T00:00:00`);
|
|
83
|
+
if (Number.isNaN(baseDate.getTime())) {
|
|
84
|
+
return date;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
const nextDate = new Date(baseDate);
|
|
88
|
+
nextDate.setMonth(nextDate.getMonth() + monthsToAdd);
|
|
89
|
+
return nextDate.toISOString().slice(0, 10);
|
|
90
|
+
};
|
|
91
|
+
|
|
92
|
+
const buildEqualInstallments = (
|
|
93
|
+
installmentsCount: number,
|
|
94
|
+
totalAmount: number,
|
|
95
|
+
firstDueDate: string
|
|
96
|
+
) => {
|
|
97
|
+
const safeInstallmentsCount = Math.max(1, Math.floor(installmentsCount || 1));
|
|
98
|
+
const totalCents = Math.max(0, Math.round((totalAmount || 0) * 100));
|
|
99
|
+
const baseCents = Math.floor(totalCents / safeInstallmentsCount);
|
|
100
|
+
const remainder = totalCents % safeInstallmentsCount;
|
|
101
|
+
|
|
102
|
+
return Array.from({ length: safeInstallmentsCount }, (_, index) => ({
|
|
103
|
+
dueDate: addMonthsToDate(firstDueDate, index),
|
|
104
|
+
amount: (baseCents + (index < remainder ? 1 : 0)) / 100,
|
|
105
|
+
}));
|
|
106
|
+
};
|
|
107
|
+
|
|
108
|
+
const redistributeRemainingInstallments = (
|
|
109
|
+
installments: Array<{ dueDate: string; amount: number }>,
|
|
110
|
+
editedIndex: number,
|
|
111
|
+
editedAmount: number,
|
|
112
|
+
totalAmount: number
|
|
113
|
+
) => {
|
|
114
|
+
if (installments.length === 0) {
|
|
115
|
+
return installments;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const totalCents = Math.max(0, Math.round((totalAmount || 0) * 100));
|
|
119
|
+
|
|
120
|
+
if (installments.length === 1) {
|
|
121
|
+
return installments.map((installment, index) =>
|
|
122
|
+
index === editedIndex
|
|
123
|
+
? {
|
|
124
|
+
...installment,
|
|
125
|
+
amount: totalCents / 100,
|
|
126
|
+
}
|
|
127
|
+
: installment
|
|
128
|
+
);
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const minimumPerInstallmentCents = totalCents >= installments.length ? 1 : 0;
|
|
132
|
+
const remainingInstallmentsCount = installments.length - 1;
|
|
133
|
+
|
|
134
|
+
let editedCents = Math.round((editedAmount || 0) * 100);
|
|
135
|
+
const minEditedCents = minimumPerInstallmentCents;
|
|
136
|
+
const maxEditedCents =
|
|
137
|
+
totalCents - remainingInstallmentsCount * minimumPerInstallmentCents;
|
|
138
|
+
|
|
139
|
+
if (maxEditedCents < minEditedCents) {
|
|
140
|
+
editedCents = Math.min(Math.max(editedCents, 0), totalCents);
|
|
141
|
+
} else {
|
|
142
|
+
editedCents = Math.min(
|
|
143
|
+
Math.max(editedCents, minEditedCents),
|
|
144
|
+
maxEditedCents
|
|
145
|
+
);
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
const remainingCents = totalCents - editedCents;
|
|
149
|
+
const baseCents = Math.floor(remainingCents / remainingInstallmentsCount);
|
|
150
|
+
let remainderCents = remainingCents % remainingInstallmentsCount;
|
|
151
|
+
let processedInstallments = 0;
|
|
152
|
+
|
|
153
|
+
return installments.map((installment, index) => {
|
|
154
|
+
if (index === editedIndex) {
|
|
155
|
+
return {
|
|
156
|
+
...installment,
|
|
157
|
+
amount: editedCents / 100,
|
|
158
|
+
};
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const amountCents =
|
|
162
|
+
baseCents + (processedInstallments < remainderCents ? 1 : 0);
|
|
163
|
+
processedInstallments += 1;
|
|
164
|
+
|
|
165
|
+
return {
|
|
166
|
+
...installment,
|
|
167
|
+
amount: amountCents / 100,
|
|
168
|
+
};
|
|
169
|
+
});
|
|
170
|
+
};
|
|
171
|
+
|
|
172
|
+
const newTitleFormSchema = z
|
|
173
|
+
.object({
|
|
174
|
+
documento: z.string().trim().min(1, 'Documento é obrigatório'),
|
|
175
|
+
fornecedorId: z.string().min(1, 'Fornecedor é obrigatório'),
|
|
176
|
+
competencia: z.string().optional(),
|
|
177
|
+
vencimento: z.string().min(1, 'Vencimento é obrigatório'),
|
|
178
|
+
valor: z.number().min(0.01, 'Valor deve ser maior que zero'),
|
|
179
|
+
installmentsCount: z.coerce
|
|
180
|
+
.number({ invalid_type_error: 'Quantidade de parcelas inválida' })
|
|
181
|
+
.int('Quantidade de parcelas inválida')
|
|
182
|
+
.min(1, 'Mínimo de 1 parcela')
|
|
183
|
+
.max(120, 'Máximo de 120 parcelas'),
|
|
184
|
+
installments: z
|
|
185
|
+
.array(
|
|
186
|
+
z.object({
|
|
187
|
+
dueDate: z.string().min(1, 'Vencimento da parcela é obrigatório'),
|
|
188
|
+
amount: z
|
|
189
|
+
.number()
|
|
190
|
+
.min(0.01, 'Valor da parcela deve ser maior que zero'),
|
|
191
|
+
})
|
|
192
|
+
)
|
|
193
|
+
.min(1, 'Informe ao menos uma parcela'),
|
|
194
|
+
categoriaId: z.string().optional(),
|
|
195
|
+
centroCustoId: z.string().optional(),
|
|
196
|
+
metodo: z.string().optional(),
|
|
197
|
+
descricao: z.string().optional(),
|
|
198
|
+
})
|
|
199
|
+
.superRefine((values, ctx) => {
|
|
200
|
+
if (values.installments.length !== values.installmentsCount) {
|
|
201
|
+
ctx.addIssue({
|
|
202
|
+
code: z.ZodIssueCode.custom,
|
|
203
|
+
path: ['installments'],
|
|
204
|
+
message: 'Quantidade de parcelas não confere com o detalhamento',
|
|
205
|
+
});
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
const installmentsTotalCents = values.installments.reduce(
|
|
209
|
+
(acc, installment) => acc + Math.round((installment.amount || 0) * 100),
|
|
210
|
+
0
|
|
211
|
+
);
|
|
212
|
+
const totalCents = Math.round((values.valor || 0) * 100);
|
|
213
|
+
|
|
214
|
+
if (installmentsTotalCents !== totalCents) {
|
|
215
|
+
ctx.addIssue({
|
|
216
|
+
code: z.ZodIssueCode.custom,
|
|
217
|
+
path: ['installments'],
|
|
218
|
+
message: 'A soma das parcelas deve ser igual ao valor total',
|
|
219
|
+
});
|
|
220
|
+
}
|
|
221
|
+
});
|
|
84
222
|
|
|
85
223
|
type NewTitleFormValues = z.infer<typeof newTitleFormSchema>;
|
|
86
224
|
|
|
@@ -101,6 +239,12 @@ function NovoTituloSheet({
|
|
|
101
239
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
102
240
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
103
241
|
const [isExtractingFileData, setIsExtractingFileData] = useState(false);
|
|
242
|
+
const [isInstallmentsEdited, setIsInstallmentsEdited] = useState(false);
|
|
243
|
+
const [autoRedistributeInstallments, setAutoRedistributeInstallments] =
|
|
244
|
+
useState(true);
|
|
245
|
+
const redistributionTimeoutRef = useRef<
|
|
246
|
+
Record<number, ReturnType<typeof setTimeout>>
|
|
247
|
+
>({});
|
|
104
248
|
const [extractionConfidence, setExtractionConfidence] = useState<
|
|
105
249
|
number | null
|
|
106
250
|
>(null);
|
|
@@ -133,6 +277,8 @@ function NovoTituloSheet({
|
|
|
133
277
|
competencia: '',
|
|
134
278
|
vencimento: '',
|
|
135
279
|
valor: 0,
|
|
280
|
+
installmentsCount: 1,
|
|
281
|
+
installments: [{ dueDate: '', amount: 0 }],
|
|
136
282
|
categoriaId: '',
|
|
137
283
|
centroCustoId: '',
|
|
138
284
|
metodo: '',
|
|
@@ -140,6 +286,101 @@ function NovoTituloSheet({
|
|
|
140
286
|
},
|
|
141
287
|
});
|
|
142
288
|
|
|
289
|
+
const { fields: installmentFields, replace: replaceInstallments } =
|
|
290
|
+
useFieldArray({
|
|
291
|
+
control: form.control,
|
|
292
|
+
name: 'installments',
|
|
293
|
+
});
|
|
294
|
+
|
|
295
|
+
const watchedInstallmentsCount = form.watch('installmentsCount');
|
|
296
|
+
const watchedTotalValue = form.watch('valor');
|
|
297
|
+
const watchedDueDate = form.watch('vencimento');
|
|
298
|
+
const watchedInstallments = form.watch('installments');
|
|
299
|
+
|
|
300
|
+
useEffect(() => {
|
|
301
|
+
if (isInstallmentsEdited) {
|
|
302
|
+
return;
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
replaceInstallments(
|
|
306
|
+
buildEqualInstallments(
|
|
307
|
+
watchedInstallmentsCount,
|
|
308
|
+
watchedTotalValue,
|
|
309
|
+
watchedDueDate
|
|
310
|
+
)
|
|
311
|
+
);
|
|
312
|
+
}, [
|
|
313
|
+
isInstallmentsEdited,
|
|
314
|
+
replaceInstallments,
|
|
315
|
+
watchedDueDate,
|
|
316
|
+
watchedInstallmentsCount,
|
|
317
|
+
watchedTotalValue,
|
|
318
|
+
]);
|
|
319
|
+
|
|
320
|
+
const installmentsTotal = (watchedInstallments || []).reduce(
|
|
321
|
+
(acc, installment) => acc + Number(installment?.amount || 0),
|
|
322
|
+
0
|
|
323
|
+
);
|
|
324
|
+
const installmentsDiffCents = Math.abs(
|
|
325
|
+
Math.round(installmentsTotal * 100) -
|
|
326
|
+
Math.round((watchedTotalValue || 0) * 100)
|
|
327
|
+
);
|
|
328
|
+
|
|
329
|
+
const clearScheduledRedistribution = (index: number) => {
|
|
330
|
+
const timeout = redistributionTimeoutRef.current[index];
|
|
331
|
+
if (!timeout) {
|
|
332
|
+
return;
|
|
333
|
+
}
|
|
334
|
+
|
|
335
|
+
clearTimeout(timeout);
|
|
336
|
+
delete redistributionTimeoutRef.current[index];
|
|
337
|
+
};
|
|
338
|
+
|
|
339
|
+
const runInstallmentRedistribution = (index: number) => {
|
|
340
|
+
if (!autoRedistributeInstallments) {
|
|
341
|
+
return;
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
const currentInstallments = form.getValues('installments');
|
|
345
|
+
const editedInstallmentAmount = Number(
|
|
346
|
+
currentInstallments[index]?.amount || 0
|
|
347
|
+
);
|
|
348
|
+
|
|
349
|
+
const redistributedInstallments = redistributeRemainingInstallments(
|
|
350
|
+
currentInstallments,
|
|
351
|
+
index,
|
|
352
|
+
editedInstallmentAmount,
|
|
353
|
+
form.getValues('valor')
|
|
354
|
+
);
|
|
355
|
+
|
|
356
|
+
replaceInstallments(redistributedInstallments);
|
|
357
|
+
};
|
|
358
|
+
|
|
359
|
+
const scheduleInstallmentRedistribution = (index: number) => {
|
|
360
|
+
clearScheduledRedistribution(index);
|
|
361
|
+
|
|
362
|
+
redistributionTimeoutRef.current[index] = setTimeout(() => {
|
|
363
|
+
runInstallmentRedistribution(index);
|
|
364
|
+
delete redistributionTimeoutRef.current[index];
|
|
365
|
+
}, INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS);
|
|
366
|
+
};
|
|
367
|
+
|
|
368
|
+
useEffect(() => {
|
|
369
|
+
if (autoRedistributeInstallments) {
|
|
370
|
+
return;
|
|
371
|
+
}
|
|
372
|
+
|
|
373
|
+
Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
|
|
374
|
+
redistributionTimeoutRef.current = {};
|
|
375
|
+
}, [autoRedistributeInstallments]);
|
|
376
|
+
|
|
377
|
+
useEffect(() => {
|
|
378
|
+
return () => {
|
|
379
|
+
Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
|
|
380
|
+
redistributionTimeoutRef.current = {};
|
|
381
|
+
};
|
|
382
|
+
}, []);
|
|
383
|
+
|
|
143
384
|
const handleSubmit = async (values: NewTitleFormValues) => {
|
|
144
385
|
try {
|
|
145
386
|
await request({
|
|
@@ -161,6 +402,11 @@ function NovoTituloSheet({
|
|
|
161
402
|
: undefined,
|
|
162
403
|
payment_channel: values.metodo || undefined,
|
|
163
404
|
description: values.descricao?.trim() || undefined,
|
|
405
|
+
installments: values.installments.map((installment, index) => ({
|
|
406
|
+
installment_number: index + 1,
|
|
407
|
+
due_date: installment.dueDate || values.vencimento,
|
|
408
|
+
amount: installment.amount,
|
|
409
|
+
})),
|
|
164
410
|
attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
|
|
165
411
|
},
|
|
166
412
|
});
|
|
@@ -171,6 +417,8 @@ function NovoTituloSheet({
|
|
|
171
417
|
setUploadedFileName('');
|
|
172
418
|
setExtractionConfidence(null);
|
|
173
419
|
setExtractionWarnings([]);
|
|
420
|
+
setIsInstallmentsEdited(false);
|
|
421
|
+
setAutoRedistributeInstallments(true);
|
|
174
422
|
setOpen(false);
|
|
175
423
|
showToastHandler?.('success', 'Título criado com sucesso');
|
|
176
424
|
} catch {
|
|
@@ -185,6 +433,8 @@ function NovoTituloSheet({
|
|
|
185
433
|
setExtractionConfidence(null);
|
|
186
434
|
setExtractionWarnings([]);
|
|
187
435
|
setUploadProgress(0);
|
|
436
|
+
setIsInstallmentsEdited(false);
|
|
437
|
+
setAutoRedistributeInstallments(true);
|
|
188
438
|
setOpen(false);
|
|
189
439
|
};
|
|
190
440
|
|
|
@@ -507,6 +757,168 @@ function NovoTituloSheet({
|
|
|
507
757
|
)}
|
|
508
758
|
/>
|
|
509
759
|
|
|
760
|
+
<FormField
|
|
761
|
+
control={form.control}
|
|
762
|
+
name="installmentsCount"
|
|
763
|
+
render={({ field }) => (
|
|
764
|
+
<FormItem>
|
|
765
|
+
<FormLabel>Quantidade de Parcelas</FormLabel>
|
|
766
|
+
<FormControl>
|
|
767
|
+
<Input
|
|
768
|
+
type="number"
|
|
769
|
+
min={1}
|
|
770
|
+
max={120}
|
|
771
|
+
value={field.value}
|
|
772
|
+
onChange={(event) => {
|
|
773
|
+
const nextValue = Number(event.target.value || 1);
|
|
774
|
+
field.onChange(
|
|
775
|
+
Number.isNaN(nextValue) ? 1 : nextValue
|
|
776
|
+
);
|
|
777
|
+
}}
|
|
778
|
+
/>
|
|
779
|
+
</FormControl>
|
|
780
|
+
<FormMessage />
|
|
781
|
+
</FormItem>
|
|
782
|
+
)}
|
|
783
|
+
/>
|
|
784
|
+
|
|
785
|
+
<div className="space-y-3 rounded-md border p-3">
|
|
786
|
+
<div className="flex items-center justify-between gap-2">
|
|
787
|
+
<p className="text-sm font-medium">Parcelas</p>
|
|
788
|
+
<Button
|
|
789
|
+
type="button"
|
|
790
|
+
variant="outline"
|
|
791
|
+
size="sm"
|
|
792
|
+
onClick={() => {
|
|
793
|
+
setIsInstallmentsEdited(false);
|
|
794
|
+
replaceInstallments(
|
|
795
|
+
buildEqualInstallments(
|
|
796
|
+
form.getValues('installmentsCount'),
|
|
797
|
+
form.getValues('valor'),
|
|
798
|
+
form.getValues('vencimento')
|
|
799
|
+
)
|
|
800
|
+
);
|
|
801
|
+
}}
|
|
802
|
+
>
|
|
803
|
+
Recalcular automaticamente
|
|
804
|
+
</Button>
|
|
805
|
+
</div>
|
|
806
|
+
|
|
807
|
+
<div className="flex items-center gap-2">
|
|
808
|
+
<Checkbox
|
|
809
|
+
id="auto-redistribute-installments-payable"
|
|
810
|
+
checked={autoRedistributeInstallments}
|
|
811
|
+
onCheckedChange={(checked) =>
|
|
812
|
+
setAutoRedistributeInstallments(checked === true)
|
|
813
|
+
}
|
|
814
|
+
/>
|
|
815
|
+
<Label
|
|
816
|
+
htmlFor="auto-redistribute-installments-payable"
|
|
817
|
+
className="text-xs text-muted-foreground"
|
|
818
|
+
>
|
|
819
|
+
Redistribuir automaticamente o restante ao editar parcela
|
|
820
|
+
</Label>
|
|
821
|
+
</div>
|
|
822
|
+
{autoRedistributeInstallments && (
|
|
823
|
+
<p className="text-xs text-muted-foreground">
|
|
824
|
+
A redistribuição ocorre ao parar de digitar e ao sair do
|
|
825
|
+
campo.
|
|
826
|
+
</p>
|
|
827
|
+
)}
|
|
828
|
+
|
|
829
|
+
<div className="space-y-2">
|
|
830
|
+
{installmentFields.map((installment, index) => (
|
|
831
|
+
<div
|
|
832
|
+
key={installment.id}
|
|
833
|
+
className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
834
|
+
>
|
|
835
|
+
<div className="flex items-center text-sm text-muted-foreground">
|
|
836
|
+
#{index + 1}
|
|
837
|
+
</div>
|
|
838
|
+
|
|
839
|
+
<FormField
|
|
840
|
+
control={form.control}
|
|
841
|
+
name={`installments.${index}.dueDate` as const}
|
|
842
|
+
render={({ field }) => (
|
|
843
|
+
<FormItem>
|
|
844
|
+
<FormLabel className="text-xs">
|
|
845
|
+
Vencimento
|
|
846
|
+
</FormLabel>
|
|
847
|
+
<FormControl>
|
|
848
|
+
<Input
|
|
849
|
+
type="date"
|
|
850
|
+
{...field}
|
|
851
|
+
value={field.value || ''}
|
|
852
|
+
onChange={(event) => {
|
|
853
|
+
setIsInstallmentsEdited(true);
|
|
854
|
+
field.onChange(event);
|
|
855
|
+
}}
|
|
856
|
+
/>
|
|
857
|
+
</FormControl>
|
|
858
|
+
<FormMessage />
|
|
859
|
+
</FormItem>
|
|
860
|
+
)}
|
|
861
|
+
/>
|
|
862
|
+
|
|
863
|
+
<FormField
|
|
864
|
+
control={form.control}
|
|
865
|
+
name={`installments.${index}.amount` as const}
|
|
866
|
+
render={({ field }) => (
|
|
867
|
+
<FormItem>
|
|
868
|
+
<FormLabel className="text-xs">Valor</FormLabel>
|
|
869
|
+
<FormControl>
|
|
870
|
+
<InputMoney
|
|
871
|
+
ref={field.ref}
|
|
872
|
+
name={field.name}
|
|
873
|
+
value={field.value}
|
|
874
|
+
onBlur={() => {
|
|
875
|
+
field.onBlur();
|
|
876
|
+
|
|
877
|
+
if (!autoRedistributeInstallments) {
|
|
878
|
+
return;
|
|
879
|
+
}
|
|
880
|
+
|
|
881
|
+
clearScheduledRedistribution(index);
|
|
882
|
+
runInstallmentRedistribution(index);
|
|
883
|
+
}}
|
|
884
|
+
onValueChange={(value) => {
|
|
885
|
+
setIsInstallmentsEdited(true);
|
|
886
|
+
field.onChange(value ?? 0);
|
|
887
|
+
|
|
888
|
+
if (!autoRedistributeInstallments) {
|
|
889
|
+
return;
|
|
890
|
+
}
|
|
891
|
+
|
|
892
|
+
scheduleInstallmentRedistribution(index);
|
|
893
|
+
}}
|
|
894
|
+
placeholder="0,00"
|
|
895
|
+
/>
|
|
896
|
+
</FormControl>
|
|
897
|
+
<FormMessage />
|
|
898
|
+
</FormItem>
|
|
899
|
+
)}
|
|
900
|
+
/>
|
|
901
|
+
</div>
|
|
902
|
+
))}
|
|
903
|
+
</div>
|
|
904
|
+
|
|
905
|
+
<p
|
|
906
|
+
className={`text-xs ${
|
|
907
|
+
installmentsDiffCents === 0
|
|
908
|
+
? 'text-muted-foreground'
|
|
909
|
+
: 'text-destructive'
|
|
910
|
+
}`}
|
|
911
|
+
>
|
|
912
|
+
Soma das parcelas: {installmentsTotal.toFixed(2)}
|
|
913
|
+
{installmentsDiffCents > 0 && ' (ajuste necessário)'}
|
|
914
|
+
</p>
|
|
915
|
+
{form.formState.errors.installments?.message && (
|
|
916
|
+
<p className="text-xs text-destructive">
|
|
917
|
+
{form.formState.errors.installments.message}
|
|
918
|
+
</p>
|
|
919
|
+
)}
|
|
920
|
+
</div>
|
|
921
|
+
|
|
510
922
|
<FormField
|
|
511
923
|
control={form.control}
|
|
512
924
|
name="categoriaId"
|
|
@@ -855,15 +1267,27 @@ export default function TitulosPagarPage() {
|
|
|
855
1267
|
{t('table.actions.edit')}
|
|
856
1268
|
</DropdownMenuItem>
|
|
857
1269
|
<DropdownMenuSeparator />
|
|
858
|
-
<DropdownMenuItem
|
|
1270
|
+
<DropdownMenuItem
|
|
1271
|
+
disabled={titulo.status !== 'rascunho'}
|
|
1272
|
+
>
|
|
859
1273
|
<CheckCircle className="mr-2 h-4 w-4" />
|
|
860
1274
|
{t('table.actions.approve')}
|
|
861
1275
|
</DropdownMenuItem>
|
|
862
|
-
<DropdownMenuItem
|
|
1276
|
+
<DropdownMenuItem
|
|
1277
|
+
disabled={
|
|
1278
|
+
!['aprovado', 'aberto', 'parcial'].includes(
|
|
1279
|
+
titulo.status
|
|
1280
|
+
)
|
|
1281
|
+
}
|
|
1282
|
+
>
|
|
863
1283
|
<Download className="mr-2 h-4 w-4" />
|
|
864
1284
|
{t('table.actions.settle')}
|
|
865
1285
|
</DropdownMenuItem>
|
|
866
|
-
<DropdownMenuItem
|
|
1286
|
+
<DropdownMenuItem
|
|
1287
|
+
disabled={
|
|
1288
|
+
!['parcial', 'liquidado'].includes(titulo.status)
|
|
1289
|
+
}
|
|
1290
|
+
>
|
|
867
1291
|
<Undo className="mr-2 h-4 w-4" />
|
|
868
1292
|
{t('table.actions.reverse')}
|
|
869
1293
|
</DropdownMenuItem>
|