@hed-hog/finance 0.0.253 → 0.0.256
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/finance-installments.controller.d.ts +66 -2
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance.service.d.ts +66 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +63 -7
- package/dist/finance.service.js.map +1 -1
- package/hedhog/frontend/app/_components/finance-entity-field-with-create.tsx.ejs +572 -0
- package/hedhog/frontend/app/_components/finance-title-actions-menu.tsx.ejs +244 -0
- package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +143 -51
- package/hedhog/frontend/app/_lib/title-action-rules.ts.ejs +36 -0
- package/hedhog/frontend/app/accounts-payable/installments/[id]/page.tsx.ejs +189 -242
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +1189 -545
- package/hedhog/frontend/app/accounts-receivable/installments/[id]/page.tsx.ejs +176 -133
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +1459 -312
- package/hedhog/frontend/app/page.tsx.ejs +15 -4
- package/hedhog/frontend/messages/en.json +294 -5
- package/hedhog/frontend/messages/pt.json +294 -5
- package/package.json +5 -5
- package/src/finance.service.ts +85 -10
|
@@ -1,16 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
CategoryFieldWithCreate,
|
|
5
|
+
CostCenterFieldWithCreate,
|
|
6
|
+
} from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
|
|
7
|
+
import { FinanceTitleActionsMenu } from '@/app/(app)/(libraries)/finance/_components/finance-title-actions-menu';
|
|
3
8
|
import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/finance/_components/person-field-with-create';
|
|
4
9
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
5
10
|
import { Button } from '@/components/ui/button';
|
|
6
11
|
import { Checkbox } from '@/components/ui/checkbox';
|
|
7
|
-
import {
|
|
8
|
-
DropdownMenu,
|
|
9
|
-
DropdownMenuContent,
|
|
10
|
-
DropdownMenuItem,
|
|
11
|
-
DropdownMenuSeparator,
|
|
12
|
-
DropdownMenuTrigger,
|
|
13
|
-
} from '@/components/ui/dropdown-menu';
|
|
14
12
|
import { FilterBar } from '@/components/ui/filter-bar';
|
|
15
13
|
import {
|
|
16
14
|
Form,
|
|
@@ -50,20 +48,14 @@ import {
|
|
|
50
48
|
TableRow,
|
|
51
49
|
} from '@/components/ui/table';
|
|
52
50
|
import { Textarea } from '@/components/ui/textarea';
|
|
51
|
+
import {
|
|
52
|
+
Tooltip,
|
|
53
|
+
TooltipContent,
|
|
54
|
+
TooltipTrigger,
|
|
55
|
+
} from '@/components/ui/tooltip';
|
|
53
56
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
54
57
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
55
|
-
import {
|
|
56
|
-
CheckCircle,
|
|
57
|
-
Download,
|
|
58
|
-
Edit,
|
|
59
|
-
Eye,
|
|
60
|
-
Loader2,
|
|
61
|
-
MoreHorizontal,
|
|
62
|
-
Paperclip,
|
|
63
|
-
Plus,
|
|
64
|
-
Undo,
|
|
65
|
-
XCircle,
|
|
66
|
-
} from 'lucide-react';
|
|
58
|
+
import { Loader2, Paperclip, Plus, Trash2, Upload } from 'lucide-react';
|
|
67
59
|
import { useTranslations } from 'next-intl';
|
|
68
60
|
import Link from 'next/link';
|
|
69
61
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
@@ -71,6 +63,14 @@ import { useEffect, useMemo, useRef, useState } from 'react';
|
|
|
71
63
|
import { useFieldArray, useForm } from 'react-hook-form';
|
|
72
64
|
import { z } from 'zod';
|
|
73
65
|
import { formatarData } from '../../_lib/formatters';
|
|
66
|
+
import {
|
|
67
|
+
canApproveTitle,
|
|
68
|
+
canCancelTitle,
|
|
69
|
+
canEditTitle,
|
|
70
|
+
canReverseTitle,
|
|
71
|
+
canSettleTitle,
|
|
72
|
+
getFirstActiveSettlementId,
|
|
73
|
+
} from '../../_lib/title-action-rules';
|
|
74
74
|
import { useFinanceData } from '../../_lib/use-finance-data';
|
|
75
75
|
|
|
76
76
|
const INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS = 300;
|
|
@@ -170,69 +170,87 @@ const redistributeRemainingInstallments = (
|
|
|
170
170
|
});
|
|
171
171
|
};
|
|
172
172
|
|
|
173
|
-
const
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
173
|
+
const getNewTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
|
|
174
|
+
z
|
|
175
|
+
.object({
|
|
176
|
+
documento: z.string().trim().min(1, t('validation.documentRequired')),
|
|
177
|
+
fornecedorId: z.string().min(1, t('validation.supplierRequired')),
|
|
178
|
+
competencia: z.string().optional(),
|
|
179
|
+
vencimento: z.string().min(1, t('validation.dueDateRequired')),
|
|
180
|
+
valor: z.number().min(0.01, t('validation.amountGreaterThanZero')),
|
|
181
|
+
installmentsCount: z.coerce
|
|
182
|
+
.number({ invalid_type_error: t('validation.invalidInstallmentCount') })
|
|
183
|
+
.int(t('validation.invalidInstallmentCount'))
|
|
184
|
+
.min(1, t('validation.installmentsMin'))
|
|
185
|
+
.max(120, t('validation.installmentsMax')),
|
|
186
|
+
installments: z
|
|
187
|
+
.array(
|
|
188
|
+
z.object({
|
|
189
|
+
dueDate: z
|
|
190
|
+
.string()
|
|
191
|
+
.min(1, t('validation.installmentDueDateRequired')),
|
|
192
|
+
amount: z
|
|
193
|
+
.number()
|
|
194
|
+
.min(0.01, t('validation.installmentAmountGreaterThanZero')),
|
|
195
|
+
})
|
|
196
|
+
)
|
|
197
|
+
.min(1, t('validation.installmentsRequired')),
|
|
198
|
+
categoriaId: z.string().optional(),
|
|
199
|
+
centroCustoId: z.string().optional(),
|
|
200
|
+
metodo: z.string().optional(),
|
|
201
|
+
descricao: z.string().optional(),
|
|
202
|
+
})
|
|
203
|
+
.superRefine((values, ctx) => {
|
|
204
|
+
if (values.installments.length !== values.installmentsCount) {
|
|
205
|
+
ctx.addIssue({
|
|
206
|
+
code: z.ZodIssueCode.custom,
|
|
207
|
+
path: ['installments'],
|
|
208
|
+
message: t('validation.installmentsCountMismatch'),
|
|
209
|
+
});
|
|
210
|
+
}
|
|
208
211
|
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
212
|
+
const installmentsTotalCents = values.installments.reduce(
|
|
213
|
+
(acc, installment) => acc + Math.round((installment.amount || 0) * 100),
|
|
214
|
+
0
|
|
215
|
+
);
|
|
216
|
+
const totalCents = Math.round((values.valor || 0) * 100);
|
|
214
217
|
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
218
|
+
if (installmentsTotalCents !== totalCents) {
|
|
219
|
+
ctx.addIssue({
|
|
220
|
+
code: z.ZodIssueCode.custom,
|
|
221
|
+
path: ['installments'],
|
|
222
|
+
message: t('validation.installmentsSumMismatch'),
|
|
223
|
+
});
|
|
224
|
+
}
|
|
225
|
+
});
|
|
226
|
+
|
|
227
|
+
type NewTitleFormValues = z.infer<ReturnType<typeof getNewTitleFormSchema>>;
|
|
228
|
+
|
|
229
|
+
const getSettleTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
|
|
230
|
+
z.object({
|
|
231
|
+
installmentId: z.string().min(1, t('validation.installmentRequired')),
|
|
232
|
+
amount: z.number().min(0.01, t('validation.amountGreaterThanZero')),
|
|
233
|
+
description: z.string().optional(),
|
|
222
234
|
});
|
|
223
235
|
|
|
224
|
-
type
|
|
236
|
+
type SettleTitleFormValues = z.infer<
|
|
237
|
+
ReturnType<typeof getSettleTitleFormSchema>
|
|
238
|
+
>;
|
|
225
239
|
|
|
226
240
|
function NovoTituloSheet({
|
|
227
241
|
categorias,
|
|
228
242
|
centrosCusto,
|
|
229
243
|
t,
|
|
230
244
|
onCreated,
|
|
245
|
+
onCategoriesUpdated,
|
|
246
|
+
onCostCentersUpdated,
|
|
231
247
|
}: {
|
|
232
248
|
categorias: any[];
|
|
233
249
|
centrosCusto: any[];
|
|
234
250
|
t: ReturnType<typeof useTranslations>;
|
|
235
251
|
onCreated: () => Promise<any> | void;
|
|
252
|
+
onCategoriesUpdated?: () => Promise<any> | void;
|
|
253
|
+
onCostCentersUpdated?: () => Promise<any> | void;
|
|
236
254
|
}) {
|
|
237
255
|
const { request, showToastHandler } = useApp();
|
|
238
256
|
const [open, setOpen] = useState(false);
|
|
@@ -251,6 +269,7 @@ function NovoTituloSheet({
|
|
|
251
269
|
>(null);
|
|
252
270
|
const [extractionWarnings, setExtractionWarnings] = useState<string[]>([]);
|
|
253
271
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
272
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
254
273
|
|
|
255
274
|
const normalizeFilenameForDisplay = (filename: string) => {
|
|
256
275
|
if (!filename) {
|
|
@@ -270,6 +289,8 @@ function NovoTituloSheet({
|
|
|
270
289
|
}
|
|
271
290
|
};
|
|
272
291
|
|
|
292
|
+
const newTitleFormSchema = useMemo(() => getNewTitleFormSchema(t), [t]);
|
|
293
|
+
|
|
273
294
|
const form = useForm<NewTitleFormValues>({
|
|
274
295
|
resolver: zodResolver(newTitleFormSchema),
|
|
275
296
|
defaultValues: {
|
|
@@ -421,9 +442,9 @@ function NovoTituloSheet({
|
|
|
421
442
|
setIsInstallmentsEdited(false);
|
|
422
443
|
setAutoRedistributeInstallments(true);
|
|
423
444
|
setOpen(false);
|
|
424
|
-
showToastHandler?.('success', '
|
|
445
|
+
showToastHandler?.('success', t('messages.createSuccess'));
|
|
425
446
|
} catch {
|
|
426
|
-
showToastHandler?.('error', '
|
|
447
|
+
showToastHandler?.('error', t('messages.createError'));
|
|
427
448
|
}
|
|
428
449
|
};
|
|
429
450
|
|
|
@@ -439,6 +460,25 @@ function NovoTituloSheet({
|
|
|
439
460
|
setOpen(false);
|
|
440
461
|
};
|
|
441
462
|
|
|
463
|
+
const clearUploadedFile = () => {
|
|
464
|
+
setUploadedFileId(null);
|
|
465
|
+
setUploadedFileName('');
|
|
466
|
+
setExtractionConfidence(null);
|
|
467
|
+
setExtractionWarnings([]);
|
|
468
|
+
setUploadProgress(0);
|
|
469
|
+
|
|
470
|
+
if (fileInputRef.current) {
|
|
471
|
+
fileInputRef.current.value = '';
|
|
472
|
+
}
|
|
473
|
+
};
|
|
474
|
+
|
|
475
|
+
const handleSelectFile = () => {
|
|
476
|
+
if (fileInputRef.current) {
|
|
477
|
+
fileInputRef.current.value = '';
|
|
478
|
+
fileInputRef.current.click();
|
|
479
|
+
}
|
|
480
|
+
};
|
|
481
|
+
|
|
442
482
|
const uploadRelatedFile = async (file: File) => {
|
|
443
483
|
setIsUploadingFile(true);
|
|
444
484
|
setUploadProgress(0);
|
|
@@ -466,7 +506,7 @@ function NovoTituloSheet({
|
|
|
466
506
|
});
|
|
467
507
|
|
|
468
508
|
if (!data?.id) {
|
|
469
|
-
throw new Error('
|
|
509
|
+
throw new Error(t('messages.invalidFile'));
|
|
470
510
|
}
|
|
471
511
|
|
|
472
512
|
setUploadedFileId(data.id);
|
|
@@ -474,7 +514,7 @@ function NovoTituloSheet({
|
|
|
474
514
|
normalizeFilenameForDisplay(data.filename || file.name)
|
|
475
515
|
);
|
|
476
516
|
setUploadProgress(100);
|
|
477
|
-
showToastHandler?.('success', '
|
|
517
|
+
showToastHandler?.('success', t('messages.attachSuccess'));
|
|
478
518
|
|
|
479
519
|
setIsExtractingFileData(true);
|
|
480
520
|
try {
|
|
@@ -563,17 +603,11 @@ function NovoTituloSheet({
|
|
|
563
603
|
});
|
|
564
604
|
}
|
|
565
605
|
|
|
566
|
-
showToastHandler?.(
|
|
567
|
-
'success',
|
|
568
|
-
'Dados da fatura extraídos e preenchidos automaticamente'
|
|
569
|
-
);
|
|
606
|
+
showToastHandler?.('success', t('messages.aiExtractSuccess'));
|
|
570
607
|
} catch {
|
|
571
608
|
setExtractionConfidence(null);
|
|
572
609
|
setExtractionWarnings([]);
|
|
573
|
-
showToastHandler?.(
|
|
574
|
-
'error',
|
|
575
|
-
'Não foi possível extrair os dados automaticamente'
|
|
576
|
-
);
|
|
610
|
+
showToastHandler?.('error', t('messages.aiExtractError'));
|
|
577
611
|
} finally {
|
|
578
612
|
setIsExtractingFileData(false);
|
|
579
613
|
}
|
|
@@ -583,7 +617,7 @@ function NovoTituloSheet({
|
|
|
583
617
|
setExtractionConfidence(null);
|
|
584
618
|
setExtractionWarnings([]);
|
|
585
619
|
setUploadProgress(0);
|
|
586
|
-
showToastHandler?.('error', '
|
|
620
|
+
showToastHandler?.('error', t('messages.uploadError'));
|
|
587
621
|
} finally {
|
|
588
622
|
setIsUploadingFile(false);
|
|
589
623
|
}
|
|
@@ -604,11 +638,13 @@ function NovoTituloSheet({
|
|
|
604
638
|
</SheetHeader>
|
|
605
639
|
<Form {...form}>
|
|
606
640
|
<form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
|
|
607
|
-
<div className="grid gap-
|
|
608
|
-
<div className="grid gap-2">
|
|
609
|
-
<
|
|
610
|
-
|
|
641
|
+
<div className="grid gap-3">
|
|
642
|
+
<div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
|
|
643
|
+
<div className="grid gap-2">
|
|
644
|
+
<FormLabel>{t('common.upload.label')}</FormLabel>
|
|
611
645
|
<Input
|
|
646
|
+
ref={fileInputRef}
|
|
647
|
+
className="hidden"
|
|
612
648
|
type="file"
|
|
613
649
|
accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
|
|
614
650
|
onChange={(event) => {
|
|
@@ -617,11 +653,7 @@ function NovoTituloSheet({
|
|
|
617
653
|
return;
|
|
618
654
|
}
|
|
619
655
|
|
|
620
|
-
|
|
621
|
-
setUploadedFileName('');
|
|
622
|
-
setExtractionConfidence(null);
|
|
623
|
-
setExtractionWarnings([]);
|
|
624
|
-
setUploadProgress(0);
|
|
656
|
+
clearUploadedFile();
|
|
625
657
|
void uploadRelatedFile(file);
|
|
626
658
|
}}
|
|
627
659
|
disabled={
|
|
@@ -630,67 +662,128 @@ function NovoTituloSheet({
|
|
|
630
662
|
form.formState.isSubmitting
|
|
631
663
|
}
|
|
632
664
|
/>
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
655
|
-
|
|
656
|
-
|
|
665
|
+
|
|
666
|
+
<div className="grid w-full grid-cols-2 gap-2">
|
|
667
|
+
<Tooltip>
|
|
668
|
+
<TooltipTrigger asChild>
|
|
669
|
+
<Button
|
|
670
|
+
type="button"
|
|
671
|
+
variant="outline"
|
|
672
|
+
className={
|
|
673
|
+
uploadedFileId ? 'w-full' : 'col-span-2 w-full'
|
|
674
|
+
}
|
|
675
|
+
onClick={handleSelectFile}
|
|
676
|
+
aria-label={
|
|
677
|
+
uploadedFileId
|
|
678
|
+
? t('common.upload.change')
|
|
679
|
+
: t('common.upload.upload')
|
|
680
|
+
}
|
|
681
|
+
disabled={
|
|
682
|
+
isUploadingFile ||
|
|
683
|
+
isExtractingFileData ||
|
|
684
|
+
form.formState.isSubmitting
|
|
685
|
+
}
|
|
686
|
+
>
|
|
687
|
+
{uploadedFileId ? (
|
|
688
|
+
<Upload className="h-4 w-4" />
|
|
689
|
+
) : (
|
|
690
|
+
<>
|
|
691
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
692
|
+
{t('common.upload.upload')}
|
|
693
|
+
</>
|
|
694
|
+
)}
|
|
695
|
+
</Button>
|
|
696
|
+
</TooltipTrigger>
|
|
697
|
+
<TooltipContent>
|
|
698
|
+
{uploadedFileId
|
|
699
|
+
? t('common.upload.change')
|
|
700
|
+
: t('common.upload.upload')}
|
|
701
|
+
</TooltipContent>
|
|
702
|
+
</Tooltip>
|
|
703
|
+
|
|
704
|
+
{uploadedFileId && (
|
|
705
|
+
<Tooltip>
|
|
706
|
+
<TooltipTrigger asChild>
|
|
707
|
+
<Button
|
|
708
|
+
type="button"
|
|
709
|
+
variant="outline"
|
|
710
|
+
className="w-full"
|
|
711
|
+
onClick={clearUploadedFile}
|
|
712
|
+
aria-label={t('common.upload.remove')}
|
|
713
|
+
disabled={
|
|
714
|
+
isUploadingFile ||
|
|
715
|
+
isExtractingFileData ||
|
|
716
|
+
form.formState.isSubmitting
|
|
717
|
+
}
|
|
718
|
+
>
|
|
719
|
+
<Trash2 className="h-4 w-4" />
|
|
720
|
+
</Button>
|
|
721
|
+
</TooltipTrigger>
|
|
722
|
+
<TooltipContent>
|
|
723
|
+
{t('common.upload.remove')}
|
|
724
|
+
</TooltipContent>
|
|
725
|
+
</Tooltip>
|
|
726
|
+
)}
|
|
657
727
|
</div>
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
664
|
-
|
|
665
|
-
|
|
728
|
+
|
|
729
|
+
<div className="space-y-1">
|
|
730
|
+
{uploadedFileId && (
|
|
731
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
732
|
+
{t('common.upload.selectedPrefix')} {uploadedFileName}
|
|
733
|
+
</p>
|
|
734
|
+
)}
|
|
735
|
+
|
|
736
|
+
{isUploadingFile && !isExtractingFileData && (
|
|
737
|
+
<div className="space-y-1">
|
|
738
|
+
<Progress value={uploadProgress} className="h-2" />
|
|
739
|
+
<p className="text-xs text-muted-foreground">
|
|
740
|
+
{t('common.upload.uploadingProgress', {
|
|
741
|
+
progress: uploadProgress,
|
|
742
|
+
})}
|
|
743
|
+
</p>
|
|
666
744
|
</div>
|
|
667
|
-
|
|
668
|
-
|
|
669
|
-
|
|
745
|
+
)}
|
|
746
|
+
|
|
747
|
+
{isExtractingFileData && (
|
|
748
|
+
<p className="flex items-center gap-2 text-xs text-primary">
|
|
749
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
750
|
+
{t('common.upload.processingAi')}
|
|
670
751
|
</p>
|
|
671
|
-
|
|
672
|
-
|
|
673
|
-
|
|
752
|
+
)}
|
|
753
|
+
|
|
754
|
+
{!isExtractingFileData &&
|
|
755
|
+
extractionConfidence !== null &&
|
|
756
|
+
extractionConfidence < 70 && (
|
|
757
|
+
<p className="text-xs text-destructive">
|
|
758
|
+
{t('common.upload.lowConfidence', {
|
|
759
|
+
confidence: Math.round(extractionConfidence),
|
|
760
|
+
})}
|
|
674
761
|
</p>
|
|
675
762
|
)}
|
|
676
|
-
|
|
763
|
+
|
|
764
|
+
{!isExtractingFileData && extractionWarnings.length > 0 && (
|
|
765
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
766
|
+
{extractionWarnings[0]}
|
|
767
|
+
</p>
|
|
768
|
+
)}
|
|
769
|
+
</div>
|
|
770
|
+
</div>
|
|
771
|
+
|
|
772
|
+
<FormField
|
|
773
|
+
control={form.control}
|
|
774
|
+
name="documento"
|
|
775
|
+
render={({ field }) => (
|
|
776
|
+
<FormItem>
|
|
777
|
+
<FormLabel>{t('fields.document')}</FormLabel>
|
|
778
|
+
<FormControl>
|
|
779
|
+
<Input placeholder="NF-00000" {...field} />
|
|
780
|
+
</FormControl>
|
|
781
|
+
<FormMessage />
|
|
782
|
+
</FormItem>
|
|
677
783
|
)}
|
|
784
|
+
/>
|
|
678
785
|
</div>
|
|
679
786
|
|
|
680
|
-
<FormField
|
|
681
|
-
control={form.control}
|
|
682
|
-
name="documento"
|
|
683
|
-
render={({ field }) => (
|
|
684
|
-
<FormItem>
|
|
685
|
-
<FormLabel>{t('fields.document')}</FormLabel>
|
|
686
|
-
<FormControl>
|
|
687
|
-
<Input placeholder="NF-00000" {...field} />
|
|
688
|
-
</FormControl>
|
|
689
|
-
<FormMessage />
|
|
690
|
-
</FormItem>
|
|
691
|
-
)}
|
|
692
|
-
/>
|
|
693
|
-
|
|
694
787
|
<PersonFieldWithCreate
|
|
695
788
|
form={form}
|
|
696
789
|
name="fornecedorId"
|
|
@@ -699,7 +792,7 @@ function NovoTituloSheet({
|
|
|
699
792
|
selectPlaceholder={t('common.select')}
|
|
700
793
|
/>
|
|
701
794
|
|
|
702
|
-
<div className="grid grid-cols-2 gap-
|
|
795
|
+
<div className="grid grid-cols-2 items-start gap-3">
|
|
703
796
|
<FormField
|
|
704
797
|
control={form.control}
|
|
705
798
|
name="competencia"
|
|
@@ -737,55 +830,61 @@ function NovoTituloSheet({
|
|
|
737
830
|
/>
|
|
738
831
|
</div>
|
|
739
832
|
|
|
740
|
-
<
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
<
|
|
746
|
-
|
|
747
|
-
<
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
833
|
+
<div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
|
|
834
|
+
<FormField
|
|
835
|
+
control={form.control}
|
|
836
|
+
name="valor"
|
|
837
|
+
render={({ field }) => (
|
|
838
|
+
<FormItem>
|
|
839
|
+
<FormLabel>{t('fields.totalValue')}</FormLabel>
|
|
840
|
+
<FormControl>
|
|
841
|
+
<InputMoney
|
|
842
|
+
ref={field.ref}
|
|
843
|
+
name={field.name}
|
|
844
|
+
value={field.value}
|
|
845
|
+
onBlur={field.onBlur}
|
|
846
|
+
onValueChange={(value) => field.onChange(value ?? 0)}
|
|
847
|
+
placeholder="0,00"
|
|
848
|
+
/>
|
|
849
|
+
</FormControl>
|
|
850
|
+
<FormMessage />
|
|
851
|
+
</FormItem>
|
|
852
|
+
)}
|
|
853
|
+
/>
|
|
760
854
|
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
776
|
-
Number.
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
784
|
-
|
|
855
|
+
<FormField
|
|
856
|
+
control={form.control}
|
|
857
|
+
name="installmentsCount"
|
|
858
|
+
render={({ field }) => (
|
|
859
|
+
<FormItem>
|
|
860
|
+
<FormLabel>
|
|
861
|
+
{t('installmentsEditor.countLabel')}
|
|
862
|
+
</FormLabel>
|
|
863
|
+
<FormControl>
|
|
864
|
+
<Input
|
|
865
|
+
type="number"
|
|
866
|
+
min={1}
|
|
867
|
+
max={120}
|
|
868
|
+
value={field.value}
|
|
869
|
+
onChange={(event) => {
|
|
870
|
+
const nextValue = Number(event.target.value || 1);
|
|
871
|
+
field.onChange(
|
|
872
|
+
Number.isNaN(nextValue) ? 1 : nextValue
|
|
873
|
+
);
|
|
874
|
+
}}
|
|
875
|
+
/>
|
|
876
|
+
</FormControl>
|
|
877
|
+
<FormMessage />
|
|
878
|
+
</FormItem>
|
|
879
|
+
)}
|
|
880
|
+
/>
|
|
881
|
+
</div>
|
|
785
882
|
|
|
786
883
|
<div className="space-y-3 rounded-md border p-3">
|
|
787
884
|
<div className="flex items-center justify-between gap-2">
|
|
788
|
-
<p className="text-sm font-medium">
|
|
885
|
+
<p className="text-sm font-medium">
|
|
886
|
+
{t('installmentsEditor.title')}
|
|
887
|
+
</p>
|
|
789
888
|
<Button
|
|
790
889
|
type="button"
|
|
791
890
|
variant="outline"
|
|
@@ -801,7 +900,7 @@ function NovoTituloSheet({
|
|
|
801
900
|
);
|
|
802
901
|
}}
|
|
803
902
|
>
|
|
804
|
-
|
|
903
|
+
{t('installmentsEditor.recalculate')}
|
|
805
904
|
</Button>
|
|
806
905
|
</div>
|
|
807
906
|
|
|
@@ -817,13 +916,12 @@ function NovoTituloSheet({
|
|
|
817
916
|
htmlFor="auto-redistribute-installments-payable"
|
|
818
917
|
className="text-xs text-muted-foreground"
|
|
819
918
|
>
|
|
820
|
-
|
|
919
|
+
{t('installmentsEditor.autoRedistributeLabel')}
|
|
821
920
|
</Label>
|
|
822
921
|
</div>
|
|
823
922
|
{autoRedistributeInstallments && (
|
|
824
923
|
<p className="text-xs text-muted-foreground">
|
|
825
|
-
|
|
826
|
-
campo.
|
|
924
|
+
{t('installmentsEditor.autoRedistributeHint')}
|
|
827
925
|
</p>
|
|
828
926
|
)}
|
|
829
927
|
|
|
@@ -831,7 +929,7 @@ function NovoTituloSheet({
|
|
|
831
929
|
{installmentFields.map((installment, index) => (
|
|
832
930
|
<div
|
|
833
931
|
key={installment.id}
|
|
834
|
-
className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
932
|
+
className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
835
933
|
>
|
|
836
934
|
<div className="flex items-center text-sm text-muted-foreground">
|
|
837
935
|
#{index + 1}
|
|
@@ -843,7 +941,7 @@ function NovoTituloSheet({
|
|
|
843
941
|
render={({ field }) => (
|
|
844
942
|
<FormItem>
|
|
845
943
|
<FormLabel className="text-xs">
|
|
846
|
-
|
|
944
|
+
{t('installmentsEditor.dueDateLabel')}
|
|
847
945
|
</FormLabel>
|
|
848
946
|
<FormControl>
|
|
849
947
|
<Input
|
|
@@ -866,7 +964,9 @@ function NovoTituloSheet({
|
|
|
866
964
|
name={`installments.${index}.amount` as const}
|
|
867
965
|
render={({ field }) => (
|
|
868
966
|
<FormItem>
|
|
869
|
-
<FormLabel className="text-xs">
|
|
967
|
+
<FormLabel className="text-xs">
|
|
968
|
+
{t('installmentsEditor.amountLabel')}
|
|
969
|
+
</FormLabel>
|
|
870
970
|
<FormControl>
|
|
871
971
|
<InputMoney
|
|
872
972
|
ref={field.ref}
|
|
@@ -910,8 +1010,11 @@ function NovoTituloSheet({
|
|
|
910
1010
|
: 'text-destructive'
|
|
911
1011
|
}`}
|
|
912
1012
|
>
|
|
913
|
-
|
|
914
|
-
|
|
1013
|
+
{t('installmentsEditor.totalPrefix', {
|
|
1014
|
+
total: installmentsTotal.toFixed(2),
|
|
1015
|
+
})}
|
|
1016
|
+
{installmentsDiffCents > 0 &&
|
|
1017
|
+
` ${t('installmentsEditor.adjustmentNeeded')}`}
|
|
915
1018
|
</p>
|
|
916
1019
|
{form.formState.errors.installments?.message && (
|
|
917
1020
|
<p className="text-xs text-destructive">
|
|
@@ -920,56 +1023,23 @@ function NovoTituloSheet({
|
|
|
920
1023
|
)}
|
|
921
1024
|
</div>
|
|
922
1025
|
|
|
923
|
-
<
|
|
924
|
-
|
|
1026
|
+
<CategoryFieldWithCreate
|
|
1027
|
+
form={form}
|
|
925
1028
|
name="categoriaId"
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
<SelectTrigger className="w-full">
|
|
932
|
-
<SelectValue placeholder={t('common.select')} />
|
|
933
|
-
</SelectTrigger>
|
|
934
|
-
</FormControl>
|
|
935
|
-
<SelectContent>
|
|
936
|
-
{categorias
|
|
937
|
-
.filter((c) => c.natureza === 'despesa')
|
|
938
|
-
.map((c) => (
|
|
939
|
-
<SelectItem key={c.id} value={String(c.id)}>
|
|
940
|
-
{c.codigo} - {c.nome}
|
|
941
|
-
</SelectItem>
|
|
942
|
-
))}
|
|
943
|
-
</SelectContent>
|
|
944
|
-
</Select>
|
|
945
|
-
<FormMessage />
|
|
946
|
-
</FormItem>
|
|
947
|
-
)}
|
|
1029
|
+
label={t('fields.category')}
|
|
1030
|
+
selectPlaceholder={t('common.select')}
|
|
1031
|
+
categories={categorias}
|
|
1032
|
+
categoryKind="despesa"
|
|
1033
|
+
onCreated={onCategoriesUpdated}
|
|
948
1034
|
/>
|
|
949
1035
|
|
|
950
|
-
<
|
|
951
|
-
|
|
1036
|
+
<CostCenterFieldWithCreate
|
|
1037
|
+
form={form}
|
|
952
1038
|
name="centroCustoId"
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
|
|
956
|
-
|
|
957
|
-
<FormControl>
|
|
958
|
-
<SelectTrigger className="w-full">
|
|
959
|
-
<SelectValue placeholder={t('common.select')} />
|
|
960
|
-
</SelectTrigger>
|
|
961
|
-
</FormControl>
|
|
962
|
-
<SelectContent>
|
|
963
|
-
{centrosCusto.map((c) => (
|
|
964
|
-
<SelectItem key={c.id} value={String(c.id)}>
|
|
965
|
-
{c.codigo} - {c.nome}
|
|
966
|
-
</SelectItem>
|
|
967
|
-
))}
|
|
968
|
-
</SelectContent>
|
|
969
|
-
</Select>
|
|
970
|
-
<FormMessage />
|
|
971
|
-
</FormItem>
|
|
972
|
-
)}
|
|
1039
|
+
label={t('fields.costCenter')}
|
|
1040
|
+
selectPlaceholder={t('common.select')}
|
|
1041
|
+
costCenters={centrosCusto}
|
|
1042
|
+
onCreated={onCostCentersUpdated}
|
|
973
1043
|
/>
|
|
974
1044
|
|
|
975
1045
|
<FormField
|
|
@@ -1027,10 +1097,7 @@ function NovoTituloSheet({
|
|
|
1027
1097
|
/>
|
|
1028
1098
|
</div>
|
|
1029
1099
|
|
|
1030
|
-
<div className="flex
|
|
1031
|
-
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
1032
|
-
{t('common.cancel')}
|
|
1033
|
-
</Button>
|
|
1100
|
+
<div className="flex flex-col gap-2 py-4">
|
|
1034
1101
|
<Button
|
|
1035
1102
|
type="submit"
|
|
1036
1103
|
disabled={
|
|
@@ -1043,9 +1110,9 @@ function NovoTituloSheet({
|
|
|
1043
1110
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1044
1111
|
)}
|
|
1045
1112
|
{isExtractingFileData
|
|
1046
|
-
? '
|
|
1113
|
+
? t('common.upload.fillingWithAi')
|
|
1047
1114
|
: isUploadingFile
|
|
1048
|
-
? '
|
|
1115
|
+
? t('common.upload.uploadingFile')
|
|
1049
1116
|
: t('common.save')}
|
|
1050
1117
|
</Button>
|
|
1051
1118
|
</div>
|
|
@@ -1060,29 +1127,61 @@ function EditarTituloSheet({
|
|
|
1060
1127
|
open,
|
|
1061
1128
|
onOpenChange,
|
|
1062
1129
|
titulo,
|
|
1063
|
-
pessoas,
|
|
1064
1130
|
categorias,
|
|
1065
1131
|
centrosCusto,
|
|
1066
1132
|
t,
|
|
1067
1133
|
onUpdated,
|
|
1134
|
+
onCategoriesUpdated,
|
|
1135
|
+
onCostCentersUpdated,
|
|
1068
1136
|
}: {
|
|
1069
1137
|
open: boolean;
|
|
1070
1138
|
onOpenChange: (open: boolean) => void;
|
|
1071
1139
|
titulo?: any;
|
|
1072
|
-
pessoas: any[];
|
|
1073
1140
|
categorias: any[];
|
|
1074
1141
|
centrosCusto: any[];
|
|
1075
1142
|
t: ReturnType<typeof useTranslations>;
|
|
1076
1143
|
onUpdated: () => Promise<any> | void;
|
|
1144
|
+
onCategoriesUpdated?: () => Promise<any> | void;
|
|
1145
|
+
onCostCentersUpdated?: () => Promise<any> | void;
|
|
1077
1146
|
}) {
|
|
1078
1147
|
const { request, showToastHandler } = useApp();
|
|
1148
|
+
const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
|
|
1149
|
+
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
1150
|
+
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
1151
|
+
const [isExtractingFileData, setIsExtractingFileData] = useState(false);
|
|
1079
1152
|
const [isInstallmentsEdited, setIsInstallmentsEdited] = useState(false);
|
|
1080
1153
|
const [autoRedistributeInstallments, setAutoRedistributeInstallments] =
|
|
1081
1154
|
useState(true);
|
|
1155
|
+
const [extractionConfidence, setExtractionConfidence] = useState<
|
|
1156
|
+
number | null
|
|
1157
|
+
>(null);
|
|
1158
|
+
const [extractionWarnings, setExtractionWarnings] = useState<string[]>([]);
|
|
1159
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
1160
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
1082
1161
|
const redistributionTimeoutRef = useRef<
|
|
1083
1162
|
Record<number, ReturnType<typeof setTimeout>>
|
|
1084
1163
|
>({});
|
|
1085
1164
|
|
|
1165
|
+
const normalizeFilenameForDisplay = (filename: string) => {
|
|
1166
|
+
if (!filename) {
|
|
1167
|
+
return filename;
|
|
1168
|
+
}
|
|
1169
|
+
|
|
1170
|
+
if (!/Ã.|Â.|â[\u0080-\u00BF]/.test(filename)) {
|
|
1171
|
+
return filename;
|
|
1172
|
+
}
|
|
1173
|
+
|
|
1174
|
+
try {
|
|
1175
|
+
const bytes = Uint8Array.from(filename, (char) => char.charCodeAt(0));
|
|
1176
|
+
const decoded = new TextDecoder('utf-8').decode(bytes);
|
|
1177
|
+
return /Ã.|Â.|â[\u0080-\u00BF]/.test(decoded) ? filename : decoded;
|
|
1178
|
+
} catch {
|
|
1179
|
+
return filename;
|
|
1180
|
+
}
|
|
1181
|
+
};
|
|
1182
|
+
|
|
1183
|
+
const newTitleFormSchema = useMemo(() => getNewTitleFormSchema(t), [t]);
|
|
1184
|
+
|
|
1086
1185
|
const form = useForm<NewTitleFormValues>({
|
|
1087
1186
|
resolver: zodResolver(newTitleFormSchema),
|
|
1088
1187
|
defaultValues: {
|
|
@@ -1162,6 +1261,40 @@ function EditarTituloSheet({
|
|
|
1162
1261
|
descricao: titulo.descricao || '',
|
|
1163
1262
|
});
|
|
1164
1263
|
|
|
1264
|
+
const attachmentSource = Array.isArray(titulo.anexosDetalhes)
|
|
1265
|
+
? titulo.anexosDetalhes
|
|
1266
|
+
: Array.isArray(titulo.anexos)
|
|
1267
|
+
? titulo.anexos
|
|
1268
|
+
: [];
|
|
1269
|
+
const firstAttachment = attachmentSource[0] as any;
|
|
1270
|
+
const firstAttachmentIdRaw =
|
|
1271
|
+
firstAttachment && typeof firstAttachment === 'object'
|
|
1272
|
+
? (firstAttachment.id ??
|
|
1273
|
+
firstAttachment.file_id ??
|
|
1274
|
+
firstAttachment.fileId)
|
|
1275
|
+
: undefined;
|
|
1276
|
+
const parsedAttachmentId = Number(firstAttachmentIdRaw);
|
|
1277
|
+
|
|
1278
|
+
setUploadedFileId(
|
|
1279
|
+
Number.isFinite(parsedAttachmentId) ? parsedAttachmentId : null
|
|
1280
|
+
);
|
|
1281
|
+
setUploadedFileName(
|
|
1282
|
+
normalizeFilenameForDisplay(
|
|
1283
|
+
typeof firstAttachment === 'string'
|
|
1284
|
+
? firstAttachment
|
|
1285
|
+
: firstAttachment?.filename ||
|
|
1286
|
+
firstAttachment?.originalname ||
|
|
1287
|
+
firstAttachment?.name ||
|
|
1288
|
+
firstAttachment?.nome ||
|
|
1289
|
+
firstAttachment?.file_name ||
|
|
1290
|
+
firstAttachment?.fileName ||
|
|
1291
|
+
''
|
|
1292
|
+
)
|
|
1293
|
+
);
|
|
1294
|
+
setExtractionConfidence(null);
|
|
1295
|
+
setExtractionWarnings([]);
|
|
1296
|
+
setUploadProgress(0);
|
|
1297
|
+
|
|
1165
1298
|
setIsInstallmentsEdited(true);
|
|
1166
1299
|
}, [form, open, titulo]);
|
|
1167
1300
|
|
|
@@ -1252,7 +1385,7 @@ function EditarTituloSheet({
|
|
|
1252
1385
|
|
|
1253
1386
|
const handleSubmit = async (values: NewTitleFormValues) => {
|
|
1254
1387
|
if (!titulo?.id) {
|
|
1255
|
-
showToastHandler?.('error', '
|
|
1388
|
+
showToastHandler?.('error', t('messages.invalidTitleForEdit'));
|
|
1256
1389
|
return;
|
|
1257
1390
|
}
|
|
1258
1391
|
|
|
@@ -1281,69 +1414,345 @@ function EditarTituloSheet({
|
|
|
1281
1414
|
due_date: installment.dueDate || values.vencimento,
|
|
1282
1415
|
amount: installment.amount,
|
|
1283
1416
|
})),
|
|
1417
|
+
attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
|
|
1284
1418
|
},
|
|
1285
1419
|
});
|
|
1286
1420
|
|
|
1287
1421
|
await onUpdated();
|
|
1288
|
-
showToastHandler?.('success', '
|
|
1422
|
+
showToastHandler?.('success', t('messages.updateSuccess'));
|
|
1289
1423
|
onOpenChange(false);
|
|
1290
1424
|
} catch {
|
|
1291
|
-
showToastHandler?.('error', '
|
|
1425
|
+
showToastHandler?.('error', t('messages.updateError'));
|
|
1292
1426
|
}
|
|
1293
1427
|
};
|
|
1294
1428
|
|
|
1295
|
-
|
|
1296
|
-
|
|
1297
|
-
|
|
1298
|
-
|
|
1299
|
-
|
|
1300
|
-
|
|
1301
|
-
Edite os dados do título enquanto estiver em rascunho.
|
|
1302
|
-
</SheetDescription>
|
|
1303
|
-
</SheetHeader>
|
|
1304
|
-
<Form {...form}>
|
|
1305
|
-
<form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
|
|
1306
|
-
<div className="grid gap-4">
|
|
1307
|
-
<FormField
|
|
1308
|
-
control={form.control}
|
|
1309
|
-
name="documento"
|
|
1310
|
-
render={({ field }) => (
|
|
1311
|
-
<FormItem>
|
|
1312
|
-
<FormLabel>{t('fields.document')}</FormLabel>
|
|
1313
|
-
<FormControl>
|
|
1314
|
-
<Input placeholder="NF-00000" {...field} />
|
|
1315
|
-
</FormControl>
|
|
1316
|
-
<FormMessage />
|
|
1317
|
-
</FormItem>
|
|
1318
|
-
)}
|
|
1319
|
-
/>
|
|
1429
|
+
const clearUploadedFile = () => {
|
|
1430
|
+
setUploadedFileId(null);
|
|
1431
|
+
setUploadedFileName('');
|
|
1432
|
+
setExtractionConfidence(null);
|
|
1433
|
+
setExtractionWarnings([]);
|
|
1434
|
+
setUploadProgress(0);
|
|
1320
1435
|
|
|
1321
|
-
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
|
|
1336
|
-
|
|
1337
|
-
|
|
1338
|
-
|
|
1339
|
-
|
|
1340
|
-
|
|
1341
|
-
|
|
1342
|
-
|
|
1343
|
-
|
|
1436
|
+
if (fileInputRef.current) {
|
|
1437
|
+
fileInputRef.current.value = '';
|
|
1438
|
+
}
|
|
1439
|
+
};
|
|
1440
|
+
|
|
1441
|
+
const handleSelectFile = () => {
|
|
1442
|
+
if (fileInputRef.current) {
|
|
1443
|
+
fileInputRef.current.value = '';
|
|
1444
|
+
fileInputRef.current.click();
|
|
1445
|
+
}
|
|
1446
|
+
};
|
|
1447
|
+
|
|
1448
|
+
const uploadRelatedFile = async (file: File) => {
|
|
1449
|
+
setIsUploadingFile(true);
|
|
1450
|
+
setUploadProgress(0);
|
|
1451
|
+
|
|
1452
|
+
try {
|
|
1453
|
+
const formData = new FormData();
|
|
1454
|
+
formData.append('file', file);
|
|
1455
|
+
formData.append('destination', 'finance/titles');
|
|
1456
|
+
|
|
1457
|
+
const { data } = await request<{ id: number; filename: string }>({
|
|
1458
|
+
url: '/file',
|
|
1459
|
+
method: 'POST',
|
|
1460
|
+
data: formData,
|
|
1461
|
+
headers: {
|
|
1462
|
+
'Content-Type': 'multipart/form-data',
|
|
1463
|
+
},
|
|
1464
|
+
onUploadProgress: (event) => {
|
|
1465
|
+
if (!event.total) {
|
|
1466
|
+
return;
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
const progress = Math.round((event.loaded * 100) / event.total);
|
|
1470
|
+
setUploadProgress(progress);
|
|
1471
|
+
},
|
|
1472
|
+
});
|
|
1473
|
+
|
|
1474
|
+
if (!data?.id) {
|
|
1475
|
+
throw new Error(t('messages.invalidFile'));
|
|
1476
|
+
}
|
|
1477
|
+
|
|
1478
|
+
setUploadedFileId(data.id);
|
|
1479
|
+
setUploadedFileName(
|
|
1480
|
+
normalizeFilenameForDisplay(data.filename || file.name)
|
|
1481
|
+
);
|
|
1482
|
+
setUploadProgress(100);
|
|
1483
|
+
showToastHandler?.('success', t('messages.attachSuccess'));
|
|
1484
|
+
|
|
1485
|
+
setIsExtractingFileData(true);
|
|
1486
|
+
try {
|
|
1487
|
+
const extraction = await request<{
|
|
1488
|
+
documento?: string | null;
|
|
1489
|
+
fornecedorId?: string;
|
|
1490
|
+
competencia?: string;
|
|
1491
|
+
vencimento?: string;
|
|
1492
|
+
valor?: number | null;
|
|
1493
|
+
categoriaId?: string;
|
|
1494
|
+
centroCustoId?: string;
|
|
1495
|
+
metodo?: string;
|
|
1496
|
+
descricao?: string | null;
|
|
1497
|
+
confidence?: number | null;
|
|
1498
|
+
confidenceLevel?: 'low' | 'high' | null;
|
|
1499
|
+
warnings?: string[];
|
|
1500
|
+
}>({
|
|
1501
|
+
url: '/finance/accounts-payable/installments/extract-from-file',
|
|
1502
|
+
method: 'POST',
|
|
1503
|
+
data: {
|
|
1504
|
+
file_id: data.id,
|
|
1505
|
+
},
|
|
1506
|
+
});
|
|
1507
|
+
|
|
1508
|
+
const extracted = extraction.data || {};
|
|
1509
|
+
setExtractionConfidence(
|
|
1510
|
+
typeof extracted.confidence === 'number' ? extracted.confidence : null
|
|
1511
|
+
);
|
|
1512
|
+
setExtractionWarnings(
|
|
1513
|
+
Array.isArray(extracted.warnings)
|
|
1514
|
+
? extracted.warnings.filter(Boolean)
|
|
1515
|
+
: []
|
|
1516
|
+
);
|
|
1517
|
+
|
|
1518
|
+
if (extracted.documento) {
|
|
1519
|
+
form.setValue('documento', extracted.documento, {
|
|
1520
|
+
shouldValidate: true,
|
|
1521
|
+
});
|
|
1522
|
+
}
|
|
1523
|
+
|
|
1524
|
+
if (extracted.fornecedorId) {
|
|
1525
|
+
form.setValue('fornecedorId', extracted.fornecedorId, {
|
|
1526
|
+
shouldValidate: true,
|
|
1527
|
+
});
|
|
1528
|
+
}
|
|
1529
|
+
|
|
1530
|
+
if (extracted.competencia) {
|
|
1531
|
+
form.setValue('competencia', extracted.competencia, {
|
|
1532
|
+
shouldValidate: true,
|
|
1533
|
+
});
|
|
1534
|
+
}
|
|
1535
|
+
|
|
1536
|
+
if (extracted.vencimento) {
|
|
1537
|
+
form.setValue('vencimento', extracted.vencimento, {
|
|
1538
|
+
shouldValidate: true,
|
|
1539
|
+
});
|
|
1540
|
+
}
|
|
1541
|
+
|
|
1542
|
+
if (typeof extracted.valor === 'number' && extracted.valor > 0) {
|
|
1543
|
+
form.setValue('valor', extracted.valor, {
|
|
1544
|
+
shouldValidate: true,
|
|
1545
|
+
});
|
|
1546
|
+
}
|
|
1547
|
+
|
|
1548
|
+
if (extracted.categoriaId) {
|
|
1549
|
+
form.setValue('categoriaId', extracted.categoriaId, {
|
|
1550
|
+
shouldValidate: true,
|
|
1551
|
+
});
|
|
1552
|
+
}
|
|
1553
|
+
|
|
1554
|
+
if (extracted.centroCustoId) {
|
|
1555
|
+
form.setValue('centroCustoId', extracted.centroCustoId, {
|
|
1556
|
+
shouldValidate: true,
|
|
1557
|
+
});
|
|
1558
|
+
}
|
|
1559
|
+
|
|
1560
|
+
if (extracted.metodo) {
|
|
1561
|
+
form.setValue('metodo', extracted.metodo, {
|
|
1562
|
+
shouldValidate: true,
|
|
1563
|
+
});
|
|
1564
|
+
}
|
|
1565
|
+
|
|
1566
|
+
if (extracted.descricao) {
|
|
1567
|
+
form.setValue('descricao', extracted.descricao, {
|
|
1568
|
+
shouldValidate: true,
|
|
1569
|
+
});
|
|
1570
|
+
}
|
|
1571
|
+
|
|
1572
|
+
showToastHandler?.('success', t('messages.aiExtractSuccess'));
|
|
1573
|
+
} catch {
|
|
1574
|
+
setExtractionConfidence(null);
|
|
1575
|
+
setExtractionWarnings([]);
|
|
1576
|
+
showToastHandler?.('error', t('messages.aiExtractError'));
|
|
1577
|
+
} finally {
|
|
1578
|
+
setIsExtractingFileData(false);
|
|
1579
|
+
}
|
|
1580
|
+
} catch {
|
|
1581
|
+
setUploadedFileId(null);
|
|
1582
|
+
setUploadedFileName('');
|
|
1583
|
+
setExtractionConfidence(null);
|
|
1584
|
+
setExtractionWarnings([]);
|
|
1585
|
+
setUploadProgress(0);
|
|
1586
|
+
showToastHandler?.('error', t('messages.uploadError'));
|
|
1587
|
+
} finally {
|
|
1588
|
+
setIsUploadingFile(false);
|
|
1589
|
+
}
|
|
1590
|
+
};
|
|
1591
|
+
|
|
1592
|
+
return (
|
|
1593
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
1594
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
1595
|
+
<SheetHeader>
|
|
1596
|
+
<SheetTitle>{t('table.actions.edit')}</SheetTitle>
|
|
1597
|
+
<SheetDescription>{t('editTitle.description')}</SheetDescription>
|
|
1598
|
+
</SheetHeader>
|
|
1599
|
+
<Form {...form}>
|
|
1600
|
+
<form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
|
|
1601
|
+
<div className="grid gap-3">
|
|
1602
|
+
<div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
|
|
1603
|
+
<div className="grid gap-2">
|
|
1604
|
+
<FormLabel>{t('common.upload.label')}</FormLabel>
|
|
1605
|
+
<Input
|
|
1606
|
+
ref={fileInputRef}
|
|
1607
|
+
className="hidden"
|
|
1608
|
+
type="file"
|
|
1609
|
+
accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
|
|
1610
|
+
onChange={(event) => {
|
|
1611
|
+
const file = event.target.files?.[0];
|
|
1612
|
+
if (!file) {
|
|
1613
|
+
return;
|
|
1614
|
+
}
|
|
1615
|
+
|
|
1616
|
+
clearUploadedFile();
|
|
1617
|
+
void uploadRelatedFile(file);
|
|
1618
|
+
}}
|
|
1619
|
+
disabled={
|
|
1620
|
+
isUploadingFile ||
|
|
1621
|
+
isExtractingFileData ||
|
|
1622
|
+
form.formState.isSubmitting
|
|
1623
|
+
}
|
|
1624
|
+
/>
|
|
1625
|
+
|
|
1626
|
+
<div className="grid w-full grid-cols-2 gap-2">
|
|
1627
|
+
<Tooltip>
|
|
1628
|
+
<TooltipTrigger asChild>
|
|
1629
|
+
<Button
|
|
1630
|
+
type="button"
|
|
1631
|
+
variant="outline"
|
|
1632
|
+
className={
|
|
1633
|
+
uploadedFileId ? 'w-full' : 'col-span-2 w-full'
|
|
1634
|
+
}
|
|
1635
|
+
onClick={handleSelectFile}
|
|
1636
|
+
aria-label={
|
|
1637
|
+
uploadedFileId
|
|
1638
|
+
? t('common.upload.change')
|
|
1639
|
+
: t('common.upload.upload')
|
|
1640
|
+
}
|
|
1641
|
+
disabled={
|
|
1642
|
+
isUploadingFile ||
|
|
1643
|
+
isExtractingFileData ||
|
|
1644
|
+
form.formState.isSubmitting
|
|
1645
|
+
}
|
|
1646
|
+
>
|
|
1647
|
+
{uploadedFileId ? (
|
|
1648
|
+
<Upload className="h-4 w-4" />
|
|
1649
|
+
) : (
|
|
1650
|
+
<>
|
|
1651
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
1652
|
+
{t('common.upload.upload')}
|
|
1653
|
+
</>
|
|
1654
|
+
)}
|
|
1655
|
+
</Button>
|
|
1656
|
+
</TooltipTrigger>
|
|
1657
|
+
<TooltipContent>
|
|
1658
|
+
{uploadedFileId
|
|
1659
|
+
? t('common.upload.change')
|
|
1660
|
+
: t('common.upload.upload')}
|
|
1661
|
+
</TooltipContent>
|
|
1662
|
+
</Tooltip>
|
|
1663
|
+
|
|
1664
|
+
{uploadedFileId && (
|
|
1665
|
+
<Tooltip>
|
|
1666
|
+
<TooltipTrigger asChild>
|
|
1667
|
+
<Button
|
|
1668
|
+
type="button"
|
|
1669
|
+
variant="outline"
|
|
1670
|
+
className="w-full"
|
|
1671
|
+
onClick={clearUploadedFile}
|
|
1672
|
+
aria-label={t('common.upload.remove')}
|
|
1673
|
+
disabled={
|
|
1674
|
+
isUploadingFile ||
|
|
1675
|
+
isExtractingFileData ||
|
|
1676
|
+
form.formState.isSubmitting
|
|
1677
|
+
}
|
|
1678
|
+
>
|
|
1679
|
+
<Trash2 className="h-4 w-4" />
|
|
1680
|
+
</Button>
|
|
1681
|
+
</TooltipTrigger>
|
|
1682
|
+
<TooltipContent>
|
|
1683
|
+
{t('common.upload.remove')}
|
|
1684
|
+
</TooltipContent>
|
|
1685
|
+
</Tooltip>
|
|
1686
|
+
)}
|
|
1687
|
+
</div>
|
|
1688
|
+
|
|
1689
|
+
<div className="space-y-1">
|
|
1690
|
+
{(uploadedFileId || uploadedFileName) && (
|
|
1691
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1692
|
+
{t('common.upload.selectedPrefix')} {uploadedFileName}
|
|
1693
|
+
</p>
|
|
1694
|
+
)}
|
|
1695
|
+
|
|
1696
|
+
{isUploadingFile && !isExtractingFileData && (
|
|
1697
|
+
<div className="space-y-1">
|
|
1698
|
+
<Progress value={uploadProgress} className="h-2" />
|
|
1699
|
+
<p className="text-xs text-muted-foreground">
|
|
1700
|
+
{t('common.upload.uploadingProgress', {
|
|
1701
|
+
progress: uploadProgress,
|
|
1702
|
+
})}
|
|
1703
|
+
</p>
|
|
1704
|
+
</div>
|
|
1705
|
+
)}
|
|
1706
|
+
|
|
1707
|
+
{isExtractingFileData && (
|
|
1708
|
+
<p className="flex items-center gap-2 text-xs text-primary">
|
|
1709
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1710
|
+
{t('common.upload.processingAi')}
|
|
1711
|
+
</p>
|
|
1712
|
+
)}
|
|
1713
|
+
|
|
1714
|
+
{!isExtractingFileData &&
|
|
1715
|
+
extractionConfidence !== null &&
|
|
1716
|
+
extractionConfidence < 70 && (
|
|
1717
|
+
<p className="text-xs text-destructive">
|
|
1718
|
+
{t('common.upload.lowConfidence', {
|
|
1719
|
+
confidence: Math.round(extractionConfidence),
|
|
1720
|
+
})}
|
|
1721
|
+
</p>
|
|
1722
|
+
)}
|
|
1723
|
+
|
|
1724
|
+
{!isExtractingFileData && extractionWarnings.length > 0 && (
|
|
1725
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1726
|
+
{extractionWarnings[0]}
|
|
1727
|
+
</p>
|
|
1728
|
+
)}
|
|
1729
|
+
</div>
|
|
1730
|
+
</div>
|
|
1731
|
+
|
|
1732
|
+
<FormField
|
|
1733
|
+
control={form.control}
|
|
1734
|
+
name="documento"
|
|
1735
|
+
render={({ field }) => (
|
|
1736
|
+
<FormItem>
|
|
1737
|
+
<FormLabel>{t('fields.document')}</FormLabel>
|
|
1738
|
+
<FormControl>
|
|
1739
|
+
<Input placeholder="NF-00000" {...field} />
|
|
1740
|
+
</FormControl>
|
|
1741
|
+
<FormMessage />
|
|
1742
|
+
</FormItem>
|
|
1743
|
+
)}
|
|
1744
|
+
/>
|
|
1745
|
+
</div>
|
|
1746
|
+
|
|
1747
|
+
<PersonFieldWithCreate
|
|
1748
|
+
form={form}
|
|
1749
|
+
name="fornecedorId"
|
|
1750
|
+
label={t('fields.supplier')}
|
|
1751
|
+
entityLabel="fornecedor"
|
|
1752
|
+
selectPlaceholder={t('common.select')}
|
|
1344
1753
|
/>
|
|
1345
1754
|
|
|
1346
|
-
<div className="grid grid-cols-2 gap-
|
|
1755
|
+
<div className="grid grid-cols-2 items-start gap-3">
|
|
1347
1756
|
<FormField
|
|
1348
1757
|
control={form.control}
|
|
1349
1758
|
name="competencia"
|
|
@@ -1381,56 +1790,62 @@ function EditarTituloSheet({
|
|
|
1381
1790
|
/>
|
|
1382
1791
|
</div>
|
|
1383
1792
|
|
|
1384
|
-
<
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
<
|
|
1390
|
-
|
|
1391
|
-
<
|
|
1392
|
-
|
|
1393
|
-
|
|
1394
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
1401
|
-
|
|
1402
|
-
|
|
1403
|
-
|
|
1793
|
+
<div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
|
|
1794
|
+
<FormField
|
|
1795
|
+
control={form.control}
|
|
1796
|
+
name="valor"
|
|
1797
|
+
render={({ field }) => (
|
|
1798
|
+
<FormItem>
|
|
1799
|
+
<FormLabel>{t('fields.totalValue')}</FormLabel>
|
|
1800
|
+
<FormControl>
|
|
1801
|
+
<InputMoney
|
|
1802
|
+
ref={field.ref}
|
|
1803
|
+
name={field.name}
|
|
1804
|
+
value={field.value}
|
|
1805
|
+
onBlur={field.onBlur}
|
|
1806
|
+
onValueChange={(value) => field.onChange(value ?? 0)}
|
|
1807
|
+
placeholder="0,00"
|
|
1808
|
+
/>
|
|
1809
|
+
</FormControl>
|
|
1810
|
+
<FormMessage />
|
|
1811
|
+
</FormItem>
|
|
1812
|
+
)}
|
|
1813
|
+
/>
|
|
1404
1814
|
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
Number.
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
|
|
1425
|
-
|
|
1426
|
-
|
|
1427
|
-
|
|
1428
|
-
|
|
1429
|
-
|
|
1815
|
+
<FormField
|
|
1816
|
+
control={form.control}
|
|
1817
|
+
name="installmentsCount"
|
|
1818
|
+
render={({ field }) => (
|
|
1819
|
+
<FormItem>
|
|
1820
|
+
<FormLabel>
|
|
1821
|
+
{t('installmentsEditor.countLabel')}
|
|
1822
|
+
</FormLabel>
|
|
1823
|
+
<FormControl>
|
|
1824
|
+
<Input
|
|
1825
|
+
type="number"
|
|
1826
|
+
min={1}
|
|
1827
|
+
max={120}
|
|
1828
|
+
value={field.value}
|
|
1829
|
+
onChange={(event) => {
|
|
1830
|
+
const nextValue = Number(event.target.value || 1);
|
|
1831
|
+
field.onChange(
|
|
1832
|
+
Number.isNaN(nextValue) ? 1 : nextValue
|
|
1833
|
+
);
|
|
1834
|
+
setIsInstallmentsEdited(false);
|
|
1835
|
+
}}
|
|
1836
|
+
/>
|
|
1837
|
+
</FormControl>
|
|
1838
|
+
<FormMessage />
|
|
1839
|
+
</FormItem>
|
|
1840
|
+
)}
|
|
1841
|
+
/>
|
|
1842
|
+
</div>
|
|
1430
1843
|
|
|
1431
1844
|
<div className="space-y-3 rounded-md border p-3">
|
|
1432
1845
|
<div className="flex items-center justify-between gap-2">
|
|
1433
|
-
<p className="text-sm font-medium">
|
|
1846
|
+
<p className="text-sm font-medium">
|
|
1847
|
+
{t('installmentsEditor.title')}
|
|
1848
|
+
</p>
|
|
1434
1849
|
<Button
|
|
1435
1850
|
type="button"
|
|
1436
1851
|
variant="outline"
|
|
@@ -1446,7 +1861,7 @@ function EditarTituloSheet({
|
|
|
1446
1861
|
);
|
|
1447
1862
|
}}
|
|
1448
1863
|
>
|
|
1449
|
-
|
|
1864
|
+
{t('installmentsEditor.recalculate')}
|
|
1450
1865
|
</Button>
|
|
1451
1866
|
</div>
|
|
1452
1867
|
|
|
@@ -1462,7 +1877,7 @@ function EditarTituloSheet({
|
|
|
1462
1877
|
htmlFor="auto-redistribute-installments-edit-payable"
|
|
1463
1878
|
className="text-xs text-muted-foreground"
|
|
1464
1879
|
>
|
|
1465
|
-
|
|
1880
|
+
{t('installmentsEditor.autoRedistributeLabel')}
|
|
1466
1881
|
</Label>
|
|
1467
1882
|
</div>
|
|
1468
1883
|
|
|
@@ -1470,7 +1885,7 @@ function EditarTituloSheet({
|
|
|
1470
1885
|
{installmentFields.map((installment, index) => (
|
|
1471
1886
|
<div
|
|
1472
1887
|
key={installment.id}
|
|
1473
|
-
className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
1888
|
+
className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
1474
1889
|
>
|
|
1475
1890
|
<div className="flex items-center text-sm text-muted-foreground">
|
|
1476
1891
|
#{index + 1}
|
|
@@ -1482,7 +1897,7 @@ function EditarTituloSheet({
|
|
|
1482
1897
|
render={({ field }) => (
|
|
1483
1898
|
<FormItem>
|
|
1484
1899
|
<FormLabel className="text-xs">
|
|
1485
|
-
|
|
1900
|
+
{t('installmentsEditor.dueDateLabel')}
|
|
1486
1901
|
</FormLabel>
|
|
1487
1902
|
<FormControl>
|
|
1488
1903
|
<Input
|
|
@@ -1505,7 +1920,9 @@ function EditarTituloSheet({
|
|
|
1505
1920
|
name={`installments.${index}.amount` as const}
|
|
1506
1921
|
render={({ field }) => (
|
|
1507
1922
|
<FormItem>
|
|
1508
|
-
<FormLabel className="text-xs">
|
|
1923
|
+
<FormLabel className="text-xs">
|
|
1924
|
+
{t('installmentsEditor.amountLabel')}
|
|
1925
|
+
</FormLabel>
|
|
1509
1926
|
<FormControl>
|
|
1510
1927
|
<InputMoney
|
|
1511
1928
|
ref={field.ref}
|
|
@@ -1549,8 +1966,11 @@ function EditarTituloSheet({
|
|
|
1549
1966
|
: 'text-destructive'
|
|
1550
1967
|
}`}
|
|
1551
1968
|
>
|
|
1552
|
-
|
|
1553
|
-
|
|
1969
|
+
{t('installmentsEditor.totalPrefix', {
|
|
1970
|
+
total: installmentsTotal.toFixed(2),
|
|
1971
|
+
})}
|
|
1972
|
+
{installmentsDiffCents > 0 &&
|
|
1973
|
+
` ${t('installmentsEditor.adjustmentNeeded')}`}
|
|
1554
1974
|
</p>
|
|
1555
1975
|
{form.formState.errors.installments?.message && (
|
|
1556
1976
|
<p className="text-xs text-destructive">
|
|
@@ -1559,56 +1979,23 @@ function EditarTituloSheet({
|
|
|
1559
1979
|
)}
|
|
1560
1980
|
</div>
|
|
1561
1981
|
|
|
1562
|
-
<
|
|
1563
|
-
|
|
1982
|
+
<CategoryFieldWithCreate
|
|
1983
|
+
form={form}
|
|
1564
1984
|
name="categoriaId"
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
1569
|
-
|
|
1570
|
-
<SelectTrigger className="w-full">
|
|
1571
|
-
<SelectValue placeholder={t('common.select')} />
|
|
1572
|
-
</SelectTrigger>
|
|
1573
|
-
</FormControl>
|
|
1574
|
-
<SelectContent>
|
|
1575
|
-
{categorias
|
|
1576
|
-
.filter((c) => c.natureza === 'despesa')
|
|
1577
|
-
.map((c) => (
|
|
1578
|
-
<SelectItem key={c.id} value={String(c.id)}>
|
|
1579
|
-
{c.codigo} - {c.nome}
|
|
1580
|
-
</SelectItem>
|
|
1581
|
-
))}
|
|
1582
|
-
</SelectContent>
|
|
1583
|
-
</Select>
|
|
1584
|
-
<FormMessage />
|
|
1585
|
-
</FormItem>
|
|
1586
|
-
)}
|
|
1985
|
+
label={t('fields.category')}
|
|
1986
|
+
selectPlaceholder={t('common.select')}
|
|
1987
|
+
categories={categorias}
|
|
1988
|
+
categoryKind="despesa"
|
|
1989
|
+
onCreated={onCategoriesUpdated}
|
|
1587
1990
|
/>
|
|
1588
1991
|
|
|
1589
|
-
<
|
|
1590
|
-
|
|
1992
|
+
<CostCenterFieldWithCreate
|
|
1993
|
+
form={form}
|
|
1591
1994
|
name="centroCustoId"
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
|
|
1596
|
-
<FormControl>
|
|
1597
|
-
<SelectTrigger className="w-full">
|
|
1598
|
-
<SelectValue placeholder={t('common.select')} />
|
|
1599
|
-
</SelectTrigger>
|
|
1600
|
-
</FormControl>
|
|
1601
|
-
<SelectContent>
|
|
1602
|
-
{centrosCusto.map((c) => (
|
|
1603
|
-
<SelectItem key={c.id} value={String(c.id)}>
|
|
1604
|
-
{c.codigo} - {c.nome}
|
|
1605
|
-
</SelectItem>
|
|
1606
|
-
))}
|
|
1607
|
-
</SelectContent>
|
|
1608
|
-
</Select>
|
|
1609
|
-
<FormMessage />
|
|
1610
|
-
</FormItem>
|
|
1611
|
-
)}
|
|
1995
|
+
label={t('fields.costCenter')}
|
|
1996
|
+
selectPlaceholder={t('common.select')}
|
|
1997
|
+
costCenters={centrosCusto}
|
|
1998
|
+
onCreated={onCostCentersUpdated}
|
|
1612
1999
|
/>
|
|
1613
2000
|
|
|
1614
2001
|
<FormField
|
|
@@ -1666,16 +2053,23 @@ function EditarTituloSheet({
|
|
|
1666
2053
|
/>
|
|
1667
2054
|
</div>
|
|
1668
2055
|
|
|
1669
|
-
<div className="flex
|
|
2056
|
+
<div className="flex flex-col gap-2 py-4">
|
|
1670
2057
|
<Button
|
|
1671
|
-
type="
|
|
1672
|
-
|
|
1673
|
-
|
|
2058
|
+
type="submit"
|
|
2059
|
+
disabled={
|
|
2060
|
+
form.formState.isSubmitting ||
|
|
2061
|
+
isUploadingFile ||
|
|
2062
|
+
isExtractingFileData
|
|
2063
|
+
}
|
|
1674
2064
|
>
|
|
1675
|
-
{
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
{
|
|
2065
|
+
{(isUploadingFile || isExtractingFileData) && (
|
|
2066
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
2067
|
+
)}
|
|
2068
|
+
{isExtractingFileData
|
|
2069
|
+
? t('common.upload.fillingWithAi')
|
|
2070
|
+
: isUploadingFile
|
|
2071
|
+
? t('common.upload.uploadingFile')
|
|
2072
|
+
: t('common.save')}
|
|
1679
2073
|
</Button>
|
|
1680
2074
|
</div>
|
|
1681
2075
|
</form>
|
|
@@ -1691,10 +2085,10 @@ export default function TitulosPagarPage() {
|
|
|
1691
2085
|
const pathname = usePathname();
|
|
1692
2086
|
const router = useRouter();
|
|
1693
2087
|
const searchParams = useSearchParams();
|
|
1694
|
-
const { data, refetch } = useFinanceData();
|
|
1695
|
-
const { titulosPagar, pessoas } = data;
|
|
2088
|
+
const { data, refetch: refetchFinanceData } = useFinanceData();
|
|
2089
|
+
const { titulosPagar: allTitulosPagar, pessoas } = data;
|
|
1696
2090
|
|
|
1697
|
-
const { data: categoriasData } = useQuery<any[]>({
|
|
2091
|
+
const { data: categoriasData, refetch: refetchCategorias } = useQuery<any[]>({
|
|
1698
2092
|
queryKey: ['finance-categories-options', currentLocaleCode],
|
|
1699
2093
|
queryFn: async () => {
|
|
1700
2094
|
const response = await request({
|
|
@@ -1707,7 +2101,9 @@ export default function TitulosPagarPage() {
|
|
|
1707
2101
|
placeholderData: (old) => old,
|
|
1708
2102
|
});
|
|
1709
2103
|
|
|
1710
|
-
const { data: centrosCustoData } = useQuery<
|
|
2104
|
+
const { data: centrosCustoData, refetch: refetchCentrosCusto } = useQuery<
|
|
2105
|
+
any[]
|
|
2106
|
+
>({
|
|
1711
2107
|
queryKey: ['finance-cost-centers-options'],
|
|
1712
2108
|
queryFn: async () => {
|
|
1713
2109
|
const response = await request({
|
|
@@ -1728,14 +2124,107 @@ export default function TitulosPagarPage() {
|
|
|
1728
2124
|
|
|
1729
2125
|
const [search, setSearch] = useState('');
|
|
1730
2126
|
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
2127
|
+
const [page, setPage] = useState(1);
|
|
2128
|
+
const pageSize = 10;
|
|
1731
2129
|
const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
|
|
1732
2130
|
const [approvingTitleId, setApprovingTitleId] = useState<string | null>(null);
|
|
1733
2131
|
const [reversingTitleId, setReversingTitleId] = useState<string | null>(null);
|
|
1734
2132
|
const [cancelingTitleId, setCancelingTitleId] = useState<string | null>(null);
|
|
2133
|
+
const [titleToSettle, setTitleToSettle] = useState<any | null>(null);
|
|
2134
|
+
const [isSettleSheetOpen, setIsSettleSheetOpen] = useState(false);
|
|
2135
|
+
const [isSettlingTitle, setIsSettlingTitle] = useState(false);
|
|
2136
|
+
|
|
2137
|
+
const settleTitleFormSchema = useMemo(() => getSettleTitleFormSchema(t), [t]);
|
|
2138
|
+
|
|
2139
|
+
const settleTitleForm = useForm<SettleTitleFormValues>({
|
|
2140
|
+
resolver: zodResolver(settleTitleFormSchema),
|
|
2141
|
+
defaultValues: {
|
|
2142
|
+
installmentId: '',
|
|
2143
|
+
amount: 0,
|
|
2144
|
+
description: '',
|
|
2145
|
+
},
|
|
2146
|
+
});
|
|
2147
|
+
|
|
2148
|
+
const settleCandidates = useMemo(
|
|
2149
|
+
() =>
|
|
2150
|
+
(titleToSettle?.parcelas || []).filter(
|
|
2151
|
+
(installment: any) =>
|
|
2152
|
+
installment.status === 'aberto' ||
|
|
2153
|
+
installment.status === 'parcial' ||
|
|
2154
|
+
installment.status === 'vencido'
|
|
2155
|
+
),
|
|
2156
|
+
[titleToSettle]
|
|
2157
|
+
);
|
|
2158
|
+
|
|
2159
|
+
const normalizedStatusFilter =
|
|
2160
|
+
statusFilter && statusFilter !== 'all' ? statusFilter : undefined;
|
|
2161
|
+
|
|
2162
|
+
const {
|
|
2163
|
+
data: paginatedTitlesResponse,
|
|
2164
|
+
refetch: refetchTitles,
|
|
2165
|
+
isFetching: isFetchingTitles,
|
|
2166
|
+
} = useQuery<{
|
|
2167
|
+
data: any[];
|
|
2168
|
+
total: number;
|
|
2169
|
+
page: number;
|
|
2170
|
+
pageSize: number;
|
|
2171
|
+
prev: number | null;
|
|
2172
|
+
next: number | null;
|
|
2173
|
+
lastPage: number;
|
|
2174
|
+
}>({
|
|
2175
|
+
queryKey: [
|
|
2176
|
+
'finance-payable-installments-list',
|
|
2177
|
+
search,
|
|
2178
|
+
normalizedStatusFilter,
|
|
2179
|
+
page,
|
|
2180
|
+
pageSize,
|
|
2181
|
+
],
|
|
2182
|
+
queryFn: async () => {
|
|
2183
|
+
const response = await request({
|
|
2184
|
+
url: '/finance/accounts-payable/installments',
|
|
2185
|
+
method: 'GET',
|
|
2186
|
+
params: {
|
|
2187
|
+
page,
|
|
2188
|
+
pageSize,
|
|
2189
|
+
search: search.trim() || undefined,
|
|
2190
|
+
status: normalizedStatusFilter,
|
|
2191
|
+
},
|
|
2192
|
+
});
|
|
2193
|
+
|
|
2194
|
+
return response.data as {
|
|
2195
|
+
data: any[];
|
|
2196
|
+
total: number;
|
|
2197
|
+
page: number;
|
|
2198
|
+
pageSize: number;
|
|
2199
|
+
prev: number | null;
|
|
2200
|
+
next: number | null;
|
|
2201
|
+
lastPage: number;
|
|
2202
|
+
};
|
|
2203
|
+
},
|
|
2204
|
+
placeholderData: (old) => old,
|
|
2205
|
+
});
|
|
2206
|
+
|
|
2207
|
+
const titulosPagar = paginatedTitlesResponse?.data || [];
|
|
2208
|
+
|
|
2209
|
+
useEffect(() => {
|
|
2210
|
+
const firstCandidate = settleCandidates[0];
|
|
2211
|
+
|
|
2212
|
+
settleTitleForm.reset({
|
|
2213
|
+
installmentId: firstCandidate?.id || '',
|
|
2214
|
+
amount: Number(firstCandidate?.valorAberto || 0),
|
|
2215
|
+
description: '',
|
|
2216
|
+
});
|
|
2217
|
+
}, [settleCandidates, settleTitleForm]);
|
|
2218
|
+
|
|
2219
|
+
useEffect(() => {
|
|
2220
|
+
setPage(1);
|
|
2221
|
+
}, [search, normalizedStatusFilter]);
|
|
1735
2222
|
|
|
1736
2223
|
const editingTitle = useMemo(
|
|
1737
|
-
() =>
|
|
1738
|
-
|
|
2224
|
+
() =>
|
|
2225
|
+
titulosPagar.find((item) => item.id === editingTitleId) ||
|
|
2226
|
+
allTitulosPagar.find((item) => item.id === editingTitleId),
|
|
2227
|
+
[allTitulosPagar, editingTitleId, titulosPagar]
|
|
1739
2228
|
);
|
|
1740
2229
|
|
|
1741
2230
|
useEffect(() => {
|
|
@@ -1744,16 +2233,15 @@ export default function TitulosPagarPage() {
|
|
|
1744
2233
|
return;
|
|
1745
2234
|
}
|
|
1746
2235
|
|
|
1747
|
-
const foundTitle =
|
|
2236
|
+
const foundTitle =
|
|
2237
|
+
titulosPagar.find((item) => item.id === editId) ||
|
|
2238
|
+
allTitulosPagar.find((item) => item.id === editId);
|
|
1748
2239
|
if (!foundTitle) {
|
|
1749
2240
|
return;
|
|
1750
2241
|
}
|
|
1751
2242
|
|
|
1752
2243
|
if (foundTitle.status !== 'rascunho') {
|
|
1753
|
-
showToastHandler?.(
|
|
1754
|
-
'error',
|
|
1755
|
-
'Apenas títulos em rascunho podem ser editados'
|
|
1756
|
-
);
|
|
2244
|
+
showToastHandler?.('error', t('messages.editDraftOnly'));
|
|
1757
2245
|
router.replace(pathname, { scroll: false });
|
|
1758
2246
|
return;
|
|
1759
2247
|
}
|
|
@@ -1765,6 +2253,7 @@ export default function TitulosPagarPage() {
|
|
|
1765
2253
|
router,
|
|
1766
2254
|
searchParams,
|
|
1767
2255
|
showToastHandler,
|
|
2256
|
+
allTitulosPagar,
|
|
1768
2257
|
titulosPagar,
|
|
1769
2258
|
]);
|
|
1770
2259
|
|
|
@@ -1792,15 +2281,15 @@ export default function TitulosPagarPage() {
|
|
|
1792
2281
|
data: {},
|
|
1793
2282
|
});
|
|
1794
2283
|
|
|
1795
|
-
await
|
|
1796
|
-
showToastHandler?.('success', '
|
|
2284
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2285
|
+
showToastHandler?.('success', t('messages.cancelSuccess'));
|
|
1797
2286
|
} catch (error: any) {
|
|
1798
2287
|
const message = error?.response?.data?.message;
|
|
1799
2288
|
showToastHandler?.(
|
|
1800
2289
|
'error',
|
|
1801
2290
|
typeof message === 'string' && message.trim()
|
|
1802
2291
|
? message
|
|
1803
|
-
: '
|
|
2292
|
+
: t('messages.cancelError')
|
|
1804
2293
|
);
|
|
1805
2294
|
} finally {
|
|
1806
2295
|
setCancelingTitleId(null);
|
|
@@ -1819,84 +2308,123 @@ export default function TitulosPagarPage() {
|
|
|
1819
2308
|
method: 'PATCH',
|
|
1820
2309
|
});
|
|
1821
2310
|
|
|
1822
|
-
await
|
|
1823
|
-
showToastHandler?.('success', '
|
|
2311
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2312
|
+
showToastHandler?.('success', t('messages.approveSuccess'));
|
|
1824
2313
|
} catch (error: any) {
|
|
1825
2314
|
const message = error?.response?.data?.message;
|
|
1826
2315
|
showToastHandler?.(
|
|
1827
2316
|
'error',
|
|
1828
2317
|
typeof message === 'string' && message.trim()
|
|
1829
2318
|
? message
|
|
1830
|
-
: '
|
|
2319
|
+
: t('messages.approveError')
|
|
1831
2320
|
);
|
|
1832
2321
|
} finally {
|
|
1833
2322
|
setApprovingTitleId(null);
|
|
1834
2323
|
}
|
|
1835
2324
|
};
|
|
1836
2325
|
|
|
1837
|
-
const handleSettleTitle = (
|
|
1838
|
-
|
|
1839
|
-
|
|
2326
|
+
const handleSettleTitle = (title: any) => {
|
|
2327
|
+
const titleId = title?.id;
|
|
2328
|
+
|
|
2329
|
+
if (!titleId) {
|
|
2330
|
+
return;
|
|
2331
|
+
}
|
|
2332
|
+
|
|
2333
|
+
const availableInstallments = (title?.parcelas || []).filter(
|
|
2334
|
+
(installment: any) =>
|
|
2335
|
+
installment.status === 'aberto' ||
|
|
2336
|
+
installment.status === 'parcial' ||
|
|
2337
|
+
installment.status === 'vencido'
|
|
1840
2338
|
);
|
|
2339
|
+
|
|
2340
|
+
if (availableInstallments.length === 0) {
|
|
2341
|
+
showToastHandler?.('error', t('messages.noInstallmentForSettle'));
|
|
2342
|
+
return;
|
|
2343
|
+
}
|
|
2344
|
+
|
|
2345
|
+
setTitleToSettle(title);
|
|
2346
|
+
setIsSettleSheetOpen(true);
|
|
2347
|
+
};
|
|
2348
|
+
|
|
2349
|
+
const handleSubmitSettleTitle = async (values: SettleTitleFormValues) => {
|
|
2350
|
+
if (!titleToSettle?.id || isSettlingTitle) {
|
|
2351
|
+
return;
|
|
2352
|
+
}
|
|
2353
|
+
|
|
2354
|
+
setIsSettlingTitle(true);
|
|
2355
|
+
try {
|
|
2356
|
+
await request({
|
|
2357
|
+
url: `/finance/accounts-payable/installments/${titleToSettle.id}/settlements`,
|
|
2358
|
+
method: 'POST',
|
|
2359
|
+
data: {
|
|
2360
|
+
installment_id: Number(values.installmentId),
|
|
2361
|
+
amount: values.amount,
|
|
2362
|
+
description: values.description?.trim() || undefined,
|
|
2363
|
+
},
|
|
2364
|
+
});
|
|
2365
|
+
|
|
2366
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2367
|
+
setIsSettleSheetOpen(false);
|
|
2368
|
+
setTitleToSettle(null);
|
|
2369
|
+
settleTitleForm.reset({
|
|
2370
|
+
installmentId: '',
|
|
2371
|
+
amount: 0,
|
|
2372
|
+
description: '',
|
|
2373
|
+
});
|
|
2374
|
+
showToastHandler?.('success', t('messages.settleSuccess'));
|
|
2375
|
+
} catch (error: any) {
|
|
2376
|
+
const message = error?.response?.data?.message;
|
|
2377
|
+
showToastHandler?.(
|
|
2378
|
+
'error',
|
|
2379
|
+
typeof message === 'string' && message.trim()
|
|
2380
|
+
? message
|
|
2381
|
+
: t('messages.settleError')
|
|
2382
|
+
);
|
|
2383
|
+
} finally {
|
|
2384
|
+
setIsSettlingTitle(false);
|
|
2385
|
+
}
|
|
1841
2386
|
};
|
|
1842
2387
|
|
|
1843
|
-
const handleReverseTitle = async (title: any) => {
|
|
2388
|
+
const handleReverseTitle = async (title: any, reason?: string) => {
|
|
1844
2389
|
const titleId = title?.id;
|
|
1845
2390
|
|
|
1846
2391
|
if (!titleId || reversingTitleId) {
|
|
1847
2392
|
return;
|
|
1848
2393
|
}
|
|
1849
2394
|
|
|
1850
|
-
const
|
|
1851
|
-
.flatMap((parcela: any) => parcela?.liquidacoes || [])
|
|
1852
|
-
.find((liquidacao: any) => {
|
|
1853
|
-
return (
|
|
1854
|
-
!!liquidacao?.settlementId &&
|
|
1855
|
-
liquidacao?.status !== 'reversed' &&
|
|
1856
|
-
liquidacao?.status !== 'estornado'
|
|
1857
|
-
);
|
|
1858
|
-
});
|
|
2395
|
+
const settlementId = getFirstActiveSettlementId(title);
|
|
1859
2396
|
|
|
1860
|
-
if (!
|
|
1861
|
-
showToastHandler?.('error', '
|
|
2397
|
+
if (!settlementId) {
|
|
2398
|
+
showToastHandler?.('error', t('messages.noActiveSettlementToReverse'));
|
|
1862
2399
|
return;
|
|
1863
2400
|
}
|
|
1864
2401
|
|
|
1865
2402
|
setReversingTitleId(titleId);
|
|
1866
2403
|
try {
|
|
1867
2404
|
await request({
|
|
1868
|
-
url: `/finance/
|
|
1869
|
-
method: '
|
|
1870
|
-
data: {
|
|
2405
|
+
url: `/finance/settlements/${settlementId}/reverse`,
|
|
2406
|
+
method: 'POST',
|
|
2407
|
+
data: {
|
|
2408
|
+
reason: reason?.trim() || t('messages.reverseDefaultReason'),
|
|
2409
|
+
memo: reason?.trim() || t('messages.reverseDefaultReason'),
|
|
2410
|
+
},
|
|
1871
2411
|
});
|
|
1872
2412
|
|
|
1873
|
-
await
|
|
1874
|
-
showToastHandler?.('success', '
|
|
2413
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2414
|
+
showToastHandler?.('success', t('messages.reverseSuccess'));
|
|
1875
2415
|
} catch (error: any) {
|
|
1876
2416
|
const message = error?.response?.data?.message;
|
|
1877
2417
|
showToastHandler?.(
|
|
1878
2418
|
'error',
|
|
1879
2419
|
typeof message === 'string' && message.trim()
|
|
1880
2420
|
? message
|
|
1881
|
-
: '
|
|
2421
|
+
: t('messages.reverseError')
|
|
1882
2422
|
);
|
|
1883
2423
|
} finally {
|
|
1884
2424
|
setReversingTitleId(null);
|
|
1885
2425
|
}
|
|
1886
2426
|
};
|
|
1887
2427
|
|
|
1888
|
-
const filteredTitulos = titulosPagar.filter((titulo) => {
|
|
1889
|
-
const matchesSearch =
|
|
1890
|
-
titulo.documento.toLowerCase().includes(search.toLowerCase()) ||
|
|
1891
|
-
getPessoaById(titulo.fornecedorId)
|
|
1892
|
-
?.nome.toLowerCase()
|
|
1893
|
-
.includes(search.toLowerCase());
|
|
1894
|
-
|
|
1895
|
-
const matchesStatus = !statusFilter || titulo.status === statusFilter;
|
|
1896
|
-
|
|
1897
|
-
return matchesSearch && matchesStatus;
|
|
1898
|
-
});
|
|
1899
|
-
|
|
1900
2428
|
const handleOpenAttachment = async (fileId?: string) => {
|
|
1901
2429
|
if (!fileId) {
|
|
1902
2430
|
return;
|
|
@@ -1910,13 +2438,13 @@ export default function TitulosPagarPage() {
|
|
|
1910
2438
|
|
|
1911
2439
|
const url = response?.data?.url;
|
|
1912
2440
|
if (!url) {
|
|
1913
|
-
showToastHandler?.('error', '
|
|
2441
|
+
showToastHandler?.('error', t('messages.openAttachmentError'));
|
|
1914
2442
|
return;
|
|
1915
2443
|
}
|
|
1916
2444
|
|
|
1917
2445
|
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1918
2446
|
} catch {
|
|
1919
|
-
showToastHandler?.('error', '
|
|
2447
|
+
showToastHandler?.('error', t('messages.openAttachmentError'));
|
|
1920
2448
|
}
|
|
1921
2449
|
};
|
|
1922
2450
|
|
|
@@ -1936,17 +2464,24 @@ export default function TitulosPagarPage() {
|
|
|
1936
2464
|
categorias={categorias}
|
|
1937
2465
|
centrosCusto={centrosCusto}
|
|
1938
2466
|
t={t}
|
|
1939
|
-
onCreated={
|
|
2467
|
+
onCreated={async () => {
|
|
2468
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2469
|
+
}}
|
|
2470
|
+
onCategoriesUpdated={refetchCategorias}
|
|
2471
|
+
onCostCentersUpdated={refetchCentrosCusto}
|
|
1940
2472
|
/>
|
|
1941
2473
|
<EditarTituloSheet
|
|
1942
2474
|
open={!!editingTitleId && !!editingTitle}
|
|
1943
2475
|
onOpenChange={closeEditSheet}
|
|
1944
2476
|
titulo={editingTitle}
|
|
1945
|
-
pessoas={pessoas}
|
|
1946
2477
|
categorias={categorias}
|
|
1947
2478
|
centrosCusto={centrosCusto}
|
|
1948
2479
|
t={t}
|
|
1949
|
-
onUpdated={
|
|
2480
|
+
onUpdated={async () => {
|
|
2481
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2482
|
+
}}
|
|
2483
|
+
onCategoriesUpdated={refetchCategorias}
|
|
2484
|
+
onCostCentersUpdated={refetchCentrosCusto}
|
|
1950
2485
|
/>
|
|
1951
2486
|
</>
|
|
1952
2487
|
}
|
|
@@ -1973,10 +2508,135 @@ export default function TitulosPagarPage() {
|
|
|
1973
2508
|
],
|
|
1974
2509
|
},
|
|
1975
2510
|
]}
|
|
1976
|
-
activeFilters={
|
|
1977
|
-
onClearFilters={() => setStatusFilter('')}
|
|
2511
|
+
activeFilters={normalizedStatusFilter ? 1 : 0}
|
|
2512
|
+
onClearFilters={() => setStatusFilter('all')}
|
|
1978
2513
|
/>
|
|
1979
2514
|
|
|
2515
|
+
<Sheet
|
|
2516
|
+
open={isSettleSheetOpen}
|
|
2517
|
+
onOpenChange={(open) => {
|
|
2518
|
+
setIsSettleSheetOpen(open);
|
|
2519
|
+
|
|
2520
|
+
if (!open) {
|
|
2521
|
+
setTitleToSettle(null);
|
|
2522
|
+
}
|
|
2523
|
+
}}
|
|
2524
|
+
>
|
|
2525
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
2526
|
+
<SheetHeader>
|
|
2527
|
+
<SheetTitle>{t('settleSheet.title')}</SheetTitle>
|
|
2528
|
+
<SheetDescription>
|
|
2529
|
+
{t('settleSheet.description', {
|
|
2530
|
+
document: titleToSettle?.documento || '-',
|
|
2531
|
+
})}
|
|
2532
|
+
</SheetDescription>
|
|
2533
|
+
</SheetHeader>
|
|
2534
|
+
|
|
2535
|
+
<Form {...settleTitleForm}>
|
|
2536
|
+
<form
|
|
2537
|
+
className="space-y-4 px-4"
|
|
2538
|
+
onSubmit={settleTitleForm.handleSubmit(handleSubmitSettleTitle)}
|
|
2539
|
+
>
|
|
2540
|
+
<FormField
|
|
2541
|
+
control={settleTitleForm.control}
|
|
2542
|
+
name="installmentId"
|
|
2543
|
+
render={({ field }) => (
|
|
2544
|
+
<FormItem>
|
|
2545
|
+
<FormLabel>{t('settleSheet.installmentLabel')}</FormLabel>
|
|
2546
|
+
<Select
|
|
2547
|
+
value={field.value}
|
|
2548
|
+
onValueChange={(value) => {
|
|
2549
|
+
field.onChange(value);
|
|
2550
|
+
|
|
2551
|
+
const selected = settleCandidates.find(
|
|
2552
|
+
(installment: any) => installment.id === value
|
|
2553
|
+
);
|
|
2554
|
+
|
|
2555
|
+
if (selected) {
|
|
2556
|
+
settleTitleForm.setValue(
|
|
2557
|
+
'amount',
|
|
2558
|
+
Number(selected.valorAberto || 0),
|
|
2559
|
+
{ shouldValidate: true }
|
|
2560
|
+
);
|
|
2561
|
+
}
|
|
2562
|
+
}}
|
|
2563
|
+
>
|
|
2564
|
+
<FormControl>
|
|
2565
|
+
<SelectTrigger className="w-full">
|
|
2566
|
+
<SelectValue
|
|
2567
|
+
placeholder={t(
|
|
2568
|
+
'settleSheet.installmentPlaceholder'
|
|
2569
|
+
)}
|
|
2570
|
+
/>
|
|
2571
|
+
</SelectTrigger>
|
|
2572
|
+
</FormControl>
|
|
2573
|
+
<SelectContent>
|
|
2574
|
+
{settleCandidates.map((installment: any) => (
|
|
2575
|
+
<SelectItem
|
|
2576
|
+
key={installment.id}
|
|
2577
|
+
value={installment.id}
|
|
2578
|
+
>
|
|
2579
|
+
{t('settleSheet.installmentOption', {
|
|
2580
|
+
number: installment.numero,
|
|
2581
|
+
amount: new Intl.NumberFormat('pt-BR', {
|
|
2582
|
+
style: 'currency',
|
|
2583
|
+
currency: 'BRL',
|
|
2584
|
+
}).format(Number(installment.valorAberto || 0)),
|
|
2585
|
+
})}
|
|
2586
|
+
</SelectItem>
|
|
2587
|
+
))}
|
|
2588
|
+
</SelectContent>
|
|
2589
|
+
</Select>
|
|
2590
|
+
<FormMessage />
|
|
2591
|
+
</FormItem>
|
|
2592
|
+
)}
|
|
2593
|
+
/>
|
|
2594
|
+
|
|
2595
|
+
<FormField
|
|
2596
|
+
control={settleTitleForm.control}
|
|
2597
|
+
name="amount"
|
|
2598
|
+
render={({ field }) => (
|
|
2599
|
+
<FormItem>
|
|
2600
|
+
<FormLabel>{t('settleSheet.amountLabel')}</FormLabel>
|
|
2601
|
+
<FormControl>
|
|
2602
|
+
<InputMoney
|
|
2603
|
+
value={Number(field.value || 0)}
|
|
2604
|
+
onValueChange={(value) => {
|
|
2605
|
+
field.onChange(Number(value || 0));
|
|
2606
|
+
}}
|
|
2607
|
+
/>
|
|
2608
|
+
</FormControl>
|
|
2609
|
+
<FormMessage />
|
|
2610
|
+
</FormItem>
|
|
2611
|
+
)}
|
|
2612
|
+
/>
|
|
2613
|
+
|
|
2614
|
+
<FormField
|
|
2615
|
+
control={settleTitleForm.control}
|
|
2616
|
+
name="description"
|
|
2617
|
+
render={({ field }) => (
|
|
2618
|
+
<FormItem>
|
|
2619
|
+
<FormLabel>{t('settleSheet.descriptionLabel')}</FormLabel>
|
|
2620
|
+
<FormControl>
|
|
2621
|
+
<Input {...field} value={field.value || ''} />
|
|
2622
|
+
</FormControl>
|
|
2623
|
+
<FormMessage />
|
|
2624
|
+
</FormItem>
|
|
2625
|
+
)}
|
|
2626
|
+
/>
|
|
2627
|
+
|
|
2628
|
+
<Button
|
|
2629
|
+
className="w-full"
|
|
2630
|
+
type="submit"
|
|
2631
|
+
disabled={isSettlingTitle}
|
|
2632
|
+
>
|
|
2633
|
+
{t('settleSheet.confirm')}
|
|
2634
|
+
</Button>
|
|
2635
|
+
</form>
|
|
2636
|
+
</Form>
|
|
2637
|
+
</SheetContent>
|
|
2638
|
+
</Sheet>
|
|
2639
|
+
|
|
1980
2640
|
<div className="rounded-md border">
|
|
1981
2641
|
<Table>
|
|
1982
2642
|
<TableHeader>
|
|
@@ -1994,7 +2654,7 @@ export default function TitulosPagarPage() {
|
|
|
1994
2654
|
</TableRow>
|
|
1995
2655
|
</TableHeader>
|
|
1996
2656
|
<TableBody>
|
|
1997
|
-
{
|
|
2657
|
+
{titulosPagar.map((titulo) => {
|
|
1998
2658
|
const fornecedor = getPessoaById(titulo.fornecedorId);
|
|
1999
2659
|
const categoria = getCategoriaById(titulo.categoriaId);
|
|
2000
2660
|
const proximaParcela = titulo.parcelas.find(
|
|
@@ -2023,7 +2683,7 @@ export default function TitulosPagarPage() {
|
|
|
2023
2683
|
titulo.anexosDetalhes?.[0]?.id;
|
|
2024
2684
|
void handleOpenAttachment(firstAttachmentId);
|
|
2025
2685
|
}}
|
|
2026
|
-
aria-label=
|
|
2686
|
+
aria-label={t('table.actions.openAttachment')}
|
|
2027
2687
|
>
|
|
2028
2688
|
<Paperclip className="h-3 w-3" />
|
|
2029
2689
|
</Button>
|
|
@@ -2044,78 +2704,52 @@ export default function TitulosPagarPage() {
|
|
|
2044
2704
|
<StatusBadge status={titulo.status} />
|
|
2045
2705
|
</TableCell>
|
|
2046
2706
|
<TableCell>
|
|
2047
|
-
<
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2064
|
-
|
|
2065
|
-
|
|
2066
|
-
|
|
2067
|
-
|
|
2068
|
-
|
|
2069
|
-
|
|
2070
|
-
|
|
2071
|
-
|
|
2072
|
-
|
|
2073
|
-
|
|
2074
|
-
|
|
2075
|
-
|
|
2076
|
-
|
|
2077
|
-
|
|
2078
|
-
|
|
2079
|
-
|
|
2080
|
-
|
|
2081
|
-
|
|
2082
|
-
|
|
2083
|
-
|
|
2084
|
-
|
|
2085
|
-
|
|
2086
|
-
|
|
2087
|
-
|
|
2088
|
-
|
|
2089
|
-
|
|
2090
|
-
|
|
2091
|
-
|
|
2092
|
-
|
|
2093
|
-
</DropdownMenuItem>
|
|
2094
|
-
<DropdownMenuItem
|
|
2095
|
-
disabled={
|
|
2096
|
-
!['parcial', 'liquidado'].includes(titulo.status) ||
|
|
2097
|
-
reversingTitleId === titulo.id
|
|
2098
|
-
}
|
|
2099
|
-
onClick={() => void handleReverseTitle(titulo)}
|
|
2100
|
-
>
|
|
2101
|
-
<Undo className="mr-2 h-4 w-4" />
|
|
2102
|
-
{t('table.actions.reverse')}
|
|
2103
|
-
</DropdownMenuItem>
|
|
2104
|
-
<DropdownMenuSeparator />
|
|
2105
|
-
<DropdownMenuItem
|
|
2106
|
-
className="text-destructive"
|
|
2107
|
-
disabled={
|
|
2108
|
-
['cancelado', 'liquidado'].includes(
|
|
2109
|
-
titulo.status
|
|
2110
|
-
) || cancelingTitleId === titulo.id
|
|
2111
|
-
}
|
|
2112
|
-
onClick={() => void handleCancelTitle(titulo.id)}
|
|
2113
|
-
>
|
|
2114
|
-
<XCircle className="mr-2 h-4 w-4" />
|
|
2115
|
-
{t('table.actions.cancel')}
|
|
2116
|
-
</DropdownMenuItem>
|
|
2117
|
-
</DropdownMenuContent>
|
|
2118
|
-
</DropdownMenu>
|
|
2707
|
+
<FinanceTitleActionsMenu
|
|
2708
|
+
triggerVariant="ghost"
|
|
2709
|
+
detailHref={`/finance/accounts-payable/installments/${titulo.id}`}
|
|
2710
|
+
canEdit={canEditTitle(titulo.status)}
|
|
2711
|
+
canApprove={canApproveTitle(titulo.status)}
|
|
2712
|
+
canSettle={canSettleTitle(titulo.status)}
|
|
2713
|
+
canReverse={
|
|
2714
|
+
canReverseTitle(titulo.status) &&
|
|
2715
|
+
!!getFirstActiveSettlementId(titulo)
|
|
2716
|
+
}
|
|
2717
|
+
canCancel={canCancelTitle(titulo.status)}
|
|
2718
|
+
isApproving={approvingTitleId === titulo.id}
|
|
2719
|
+
isReversing={reversingTitleId === titulo.id}
|
|
2720
|
+
isCanceling={cancelingTitleId === titulo.id}
|
|
2721
|
+
labels={{
|
|
2722
|
+
menu: t.has('actions.title')
|
|
2723
|
+
? t('actions.title')
|
|
2724
|
+
: t('table.actions.srActions'),
|
|
2725
|
+
srActions: t('table.actions.srActions'),
|
|
2726
|
+
viewDetails: t('table.actions.viewDetails'),
|
|
2727
|
+
edit: t('table.actions.edit'),
|
|
2728
|
+
approve: t('table.actions.approve'),
|
|
2729
|
+
settle: t('table.actions.settle'),
|
|
2730
|
+
reverse: t('table.actions.reverse'),
|
|
2731
|
+
cancel: t('table.actions.cancel'),
|
|
2732
|
+
}}
|
|
2733
|
+
dialogs={{
|
|
2734
|
+
cancelTitle: t('dialogs.cancel.title'),
|
|
2735
|
+
cancelDescription: t('dialogs.cancel.description'),
|
|
2736
|
+
cancelButton: t('dialogs.cancel.cancel'),
|
|
2737
|
+
confirmCancelButton: t('dialogs.cancel.confirm'),
|
|
2738
|
+
reverseTitle: t('dialogs.reverse.title'),
|
|
2739
|
+
reverseDescription: t('dialogs.reverse.description'),
|
|
2740
|
+
reverseReasonLabel: t('dialogs.reverse.reasonLabel'),
|
|
2741
|
+
reverseReasonPlaceholder: t(
|
|
2742
|
+
'dialogs.reverse.reasonPlaceholder'
|
|
2743
|
+
),
|
|
2744
|
+
reverseButton: t('dialogs.reverse.cancel'),
|
|
2745
|
+
confirmReverseButton: t('dialogs.reverse.confirm'),
|
|
2746
|
+
}}
|
|
2747
|
+
onEdit={() => setEditingTitleId(titulo.id)}
|
|
2748
|
+
onApprove={() => void handleApproveTitle(titulo.id)}
|
|
2749
|
+
onSettle={() => handleSettleTitle(titulo)}
|
|
2750
|
+
onReverse={(reason) => handleReverseTitle(titulo, reason)}
|
|
2751
|
+
onCancel={() => handleCancelTitle(titulo.id)}
|
|
2752
|
+
/>
|
|
2119
2753
|
</TableCell>
|
|
2120
2754
|
</TableRow>
|
|
2121
2755
|
);
|
|
@@ -2127,15 +2761,25 @@ export default function TitulosPagarPage() {
|
|
|
2127
2761
|
<div className="flex items-center justify-between">
|
|
2128
2762
|
<p className="text-sm text-muted-foreground">
|
|
2129
2763
|
{t('footer.showing', {
|
|
2130
|
-
filtered:
|
|
2131
|
-
total:
|
|
2764
|
+
filtered: titulosPagar.length,
|
|
2765
|
+
total: paginatedTitlesResponse?.total || 0,
|
|
2132
2766
|
})}
|
|
2133
2767
|
</p>
|
|
2134
2768
|
<div className="flex items-center gap-2">
|
|
2135
|
-
<Button
|
|
2769
|
+
<Button
|
|
2770
|
+
variant="outline"
|
|
2771
|
+
size="sm"
|
|
2772
|
+
disabled={!paginatedTitlesResponse?.prev || isFetchingTitles}
|
|
2773
|
+
onClick={() => setPage((current) => Math.max(1, current - 1))}
|
|
2774
|
+
>
|
|
2136
2775
|
{t('footer.previous')}
|
|
2137
2776
|
</Button>
|
|
2138
|
-
<Button
|
|
2777
|
+
<Button
|
|
2778
|
+
variant="outline"
|
|
2779
|
+
size="sm"
|
|
2780
|
+
disabled={!paginatedTitlesResponse?.next || isFetchingTitles}
|
|
2781
|
+
onClick={() => setPage((current) => current + 1)}
|
|
2782
|
+
>
|
|
2139
2783
|
{t('footer.next')}
|
|
2140
2784
|
</Button>
|
|
2141
2785
|
</div>
|