@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
|
@@ -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"
|
|
@@ -838,7 +1250,13 @@ export default function TitulosReceberPage() {
|
|
|
838
1250
|
{t('table.actions.edit')}
|
|
839
1251
|
</DropdownMenuItem>
|
|
840
1252
|
<DropdownMenuSeparator />
|
|
841
|
-
<DropdownMenuItem
|
|
1253
|
+
<DropdownMenuItem
|
|
1254
|
+
disabled={
|
|
1255
|
+
!['aprovado', 'aberto', 'parcial'].includes(
|
|
1256
|
+
titulo.status
|
|
1257
|
+
)
|
|
1258
|
+
}
|
|
1259
|
+
>
|
|
842
1260
|
<Download className="mr-2 h-4 w-4" />
|
|
843
1261
|
{t('table.actions.registerReceipt')}
|
|
844
1262
|
</DropdownMenuItem>
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@hed-hog/finance",
|
|
3
|
-
"version": "0.0.
|
|
3
|
+
"version": "0.0.239",
|
|
4
4
|
"main": "dist/index.js",
|
|
5
5
|
"types": "dist/index.d.ts",
|
|
6
6
|
"dependencies": {
|
|
@@ -10,13 +10,13 @@
|
|
|
10
10
|
"@nestjs/jwt": "^11",
|
|
11
11
|
"@nestjs/mapped-types": "*",
|
|
12
12
|
"@hed-hog/api-prisma": "0.0.4",
|
|
13
|
-
"@hed-hog/api-locale": "0.0.11",
|
|
14
13
|
"@hed-hog/api-pagination": "0.0.5",
|
|
15
|
-
"@hed-hog/
|
|
14
|
+
"@hed-hog/api-locale": "0.0.11",
|
|
16
15
|
"@hed-hog/api-types": "0.0.1",
|
|
16
|
+
"@hed-hog/tag": "0.0.239",
|
|
17
17
|
"@hed-hog/api": "0.0.3",
|
|
18
|
-
"@hed-hog/contact": "0.0.
|
|
19
|
-
"@hed-hog/core": "0.0.
|
|
18
|
+
"@hed-hog/contact": "0.0.239",
|
|
19
|
+
"@hed-hog/core": "0.0.239"
|
|
20
20
|
},
|
|
21
21
|
"exports": {
|
|
22
22
|
".": {
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
import { Type } from 'class-transformer';
|
|
2
|
+
import {
|
|
3
|
+
IsDateString,
|
|
4
|
+
IsInt,
|
|
5
|
+
IsNumber,
|
|
6
|
+
IsOptional,
|
|
7
|
+
IsString,
|
|
8
|
+
Min,
|
|
9
|
+
} from 'class-validator';
|
|
10
|
+
|
|
11
|
+
export class SettleInstallmentDto {
|
|
12
|
+
@Type(() => Number)
|
|
13
|
+
@IsInt()
|
|
14
|
+
installment_id: number;
|
|
15
|
+
|
|
16
|
+
@Type(() => Number)
|
|
17
|
+
@IsNumber()
|
|
18
|
+
@Min(0.01)
|
|
19
|
+
amount: number;
|
|
20
|
+
|
|
21
|
+
@IsOptional()
|
|
22
|
+
@IsDateString()
|
|
23
|
+
settled_at?: string;
|
|
24
|
+
|
|
25
|
+
@IsOptional()
|
|
26
|
+
@Type(() => Number)
|
|
27
|
+
@IsInt()
|
|
28
|
+
bank_account_id?: number;
|
|
29
|
+
|
|
30
|
+
@IsOptional()
|
|
31
|
+
@IsString()
|
|
32
|
+
payment_channel?: string;
|
|
33
|
+
|
|
34
|
+
@IsOptional()
|
|
35
|
+
@Type(() => Number)
|
|
36
|
+
@IsNumber()
|
|
37
|
+
@Min(0)
|
|
38
|
+
discount?: number;
|
|
39
|
+
|
|
40
|
+
@IsOptional()
|
|
41
|
+
@Type(() => Number)
|
|
42
|
+
@IsNumber()
|
|
43
|
+
@Min(0)
|
|
44
|
+
interest?: number;
|
|
45
|
+
|
|
46
|
+
@IsOptional()
|
|
47
|
+
@Type(() => Number)
|
|
48
|
+
@IsNumber()
|
|
49
|
+
@Min(0)
|
|
50
|
+
penalty?: number;
|
|
51
|
+
|
|
52
|
+
@IsOptional()
|
|
53
|
+
@IsString()
|
|
54
|
+
description?: string;
|
|
55
|
+
}
|