@hed-hog/finance 0.0.252 → 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/dto/reverse-settlement.dto.d.ts +1 -0
- package/dist/dto/reverse-settlement.dto.d.ts.map +1 -1
- package/dist/dto/reverse-settlement.dto.js +5 -0
- package/dist/dto/reverse-settlement.dto.js.map +1 -1
- package/dist/finance-installments.controller.d.ts +106 -4
- package/dist/finance-installments.controller.d.ts.map +1 -1
- package/dist/finance-installments.controller.js +38 -2
- package/dist/finance-installments.controller.js.map +1 -1
- package/dist/finance.service.d.ts +104 -2
- package/dist/finance.service.d.ts.map +1 -1
- package/dist/finance.service.js +366 -121
- package/dist/finance.service.js.map +1 -1
- package/hedhog/data/route.yaml +27 -0
- 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 +449 -293
- 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/hedhog/query/settlement-auditability.sql +175 -0
- package/hedhog/table/bank_reconciliation.yaml +11 -0
- package/hedhog/table/settlement.yaml +17 -1
- package/hedhog/table/settlement_allocation.yaml +3 -0
- package/package.json +7 -7
- package/src/dto/reverse-settlement.dto.ts +4 -0
- package/src/finance-installments.controller.ts +45 -12
- package/src/finance.service.ts +521 -146
|
@@ -1,5 +1,9 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
|
+
import {
|
|
4
|
+
CategoryFieldWithCreate,
|
|
5
|
+
CostCenterFieldWithCreate,
|
|
6
|
+
} from '@/app/(app)/(libraries)/finance/_components/finance-entity-field-with-create';
|
|
3
7
|
import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/finance/_components/person-field-with-create';
|
|
4
8
|
import { Page, PageHeader } from '@/components/entity-list';
|
|
5
9
|
import { Badge } from '@/components/ui/badge';
|
|
@@ -51,7 +55,12 @@ import {
|
|
|
51
55
|
TableRow,
|
|
52
56
|
} from '@/components/ui/table';
|
|
53
57
|
import { Textarea } from '@/components/ui/textarea';
|
|
54
|
-
import {
|
|
58
|
+
import {
|
|
59
|
+
Tooltip,
|
|
60
|
+
TooltipContent,
|
|
61
|
+
TooltipTrigger,
|
|
62
|
+
} from '@/components/ui/tooltip';
|
|
63
|
+
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
55
64
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
56
65
|
import {
|
|
57
66
|
Download,
|
|
@@ -62,10 +71,13 @@ import {
|
|
|
62
71
|
Paperclip,
|
|
63
72
|
Plus,
|
|
64
73
|
Send,
|
|
74
|
+
Trash2,
|
|
75
|
+
Upload,
|
|
65
76
|
} from 'lucide-react';
|
|
66
77
|
import { useTranslations } from 'next-intl';
|
|
67
78
|
import Link from 'next/link';
|
|
68
|
-
import {
|
|
79
|
+
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
80
|
+
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
69
81
|
import { useFieldArray, useForm } from 'react-hook-form';
|
|
70
82
|
import { z } from 'zod';
|
|
71
83
|
import { formatarData } from '../../_lib/formatters';
|
|
@@ -168,69 +180,74 @@ const redistributeRemainingInstallments = (
|
|
|
168
180
|
});
|
|
169
181
|
};
|
|
170
182
|
|
|
171
|
-
const
|
|
172
|
-
|
|
173
|
-
|
|
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
|
-
|
|
183
|
+
const getNewTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
|
|
184
|
+
z
|
|
185
|
+
.object({
|
|
186
|
+
documento: z.string().trim().min(1, t('validation.documentRequired')),
|
|
187
|
+
clienteId: z.string().min(1, t('validation.clientRequired')),
|
|
188
|
+
competencia: z.string().optional(),
|
|
189
|
+
vencimento: z.string().min(1, t('validation.dueDateRequired')),
|
|
190
|
+
valor: z.number().min(0.01, t('validation.amountGreaterThanZero')),
|
|
191
|
+
installmentsCount: z.coerce
|
|
192
|
+
.number({ invalid_type_error: t('validation.invalidInstallmentCount') })
|
|
193
|
+
.int(t('validation.invalidInstallmentCount'))
|
|
194
|
+
.min(1, t('validation.installmentsMin'))
|
|
195
|
+
.max(120, t('validation.installmentsMax')),
|
|
196
|
+
installments: z
|
|
197
|
+
.array(
|
|
198
|
+
z.object({
|
|
199
|
+
dueDate: z
|
|
200
|
+
.string()
|
|
201
|
+
.min(1, t('validation.installmentDueDateRequired')),
|
|
202
|
+
amount: z
|
|
203
|
+
.number()
|
|
204
|
+
.min(0.01, t('validation.installmentAmountGreaterThanZero')),
|
|
205
|
+
})
|
|
206
|
+
)
|
|
207
|
+
.min(1, t('validation.installmentsRequired')),
|
|
208
|
+
categoriaId: z.string().optional(),
|
|
209
|
+
centroCustoId: z.string().optional(),
|
|
210
|
+
canal: z.string().optional(),
|
|
211
|
+
descricao: z.string().optional(),
|
|
212
|
+
})
|
|
213
|
+
.superRefine((values, ctx) => {
|
|
214
|
+
if (values.installments.length !== values.installmentsCount) {
|
|
215
|
+
ctx.addIssue({
|
|
216
|
+
code: z.ZodIssueCode.custom,
|
|
217
|
+
path: ['installments'],
|
|
218
|
+
message: t('validation.installmentsCountMismatch'),
|
|
219
|
+
});
|
|
220
|
+
}
|
|
206
221
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
222
|
+
const installmentsTotalCents = values.installments.reduce(
|
|
223
|
+
(acc, installment) => acc + Math.round((installment.amount || 0) * 100),
|
|
224
|
+
0
|
|
225
|
+
);
|
|
226
|
+
const totalCents = Math.round((values.valor || 0) * 100);
|
|
212
227
|
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
228
|
+
if (installmentsTotalCents !== totalCents) {
|
|
229
|
+
ctx.addIssue({
|
|
230
|
+
code: z.ZodIssueCode.custom,
|
|
231
|
+
path: ['installments'],
|
|
232
|
+
message: t('validation.installmentsSumMismatch'),
|
|
233
|
+
});
|
|
234
|
+
}
|
|
235
|
+
});
|
|
221
236
|
|
|
222
|
-
type NewTitleFormValues = z.infer<typeof
|
|
237
|
+
type NewTitleFormValues = z.infer<ReturnType<typeof getNewTitleFormSchema>>;
|
|
223
238
|
|
|
224
239
|
function NovoTituloSheet({
|
|
225
240
|
categorias,
|
|
226
241
|
centrosCusto,
|
|
227
242
|
t,
|
|
228
243
|
onCreated,
|
|
244
|
+
onOptionsUpdated,
|
|
229
245
|
}: {
|
|
230
246
|
categorias: any[];
|
|
231
247
|
centrosCusto: any[];
|
|
232
248
|
t: ReturnType<typeof useTranslations>;
|
|
233
249
|
onCreated: () => Promise<any> | void;
|
|
250
|
+
onOptionsUpdated?: () => Promise<any> | void;
|
|
234
251
|
}) {
|
|
235
252
|
const { request, showToastHandler } = useApp();
|
|
236
253
|
const [open, setOpen] = useState(false);
|
|
@@ -249,6 +266,7 @@ function NovoTituloSheet({
|
|
|
249
266
|
>(null);
|
|
250
267
|
const [extractionWarnings, setExtractionWarnings] = useState<string[]>([]);
|
|
251
268
|
const [uploadProgress, setUploadProgress] = useState(0);
|
|
269
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
252
270
|
|
|
253
271
|
const normalizeFilenameForDisplay = (filename: string) => {
|
|
254
272
|
if (!filename) {
|
|
@@ -268,6 +286,8 @@ function NovoTituloSheet({
|
|
|
268
286
|
}
|
|
269
287
|
};
|
|
270
288
|
|
|
289
|
+
const newTitleFormSchema = useMemo(() => getNewTitleFormSchema(t), [t]);
|
|
290
|
+
|
|
271
291
|
const form = useForm<NewTitleFormValues>({
|
|
272
292
|
resolver: zodResolver(newTitleFormSchema),
|
|
273
293
|
defaultValues: {
|
|
@@ -419,9 +439,9 @@ function NovoTituloSheet({
|
|
|
419
439
|
setIsInstallmentsEdited(false);
|
|
420
440
|
setAutoRedistributeInstallments(true);
|
|
421
441
|
setOpen(false);
|
|
422
|
-
showToastHandler?.('success', '
|
|
442
|
+
showToastHandler?.('success', t('messages.createSuccess'));
|
|
423
443
|
} catch {
|
|
424
|
-
showToastHandler?.('error', '
|
|
444
|
+
showToastHandler?.('error', t('messages.createError'));
|
|
425
445
|
}
|
|
426
446
|
};
|
|
427
447
|
|
|
@@ -437,6 +457,25 @@ function NovoTituloSheet({
|
|
|
437
457
|
setOpen(false);
|
|
438
458
|
};
|
|
439
459
|
|
|
460
|
+
const clearUploadedFile = () => {
|
|
461
|
+
setUploadedFileId(null);
|
|
462
|
+
setUploadedFileName('');
|
|
463
|
+
setExtractionConfidence(null);
|
|
464
|
+
setExtractionWarnings([]);
|
|
465
|
+
setUploadProgress(0);
|
|
466
|
+
|
|
467
|
+
if (fileInputRef.current) {
|
|
468
|
+
fileInputRef.current.value = '';
|
|
469
|
+
}
|
|
470
|
+
};
|
|
471
|
+
|
|
472
|
+
const handleSelectFile = () => {
|
|
473
|
+
if (fileInputRef.current) {
|
|
474
|
+
fileInputRef.current.value = '';
|
|
475
|
+
fileInputRef.current.click();
|
|
476
|
+
}
|
|
477
|
+
};
|
|
478
|
+
|
|
440
479
|
const uploadRelatedFile = async (file: File) => {
|
|
441
480
|
setIsUploadingFile(true);
|
|
442
481
|
setUploadProgress(0);
|
|
@@ -464,7 +503,7 @@ function NovoTituloSheet({
|
|
|
464
503
|
});
|
|
465
504
|
|
|
466
505
|
if (!data?.id) {
|
|
467
|
-
throw new Error('
|
|
506
|
+
throw new Error(t('messages.invalidFile'));
|
|
468
507
|
}
|
|
469
508
|
|
|
470
509
|
setUploadedFileId(data.id);
|
|
@@ -472,7 +511,7 @@ function NovoTituloSheet({
|
|
|
472
511
|
normalizeFilenameForDisplay(data.filename || file.name)
|
|
473
512
|
);
|
|
474
513
|
setUploadProgress(100);
|
|
475
|
-
showToastHandler?.('success', '
|
|
514
|
+
showToastHandler?.('success', t('messages.attachSuccess'));
|
|
476
515
|
|
|
477
516
|
setIsExtractingFileData(true);
|
|
478
517
|
try {
|
|
@@ -561,17 +600,11 @@ function NovoTituloSheet({
|
|
|
561
600
|
});
|
|
562
601
|
}
|
|
563
602
|
|
|
564
|
-
showToastHandler?.(
|
|
565
|
-
'success',
|
|
566
|
-
'Dados da fatura extraídos e preenchidos automaticamente'
|
|
567
|
-
);
|
|
603
|
+
showToastHandler?.('success', t('messages.aiExtractSuccess'));
|
|
568
604
|
} catch {
|
|
569
605
|
setExtractionConfidence(null);
|
|
570
606
|
setExtractionWarnings([]);
|
|
571
|
-
showToastHandler?.(
|
|
572
|
-
'error',
|
|
573
|
-
'Não foi possível extrair os dados automaticamente'
|
|
574
|
-
);
|
|
607
|
+
showToastHandler?.('error', t('messages.aiExtractError'));
|
|
575
608
|
} finally {
|
|
576
609
|
setIsExtractingFileData(false);
|
|
577
610
|
}
|
|
@@ -581,7 +614,7 @@ function NovoTituloSheet({
|
|
|
581
614
|
setExtractionConfidence(null);
|
|
582
615
|
setExtractionWarnings([]);
|
|
583
616
|
setUploadProgress(0);
|
|
584
|
-
showToastHandler?.('error', '
|
|
617
|
+
showToastHandler?.('error', t('messages.uploadError'));
|
|
585
618
|
} finally {
|
|
586
619
|
setIsUploadingFile(false);
|
|
587
620
|
}
|
|
@@ -602,11 +635,13 @@ function NovoTituloSheet({
|
|
|
602
635
|
</SheetHeader>
|
|
603
636
|
<Form {...form}>
|
|
604
637
|
<form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
|
|
605
|
-
<div className="grid gap-
|
|
606
|
-
<div className="grid gap-2">
|
|
607
|
-
<
|
|
608
|
-
|
|
638
|
+
<div className="grid gap-3">
|
|
639
|
+
<div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
|
|
640
|
+
<div className="grid gap-2">
|
|
641
|
+
<FormLabel>{t('common.upload.label')}</FormLabel>
|
|
609
642
|
<Input
|
|
643
|
+
ref={fileInputRef}
|
|
644
|
+
className="hidden"
|
|
610
645
|
type="file"
|
|
611
646
|
accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
|
|
612
647
|
onChange={(event) => {
|
|
@@ -615,11 +650,7 @@ function NovoTituloSheet({
|
|
|
615
650
|
return;
|
|
616
651
|
}
|
|
617
652
|
|
|
618
|
-
|
|
619
|
-
setUploadedFileName('');
|
|
620
|
-
setExtractionConfidence(null);
|
|
621
|
-
setExtractionWarnings([]);
|
|
622
|
-
setUploadProgress(0);
|
|
653
|
+
clearUploadedFile();
|
|
623
654
|
void uploadRelatedFile(file);
|
|
624
655
|
}}
|
|
625
656
|
disabled={
|
|
@@ -628,67 +659,128 @@ function NovoTituloSheet({
|
|
|
628
659
|
form.formState.isSubmitting
|
|
629
660
|
}
|
|
630
661
|
/>
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
642
|
-
|
|
643
|
-
|
|
644
|
-
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
662
|
+
|
|
663
|
+
<div className="grid w-full grid-cols-2 gap-2">
|
|
664
|
+
<Tooltip>
|
|
665
|
+
<TooltipTrigger asChild>
|
|
666
|
+
<Button
|
|
667
|
+
type="button"
|
|
668
|
+
variant="outline"
|
|
669
|
+
className={
|
|
670
|
+
uploadedFileId ? 'w-full' : 'col-span-2 w-full'
|
|
671
|
+
}
|
|
672
|
+
onClick={handleSelectFile}
|
|
673
|
+
aria-label={
|
|
674
|
+
uploadedFileId
|
|
675
|
+
? t('common.upload.change')
|
|
676
|
+
: t('common.upload.upload')
|
|
677
|
+
}
|
|
678
|
+
disabled={
|
|
679
|
+
isUploadingFile ||
|
|
680
|
+
isExtractingFileData ||
|
|
681
|
+
form.formState.isSubmitting
|
|
682
|
+
}
|
|
683
|
+
>
|
|
684
|
+
{uploadedFileId ? (
|
|
685
|
+
<Upload className="h-4 w-4" />
|
|
686
|
+
) : (
|
|
687
|
+
<>
|
|
688
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
689
|
+
{t('common.upload.upload')}
|
|
690
|
+
</>
|
|
691
|
+
)}
|
|
692
|
+
</Button>
|
|
693
|
+
</TooltipTrigger>
|
|
694
|
+
<TooltipContent>
|
|
695
|
+
{uploadedFileId
|
|
696
|
+
? t('common.upload.change')
|
|
697
|
+
: t('common.upload.upload')}
|
|
698
|
+
</TooltipContent>
|
|
699
|
+
</Tooltip>
|
|
700
|
+
|
|
701
|
+
{uploadedFileId && (
|
|
702
|
+
<Tooltip>
|
|
703
|
+
<TooltipTrigger asChild>
|
|
704
|
+
<Button
|
|
705
|
+
type="button"
|
|
706
|
+
variant="outline"
|
|
707
|
+
className="w-full"
|
|
708
|
+
onClick={clearUploadedFile}
|
|
709
|
+
aria-label={t('common.upload.remove')}
|
|
710
|
+
disabled={
|
|
711
|
+
isUploadingFile ||
|
|
712
|
+
isExtractingFileData ||
|
|
713
|
+
form.formState.isSubmitting
|
|
714
|
+
}
|
|
715
|
+
>
|
|
716
|
+
<Trash2 className="h-4 w-4" />
|
|
717
|
+
</Button>
|
|
718
|
+
</TooltipTrigger>
|
|
719
|
+
<TooltipContent>
|
|
720
|
+
{t('common.upload.remove')}
|
|
721
|
+
</TooltipContent>
|
|
722
|
+
</Tooltip>
|
|
723
|
+
)}
|
|
655
724
|
</div>
|
|
656
|
-
|
|
657
|
-
|
|
658
|
-
|
|
659
|
-
|
|
660
|
-
|
|
661
|
-
|
|
662
|
-
|
|
663
|
-
|
|
725
|
+
|
|
726
|
+
<div className="space-y-1">
|
|
727
|
+
{uploadedFileId && (
|
|
728
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
729
|
+
{t('common.upload.selectedPrefix')} {uploadedFileName}
|
|
730
|
+
</p>
|
|
731
|
+
)}
|
|
732
|
+
|
|
733
|
+
{isUploadingFile && !isExtractingFileData && (
|
|
734
|
+
<div className="space-y-1">
|
|
735
|
+
<Progress value={uploadProgress} className="h-2" />
|
|
736
|
+
<p className="text-xs text-muted-foreground">
|
|
737
|
+
{t('common.upload.uploadingProgress', {
|
|
738
|
+
progress: uploadProgress,
|
|
739
|
+
})}
|
|
740
|
+
</p>
|
|
664
741
|
</div>
|
|
665
|
-
|
|
666
|
-
|
|
667
|
-
|
|
742
|
+
)}
|
|
743
|
+
|
|
744
|
+
{isExtractingFileData && (
|
|
745
|
+
<p className="flex items-center gap-2 text-xs text-primary">
|
|
746
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
747
|
+
{t('common.upload.processingAi')}
|
|
668
748
|
</p>
|
|
669
|
-
|
|
670
|
-
|
|
671
|
-
|
|
749
|
+
)}
|
|
750
|
+
|
|
751
|
+
{!isExtractingFileData &&
|
|
752
|
+
extractionConfidence !== null &&
|
|
753
|
+
extractionConfidence < 70 && (
|
|
754
|
+
<p className="text-xs text-destructive">
|
|
755
|
+
{t('common.upload.lowConfidence', {
|
|
756
|
+
confidence: Math.round(extractionConfidence),
|
|
757
|
+
})}
|
|
672
758
|
</p>
|
|
673
759
|
)}
|
|
674
|
-
|
|
760
|
+
|
|
761
|
+
{!isExtractingFileData && extractionWarnings.length > 0 && (
|
|
762
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
763
|
+
{extractionWarnings[0]}
|
|
764
|
+
</p>
|
|
765
|
+
)}
|
|
766
|
+
</div>
|
|
767
|
+
</div>
|
|
768
|
+
|
|
769
|
+
<FormField
|
|
770
|
+
control={form.control}
|
|
771
|
+
name="documento"
|
|
772
|
+
render={({ field }) => (
|
|
773
|
+
<FormItem>
|
|
774
|
+
<FormLabel>{t('fields.document')}</FormLabel>
|
|
775
|
+
<FormControl>
|
|
776
|
+
<Input placeholder="FAT-00000" {...field} />
|
|
777
|
+
</FormControl>
|
|
778
|
+
<FormMessage />
|
|
779
|
+
</FormItem>
|
|
675
780
|
)}
|
|
781
|
+
/>
|
|
676
782
|
</div>
|
|
677
783
|
|
|
678
|
-
<FormField
|
|
679
|
-
control={form.control}
|
|
680
|
-
name="documento"
|
|
681
|
-
render={({ field }) => (
|
|
682
|
-
<FormItem>
|
|
683
|
-
<FormLabel>{t('fields.document')}</FormLabel>
|
|
684
|
-
<FormControl>
|
|
685
|
-
<Input placeholder="FAT-00000" {...field} />
|
|
686
|
-
</FormControl>
|
|
687
|
-
<FormMessage />
|
|
688
|
-
</FormItem>
|
|
689
|
-
)}
|
|
690
|
-
/>
|
|
691
|
-
|
|
692
784
|
<PersonFieldWithCreate
|
|
693
785
|
form={form}
|
|
694
786
|
name="clienteId"
|
|
@@ -697,7 +789,7 @@ function NovoTituloSheet({
|
|
|
697
789
|
selectPlaceholder={t('common.select')}
|
|
698
790
|
/>
|
|
699
791
|
|
|
700
|
-
<div className="grid grid-cols-2 gap-
|
|
792
|
+
<div className="grid grid-cols-2 items-start gap-3">
|
|
701
793
|
<FormField
|
|
702
794
|
control={form.control}
|
|
703
795
|
name="competencia"
|
|
@@ -735,55 +827,61 @@ function NovoTituloSheet({
|
|
|
735
827
|
/>
|
|
736
828
|
</div>
|
|
737
829
|
|
|
738
|
-
<
|
|
739
|
-
|
|
740
|
-
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
<
|
|
744
|
-
|
|
745
|
-
<
|
|
746
|
-
|
|
747
|
-
|
|
748
|
-
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
757
|
-
|
|
830
|
+
<div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
|
|
831
|
+
<FormField
|
|
832
|
+
control={form.control}
|
|
833
|
+
name="valor"
|
|
834
|
+
render={({ field }) => (
|
|
835
|
+
<FormItem>
|
|
836
|
+
<FormLabel>{t('fields.totalValue')}</FormLabel>
|
|
837
|
+
<FormControl>
|
|
838
|
+
<InputMoney
|
|
839
|
+
ref={field.ref}
|
|
840
|
+
name={field.name}
|
|
841
|
+
value={field.value}
|
|
842
|
+
onBlur={field.onBlur}
|
|
843
|
+
onValueChange={(value) => field.onChange(value ?? 0)}
|
|
844
|
+
placeholder="0,00"
|
|
845
|
+
/>
|
|
846
|
+
</FormControl>
|
|
847
|
+
<FormMessage />
|
|
848
|
+
</FormItem>
|
|
849
|
+
)}
|
|
850
|
+
/>
|
|
758
851
|
|
|
759
|
-
|
|
760
|
-
|
|
761
|
-
|
|
762
|
-
|
|
763
|
-
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
Number.
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
852
|
+
<FormField
|
|
853
|
+
control={form.control}
|
|
854
|
+
name="installmentsCount"
|
|
855
|
+
render={({ field }) => (
|
|
856
|
+
<FormItem>
|
|
857
|
+
<FormLabel>
|
|
858
|
+
{t('installmentsEditor.countLabel')}
|
|
859
|
+
</FormLabel>
|
|
860
|
+
<FormControl>
|
|
861
|
+
<Input
|
|
862
|
+
type="number"
|
|
863
|
+
min={1}
|
|
864
|
+
max={120}
|
|
865
|
+
value={field.value}
|
|
866
|
+
onChange={(event) => {
|
|
867
|
+
const nextValue = Number(event.target.value || 1);
|
|
868
|
+
field.onChange(
|
|
869
|
+
Number.isNaN(nextValue) ? 1 : nextValue
|
|
870
|
+
);
|
|
871
|
+
}}
|
|
872
|
+
/>
|
|
873
|
+
</FormControl>
|
|
874
|
+
<FormMessage />
|
|
875
|
+
</FormItem>
|
|
876
|
+
)}
|
|
877
|
+
/>
|
|
878
|
+
</div>
|
|
783
879
|
|
|
784
880
|
<div className="space-y-3 rounded-md border p-3">
|
|
785
881
|
<div className="flex items-center justify-between gap-2">
|
|
786
|
-
<p className="text-sm font-medium">
|
|
882
|
+
<p className="text-sm font-medium">
|
|
883
|
+
{t('installmentsEditor.title')}
|
|
884
|
+
</p>
|
|
787
885
|
<Button
|
|
788
886
|
type="button"
|
|
789
887
|
variant="outline"
|
|
@@ -799,7 +897,7 @@ function NovoTituloSheet({
|
|
|
799
897
|
);
|
|
800
898
|
}}
|
|
801
899
|
>
|
|
802
|
-
|
|
900
|
+
{t('installmentsEditor.recalculate')}
|
|
803
901
|
</Button>
|
|
804
902
|
</div>
|
|
805
903
|
|
|
@@ -815,7 +913,7 @@ function NovoTituloSheet({
|
|
|
815
913
|
htmlFor="auto-redistribute-installments-receivable"
|
|
816
914
|
className="text-xs text-muted-foreground"
|
|
817
915
|
>
|
|
818
|
-
|
|
916
|
+
{t('installmentsEditor.autoRedistributeLabel')}
|
|
819
917
|
</Label>
|
|
820
918
|
</div>
|
|
821
919
|
{autoRedistributeInstallments && (
|
|
@@ -829,7 +927,7 @@ function NovoTituloSheet({
|
|
|
829
927
|
{installmentFields.map((installment, index) => (
|
|
830
928
|
<div
|
|
831
929
|
key={installment.id}
|
|
832
|
-
className="grid grid-cols-1 gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
930
|
+
className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
833
931
|
>
|
|
834
932
|
<div className="flex items-center text-sm text-muted-foreground">
|
|
835
933
|
#{index + 1}
|
|
@@ -841,7 +939,7 @@ function NovoTituloSheet({
|
|
|
841
939
|
render={({ field }) => (
|
|
842
940
|
<FormItem>
|
|
843
941
|
<FormLabel className="text-xs">
|
|
844
|
-
|
|
942
|
+
{t('installmentsEditor.dueDateLabel')}
|
|
845
943
|
</FormLabel>
|
|
846
944
|
<FormControl>
|
|
847
945
|
<Input
|
|
@@ -864,7 +962,9 @@ function NovoTituloSheet({
|
|
|
864
962
|
name={`installments.${index}.amount` as const}
|
|
865
963
|
render={({ field }) => (
|
|
866
964
|
<FormItem>
|
|
867
|
-
<FormLabel className="text-xs">
|
|
965
|
+
<FormLabel className="text-xs">
|
|
966
|
+
{t('installmentsEditor.amountLabel')}
|
|
967
|
+
</FormLabel>
|
|
868
968
|
<FormControl>
|
|
869
969
|
<InputMoney
|
|
870
970
|
ref={field.ref}
|
|
@@ -908,8 +1008,11 @@ function NovoTituloSheet({
|
|
|
908
1008
|
: 'text-destructive'
|
|
909
1009
|
}`}
|
|
910
1010
|
>
|
|
911
|
-
|
|
912
|
-
|
|
1011
|
+
{t('installmentsEditor.totalPrefix', {
|
|
1012
|
+
total: installmentsTotal.toFixed(2),
|
|
1013
|
+
})}
|
|
1014
|
+
{installmentsDiffCents > 0 &&
|
|
1015
|
+
` ${t('installmentsEditor.adjustmentNeeded')}`}
|
|
913
1016
|
</p>
|
|
914
1017
|
{form.formState.errors.installments?.message && (
|
|
915
1018
|
<p className="text-xs text-destructive">
|
|
@@ -918,56 +1021,23 @@ function NovoTituloSheet({
|
|
|
918
1021
|
)}
|
|
919
1022
|
</div>
|
|
920
1023
|
|
|
921
|
-
<
|
|
922
|
-
|
|
1024
|
+
<CategoryFieldWithCreate
|
|
1025
|
+
form={form}
|
|
923
1026
|
name="categoriaId"
|
|
924
|
-
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
<SelectTrigger>
|
|
930
|
-
<SelectValue placeholder={t('common.select')} />
|
|
931
|
-
</SelectTrigger>
|
|
932
|
-
</FormControl>
|
|
933
|
-
<SelectContent>
|
|
934
|
-
{categorias
|
|
935
|
-
.filter((c) => c.natureza === 'receita')
|
|
936
|
-
.map((c) => (
|
|
937
|
-
<SelectItem key={c.id} value={String(c.id)}>
|
|
938
|
-
{c.codigo} - {c.nome}
|
|
939
|
-
</SelectItem>
|
|
940
|
-
))}
|
|
941
|
-
</SelectContent>
|
|
942
|
-
</Select>
|
|
943
|
-
<FormMessage />
|
|
944
|
-
</FormItem>
|
|
945
|
-
)}
|
|
1027
|
+
label={t('fields.category')}
|
|
1028
|
+
selectPlaceholder={t('common.select')}
|
|
1029
|
+
categories={categorias}
|
|
1030
|
+
categoryKind="receita"
|
|
1031
|
+
onCreated={onOptionsUpdated}
|
|
946
1032
|
/>
|
|
947
1033
|
|
|
948
|
-
<
|
|
949
|
-
|
|
1034
|
+
<CostCenterFieldWithCreate
|
|
1035
|
+
form={form}
|
|
950
1036
|
name="centroCustoId"
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
955
|
-
<FormControl>
|
|
956
|
-
<SelectTrigger>
|
|
957
|
-
<SelectValue placeholder={t('common.select')} />
|
|
958
|
-
</SelectTrigger>
|
|
959
|
-
</FormControl>
|
|
960
|
-
<SelectContent>
|
|
961
|
-
{centrosCusto.map((c) => (
|
|
962
|
-
<SelectItem key={c.id} value={String(c.id)}>
|
|
963
|
-
{c.codigo} - {c.nome}
|
|
964
|
-
</SelectItem>
|
|
965
|
-
))}
|
|
966
|
-
</SelectContent>
|
|
967
|
-
</Select>
|
|
968
|
-
<FormMessage />
|
|
969
|
-
</FormItem>
|
|
970
|
-
)}
|
|
1037
|
+
label={t('fields.costCenter')}
|
|
1038
|
+
selectPlaceholder={t('common.select')}
|
|
1039
|
+
costCenters={centrosCusto}
|
|
1040
|
+
onCreated={onOptionsUpdated}
|
|
971
1041
|
/>
|
|
972
1042
|
|
|
973
1043
|
<FormField
|
|
@@ -978,7 +1048,7 @@ function NovoTituloSheet({
|
|
|
978
1048
|
<FormLabel>{t('fields.channel')}</FormLabel>
|
|
979
1049
|
<Select value={field.value} onValueChange={field.onChange}>
|
|
980
1050
|
<FormControl>
|
|
981
|
-
<SelectTrigger>
|
|
1051
|
+
<SelectTrigger className="w-full">
|
|
982
1052
|
<SelectValue placeholder={t('common.select')} />
|
|
983
1053
|
</SelectTrigger>
|
|
984
1054
|
</FormControl>
|
|
@@ -1019,10 +1089,7 @@ function NovoTituloSheet({
|
|
|
1019
1089
|
/>
|
|
1020
1090
|
</div>
|
|
1021
1091
|
|
|
1022
|
-
<div className="flex
|
|
1023
|
-
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
1024
|
-
{t('common.cancel')}
|
|
1025
|
-
</Button>
|
|
1092
|
+
<div className="flex flex-col gap-2 py-4">
|
|
1026
1093
|
<Button
|
|
1027
1094
|
type="submit"
|
|
1028
1095
|
disabled={
|
|
@@ -1035,9 +1102,9 @@ function NovoTituloSheet({
|
|
|
1035
1102
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1036
1103
|
)}
|
|
1037
1104
|
{isExtractingFileData
|
|
1038
|
-
? '
|
|
1105
|
+
? t('common.upload.fillingWithAi')
|
|
1039
1106
|
: isUploadingFile
|
|
1040
|
-
? '
|
|
1107
|
+
? t('common.upload.uploadingFile')
|
|
1041
1108
|
: t('common.save')}
|
|
1042
1109
|
</Button>
|
|
1043
1110
|
</div>
|
|
@@ -1048,87 +1115,1154 @@ function NovoTituloSheet({
|
|
|
1048
1115
|
);
|
|
1049
1116
|
}
|
|
1050
1117
|
|
|
1051
|
-
|
|
1052
|
-
|
|
1118
|
+
function EditarTituloSheet({
|
|
1119
|
+
open,
|
|
1120
|
+
onOpenChange,
|
|
1121
|
+
titulo,
|
|
1122
|
+
categorias,
|
|
1123
|
+
centrosCusto,
|
|
1124
|
+
t,
|
|
1125
|
+
onUpdated,
|
|
1126
|
+
onOptionsUpdated,
|
|
1127
|
+
}: {
|
|
1128
|
+
open: boolean;
|
|
1129
|
+
onOpenChange: (open: boolean) => void;
|
|
1130
|
+
titulo?: any;
|
|
1131
|
+
categorias: any[];
|
|
1132
|
+
centrosCusto: any[];
|
|
1133
|
+
t: ReturnType<typeof useTranslations>;
|
|
1134
|
+
onUpdated: () => Promise<any> | void;
|
|
1135
|
+
onOptionsUpdated?: () => Promise<any> | void;
|
|
1136
|
+
}) {
|
|
1053
1137
|
const { request, showToastHandler } = useApp();
|
|
1054
|
-
const
|
|
1055
|
-
const
|
|
1056
|
-
|
|
1057
|
-
const
|
|
1058
|
-
|
|
1059
|
-
const [
|
|
1060
|
-
|
|
1061
|
-
|
|
1062
|
-
|
|
1063
|
-
|
|
1064
|
-
|
|
1065
|
-
|
|
1066
|
-
|
|
1067
|
-
|
|
1068
|
-
|
|
1069
|
-
|
|
1070
|
-
className: 'bg-purple-100 text-purple-700',
|
|
1071
|
-
},
|
|
1072
|
-
transferencia: {
|
|
1073
|
-
label: t('channels.transfer'),
|
|
1074
|
-
className: 'bg-orange-100 text-orange-700',
|
|
1075
|
-
},
|
|
1076
|
-
};
|
|
1077
|
-
|
|
1078
|
-
const filteredTitulos = titulosReceber.filter((titulo) => {
|
|
1079
|
-
const matchesSearch =
|
|
1080
|
-
titulo.documento.toLowerCase().includes(search.toLowerCase()) ||
|
|
1081
|
-
getPessoaById(titulo.clienteId)
|
|
1082
|
-
?.nome.toLowerCase()
|
|
1083
|
-
.includes(search.toLowerCase());
|
|
1084
|
-
|
|
1085
|
-
const matchesStatus = !statusFilter || titulo.status === statusFilter;
|
|
1138
|
+
const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
|
|
1139
|
+
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
1140
|
+
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
1141
|
+
const [isExtractingFileData, setIsExtractingFileData] = useState(false);
|
|
1142
|
+
const [isInstallmentsEdited, setIsInstallmentsEdited] = useState(false);
|
|
1143
|
+
const [autoRedistributeInstallments, setAutoRedistributeInstallments] =
|
|
1144
|
+
useState(true);
|
|
1145
|
+
const [extractionConfidence, setExtractionConfidence] = useState<
|
|
1146
|
+
number | null
|
|
1147
|
+
>(null);
|
|
1148
|
+
const [extractionWarnings, setExtractionWarnings] = useState<string[]>([]);
|
|
1149
|
+
const [uploadProgress, setUploadProgress] = useState(0);
|
|
1150
|
+
const fileInputRef = useRef<HTMLInputElement | null>(null);
|
|
1151
|
+
const redistributionTimeoutRef = useRef<
|
|
1152
|
+
Record<number, ReturnType<typeof setTimeout>>
|
|
1153
|
+
>({});
|
|
1086
1154
|
|
|
1087
|
-
|
|
1088
|
-
|
|
1155
|
+
const normalizeFilenameForDisplay = (filename: string) => {
|
|
1156
|
+
if (!filename) {
|
|
1157
|
+
return filename;
|
|
1158
|
+
}
|
|
1089
1159
|
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
return;
|
|
1160
|
+
if (!/Ã.|Â.|â[\u0080-\u00BF]/.test(filename)) {
|
|
1161
|
+
return filename;
|
|
1093
1162
|
}
|
|
1094
1163
|
|
|
1095
1164
|
try {
|
|
1096
|
-
const
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
});
|
|
1100
|
-
|
|
1101
|
-
const url = response?.data?.url;
|
|
1102
|
-
if (!url) {
|
|
1103
|
-
showToastHandler?.('error', 'Não foi possível abrir o anexo');
|
|
1104
|
-
return;
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
window.open(url, '_blank', 'noopener,noreferrer');
|
|
1165
|
+
const bytes = Uint8Array.from(filename, (char) => char.charCodeAt(0));
|
|
1166
|
+
const decoded = new TextDecoder('utf-8').decode(bytes);
|
|
1167
|
+
return /Ã.|Â.|â[\u0080-\u00BF]/.test(decoded) ? filename : decoded;
|
|
1108
1168
|
} catch {
|
|
1109
|
-
|
|
1169
|
+
return filename;
|
|
1110
1170
|
}
|
|
1111
1171
|
};
|
|
1112
1172
|
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
|
|
1117
|
-
|
|
1118
|
-
|
|
1119
|
-
|
|
1120
|
-
|
|
1121
|
-
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1173
|
+
const newTitleFormSchema = useMemo(() => getNewTitleFormSchema(t), [t]);
|
|
1174
|
+
|
|
1175
|
+
const form = useForm<NewTitleFormValues>({
|
|
1176
|
+
resolver: zodResolver(newTitleFormSchema),
|
|
1177
|
+
defaultValues: {
|
|
1178
|
+
documento: '',
|
|
1179
|
+
clienteId: '',
|
|
1180
|
+
competencia: '',
|
|
1181
|
+
vencimento: '',
|
|
1182
|
+
valor: 0,
|
|
1183
|
+
installmentsCount: 1,
|
|
1184
|
+
installments: [{ dueDate: '', amount: 0 }],
|
|
1185
|
+
categoriaId: '',
|
|
1186
|
+
centroCustoId: '',
|
|
1187
|
+
canal: '',
|
|
1188
|
+
descricao: '',
|
|
1189
|
+
},
|
|
1190
|
+
});
|
|
1191
|
+
|
|
1192
|
+
const { fields: installmentFields, replace: replaceInstallments } =
|
|
1193
|
+
useFieldArray({
|
|
1194
|
+
control: form.control,
|
|
1195
|
+
name: 'installments',
|
|
1196
|
+
});
|
|
1197
|
+
|
|
1198
|
+
const watchedInstallmentsCount = form.watch('installmentsCount');
|
|
1199
|
+
const watchedTotalValue = form.watch('valor');
|
|
1200
|
+
const watchedDueDate = form.watch('vencimento');
|
|
1201
|
+
const watchedInstallments = form.watch('installments');
|
|
1202
|
+
|
|
1203
|
+
const toDateInput = (value?: string) => {
|
|
1204
|
+
if (!value) {
|
|
1205
|
+
return '';
|
|
1206
|
+
}
|
|
1207
|
+
|
|
1208
|
+
if (/^\d{4}-\d{2}-\d{2}$/.test(value)) {
|
|
1209
|
+
return value;
|
|
1210
|
+
}
|
|
1211
|
+
|
|
1212
|
+
const parsed = new Date(value);
|
|
1213
|
+
if (Number.isNaN(parsed.getTime())) {
|
|
1214
|
+
return '';
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
return parsed.toISOString().slice(0, 10);
|
|
1218
|
+
};
|
|
1219
|
+
|
|
1220
|
+
useEffect(() => {
|
|
1221
|
+
if (!open || !titulo) {
|
|
1222
|
+
return;
|
|
1223
|
+
}
|
|
1224
|
+
|
|
1225
|
+
const installments = Array.isArray(titulo.parcelas) ? titulo.parcelas : [];
|
|
1226
|
+
const normalizedInstallments =
|
|
1227
|
+
installments.length > 0
|
|
1228
|
+
? installments.map((installment: any) => ({
|
|
1229
|
+
dueDate: toDateInput(installment.vencimento),
|
|
1230
|
+
amount: Number(installment.valor || 0),
|
|
1231
|
+
}))
|
|
1232
|
+
: [
|
|
1233
|
+
{
|
|
1234
|
+
dueDate: toDateInput(titulo?.vencimento),
|
|
1235
|
+
amount: Number(titulo?.valorTotal || 0),
|
|
1236
|
+
},
|
|
1237
|
+
];
|
|
1238
|
+
|
|
1239
|
+
form.reset({
|
|
1240
|
+
documento: titulo.documento || '',
|
|
1241
|
+
clienteId: titulo.clienteId || '',
|
|
1242
|
+
competencia: titulo.competencia || '',
|
|
1243
|
+
vencimento:
|
|
1244
|
+
normalizedInstallments[0]?.dueDate || toDateInput(titulo?.vencimento),
|
|
1245
|
+
valor: Number(titulo.valorTotal || 0),
|
|
1246
|
+
installmentsCount: normalizedInstallments.length,
|
|
1247
|
+
installments: normalizedInstallments,
|
|
1248
|
+
categoriaId: titulo.categoriaId || '',
|
|
1249
|
+
centroCustoId: titulo.centroCustoId || '',
|
|
1250
|
+
canal: installments[0]?.canal || '',
|
|
1251
|
+
descricao: titulo.descricao || '',
|
|
1252
|
+
});
|
|
1253
|
+
|
|
1254
|
+
const attachmentSource = Array.isArray(titulo.anexosDetalhes)
|
|
1255
|
+
? titulo.anexosDetalhes
|
|
1256
|
+
: Array.isArray(titulo.anexos)
|
|
1257
|
+
? titulo.anexos
|
|
1258
|
+
: [];
|
|
1259
|
+
const firstAttachment = attachmentSource[0] as any;
|
|
1260
|
+
const firstAttachmentIdRaw =
|
|
1261
|
+
firstAttachment && typeof firstAttachment === 'object'
|
|
1262
|
+
? (firstAttachment.id ??
|
|
1263
|
+
firstAttachment.file_id ??
|
|
1264
|
+
firstAttachment.fileId)
|
|
1265
|
+
: undefined;
|
|
1266
|
+
const parsedAttachmentId = Number(firstAttachmentIdRaw);
|
|
1267
|
+
|
|
1268
|
+
setUploadedFileId(
|
|
1269
|
+
Number.isFinite(parsedAttachmentId) ? parsedAttachmentId : null
|
|
1270
|
+
);
|
|
1271
|
+
setUploadedFileName(
|
|
1272
|
+
normalizeFilenameForDisplay(
|
|
1273
|
+
typeof firstAttachment === 'string'
|
|
1274
|
+
? firstAttachment
|
|
1275
|
+
: firstAttachment?.filename ||
|
|
1276
|
+
firstAttachment?.originalname ||
|
|
1277
|
+
firstAttachment?.name ||
|
|
1278
|
+
firstAttachment?.nome ||
|
|
1279
|
+
firstAttachment?.file_name ||
|
|
1280
|
+
firstAttachment?.fileName ||
|
|
1281
|
+
''
|
|
1282
|
+
)
|
|
1283
|
+
);
|
|
1284
|
+
setExtractionConfidence(null);
|
|
1285
|
+
setExtractionWarnings([]);
|
|
1286
|
+
setUploadProgress(0);
|
|
1287
|
+
|
|
1288
|
+
setIsInstallmentsEdited(true);
|
|
1289
|
+
}, [form, open, titulo]);
|
|
1290
|
+
|
|
1291
|
+
useEffect(() => {
|
|
1292
|
+
if (isInstallmentsEdited || !open) {
|
|
1293
|
+
return;
|
|
1294
|
+
}
|
|
1295
|
+
|
|
1296
|
+
replaceInstallments(
|
|
1297
|
+
buildEqualInstallments(
|
|
1298
|
+
watchedInstallmentsCount,
|
|
1299
|
+
watchedTotalValue,
|
|
1300
|
+
watchedDueDate
|
|
1301
|
+
)
|
|
1302
|
+
);
|
|
1303
|
+
}, [
|
|
1304
|
+
isInstallmentsEdited,
|
|
1305
|
+
open,
|
|
1306
|
+
replaceInstallments,
|
|
1307
|
+
watchedDueDate,
|
|
1308
|
+
watchedInstallmentsCount,
|
|
1309
|
+
watchedTotalValue,
|
|
1310
|
+
]);
|
|
1311
|
+
|
|
1312
|
+
const installmentsTotal = (watchedInstallments || []).reduce(
|
|
1313
|
+
(acc, installment) => acc + Number(installment?.amount || 0),
|
|
1314
|
+
0
|
|
1315
|
+
);
|
|
1316
|
+
const installmentsDiffCents = Math.abs(
|
|
1317
|
+
Math.round(installmentsTotal * 100) -
|
|
1318
|
+
Math.round((watchedTotalValue || 0) * 100)
|
|
1319
|
+
);
|
|
1320
|
+
|
|
1321
|
+
const clearScheduledRedistribution = (index: number) => {
|
|
1322
|
+
const timeout = redistributionTimeoutRef.current[index];
|
|
1323
|
+
if (!timeout) {
|
|
1324
|
+
return;
|
|
1325
|
+
}
|
|
1326
|
+
|
|
1327
|
+
clearTimeout(timeout);
|
|
1328
|
+
delete redistributionTimeoutRef.current[index];
|
|
1329
|
+
};
|
|
1330
|
+
|
|
1331
|
+
const runInstallmentRedistribution = (index: number) => {
|
|
1332
|
+
if (!autoRedistributeInstallments) {
|
|
1333
|
+
return;
|
|
1334
|
+
}
|
|
1335
|
+
|
|
1336
|
+
const currentInstallments = form.getValues('installments');
|
|
1337
|
+
const editedInstallmentAmount = Number(
|
|
1338
|
+
currentInstallments[index]?.amount || 0
|
|
1339
|
+
);
|
|
1340
|
+
|
|
1341
|
+
const redistributedInstallments = redistributeRemainingInstallments(
|
|
1342
|
+
currentInstallments,
|
|
1343
|
+
index,
|
|
1344
|
+
editedInstallmentAmount,
|
|
1345
|
+
form.getValues('valor')
|
|
1346
|
+
);
|
|
1347
|
+
|
|
1348
|
+
replaceInstallments(redistributedInstallments);
|
|
1349
|
+
};
|
|
1350
|
+
|
|
1351
|
+
const scheduleInstallmentRedistribution = (index: number) => {
|
|
1352
|
+
clearScheduledRedistribution(index);
|
|
1353
|
+
|
|
1354
|
+
redistributionTimeoutRef.current[index] = setTimeout(() => {
|
|
1355
|
+
runInstallmentRedistribution(index);
|
|
1356
|
+
delete redistributionTimeoutRef.current[index];
|
|
1357
|
+
}, INSTALLMENT_REDISTRIBUTION_DEBOUNCE_MS);
|
|
1358
|
+
};
|
|
1359
|
+
|
|
1360
|
+
useEffect(() => {
|
|
1361
|
+
if (autoRedistributeInstallments) {
|
|
1362
|
+
return;
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
|
|
1366
|
+
redistributionTimeoutRef.current = {};
|
|
1367
|
+
}, [autoRedistributeInstallments]);
|
|
1368
|
+
|
|
1369
|
+
useEffect(() => {
|
|
1370
|
+
return () => {
|
|
1371
|
+
Object.values(redistributionTimeoutRef.current).forEach(clearTimeout);
|
|
1372
|
+
redistributionTimeoutRef.current = {};
|
|
1373
|
+
};
|
|
1374
|
+
}, []);
|
|
1375
|
+
|
|
1376
|
+
const clearUploadedFile = () => {
|
|
1377
|
+
setUploadedFileId(null);
|
|
1378
|
+
setUploadedFileName('');
|
|
1379
|
+
setExtractionConfidence(null);
|
|
1380
|
+
setExtractionWarnings([]);
|
|
1381
|
+
setUploadProgress(0);
|
|
1382
|
+
|
|
1383
|
+
if (fileInputRef.current) {
|
|
1384
|
+
fileInputRef.current.value = '';
|
|
1385
|
+
}
|
|
1386
|
+
};
|
|
1387
|
+
|
|
1388
|
+
const handleSelectFile = () => {
|
|
1389
|
+
if (fileInputRef.current) {
|
|
1390
|
+
fileInputRef.current.value = '';
|
|
1391
|
+
fileInputRef.current.click();
|
|
1392
|
+
}
|
|
1393
|
+
};
|
|
1394
|
+
|
|
1395
|
+
const uploadRelatedFile = async (file: File) => {
|
|
1396
|
+
setIsUploadingFile(true);
|
|
1397
|
+
setUploadProgress(0);
|
|
1398
|
+
|
|
1399
|
+
try {
|
|
1400
|
+
const formData = new FormData();
|
|
1401
|
+
formData.append('file', file);
|
|
1402
|
+
formData.append('destination', 'finance/titles');
|
|
1403
|
+
|
|
1404
|
+
const { data } = await request<{ id: number; filename: string }>({
|
|
1405
|
+
url: '/file',
|
|
1406
|
+
method: 'POST',
|
|
1407
|
+
data: formData,
|
|
1408
|
+
headers: {
|
|
1409
|
+
'Content-Type': 'multipart/form-data',
|
|
1410
|
+
},
|
|
1411
|
+
onUploadProgress: (event) => {
|
|
1412
|
+
if (!event.total) {
|
|
1413
|
+
return;
|
|
1414
|
+
}
|
|
1415
|
+
|
|
1416
|
+
const progress = Math.round((event.loaded * 100) / event.total);
|
|
1417
|
+
setUploadProgress(progress);
|
|
1418
|
+
},
|
|
1419
|
+
});
|
|
1420
|
+
|
|
1421
|
+
if (!data?.id) {
|
|
1422
|
+
throw new Error(t('messages.invalidFile'));
|
|
1423
|
+
}
|
|
1424
|
+
|
|
1425
|
+
setUploadedFileId(data.id);
|
|
1426
|
+
setUploadedFileName(
|
|
1427
|
+
normalizeFilenameForDisplay(data.filename || file.name)
|
|
1428
|
+
);
|
|
1429
|
+
setUploadProgress(100);
|
|
1430
|
+
showToastHandler?.('success', t('messages.attachSuccess'));
|
|
1431
|
+
|
|
1432
|
+
setIsExtractingFileData(true);
|
|
1433
|
+
try {
|
|
1434
|
+
const extraction = await request<{
|
|
1435
|
+
documento?: string | null;
|
|
1436
|
+
clienteId?: string;
|
|
1437
|
+
competencia?: string;
|
|
1438
|
+
vencimento?: string;
|
|
1439
|
+
valor?: number | null;
|
|
1440
|
+
categoriaId?: string;
|
|
1441
|
+
centroCustoId?: string;
|
|
1442
|
+
canal?: string;
|
|
1443
|
+
descricao?: string | null;
|
|
1444
|
+
confidence?: number | null;
|
|
1445
|
+
confidenceLevel?: 'low' | 'high' | null;
|
|
1446
|
+
warnings?: string[];
|
|
1447
|
+
}>({
|
|
1448
|
+
url: '/finance/accounts-receivable/installments/extract-from-file',
|
|
1449
|
+
method: 'POST',
|
|
1450
|
+
data: {
|
|
1451
|
+
file_id: data.id,
|
|
1452
|
+
},
|
|
1453
|
+
});
|
|
1454
|
+
|
|
1455
|
+
const extracted = extraction.data || {};
|
|
1456
|
+
setExtractionConfidence(
|
|
1457
|
+
typeof extracted.confidence === 'number' ? extracted.confidence : null
|
|
1458
|
+
);
|
|
1459
|
+
setExtractionWarnings(
|
|
1460
|
+
Array.isArray(extracted.warnings)
|
|
1461
|
+
? extracted.warnings.filter(Boolean)
|
|
1462
|
+
: []
|
|
1463
|
+
);
|
|
1464
|
+
|
|
1465
|
+
if (extracted.documento) {
|
|
1466
|
+
form.setValue('documento', extracted.documento, {
|
|
1467
|
+
shouldValidate: true,
|
|
1468
|
+
});
|
|
1469
|
+
}
|
|
1470
|
+
|
|
1471
|
+
if (extracted.clienteId) {
|
|
1472
|
+
form.setValue('clienteId', extracted.clienteId, {
|
|
1473
|
+
shouldValidate: true,
|
|
1474
|
+
});
|
|
1475
|
+
}
|
|
1476
|
+
|
|
1477
|
+
if (extracted.competencia) {
|
|
1478
|
+
form.setValue('competencia', extracted.competencia, {
|
|
1479
|
+
shouldValidate: true,
|
|
1480
|
+
});
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
if (extracted.vencimento) {
|
|
1484
|
+
form.setValue('vencimento', extracted.vencimento, {
|
|
1485
|
+
shouldValidate: true,
|
|
1486
|
+
});
|
|
1487
|
+
}
|
|
1488
|
+
|
|
1489
|
+
if (typeof extracted.valor === 'number' && extracted.valor > 0) {
|
|
1490
|
+
form.setValue('valor', extracted.valor, {
|
|
1491
|
+
shouldValidate: true,
|
|
1492
|
+
});
|
|
1493
|
+
}
|
|
1494
|
+
|
|
1495
|
+
if (extracted.categoriaId) {
|
|
1496
|
+
form.setValue('categoriaId', extracted.categoriaId, {
|
|
1497
|
+
shouldValidate: true,
|
|
1498
|
+
});
|
|
1499
|
+
}
|
|
1500
|
+
|
|
1501
|
+
if (extracted.centroCustoId) {
|
|
1502
|
+
form.setValue('centroCustoId', extracted.centroCustoId, {
|
|
1503
|
+
shouldValidate: true,
|
|
1504
|
+
});
|
|
1505
|
+
}
|
|
1506
|
+
|
|
1507
|
+
if (extracted.canal) {
|
|
1508
|
+
form.setValue('canal', extracted.canal, {
|
|
1509
|
+
shouldValidate: true,
|
|
1510
|
+
});
|
|
1511
|
+
}
|
|
1512
|
+
|
|
1513
|
+
if (extracted.descricao) {
|
|
1514
|
+
form.setValue('descricao', extracted.descricao, {
|
|
1515
|
+
shouldValidate: true,
|
|
1516
|
+
});
|
|
1517
|
+
}
|
|
1518
|
+
|
|
1519
|
+
showToastHandler?.('success', t('messages.aiExtractSuccess'));
|
|
1520
|
+
} catch {
|
|
1521
|
+
setExtractionConfidence(null);
|
|
1522
|
+
setExtractionWarnings([]);
|
|
1523
|
+
showToastHandler?.('error', t('messages.aiExtractError'));
|
|
1524
|
+
} finally {
|
|
1525
|
+
setIsExtractingFileData(false);
|
|
1526
|
+
}
|
|
1527
|
+
} catch {
|
|
1528
|
+
setUploadedFileId(null);
|
|
1529
|
+
setUploadedFileName('');
|
|
1530
|
+
setExtractionConfidence(null);
|
|
1531
|
+
setExtractionWarnings([]);
|
|
1532
|
+
setUploadProgress(0);
|
|
1533
|
+
showToastHandler?.('error', t('messages.uploadError'));
|
|
1534
|
+
} finally {
|
|
1535
|
+
setIsUploadingFile(false);
|
|
1536
|
+
}
|
|
1537
|
+
};
|
|
1538
|
+
|
|
1539
|
+
const handleSubmit = async (values: NewTitleFormValues) => {
|
|
1540
|
+
if (!titulo?.id) {
|
|
1541
|
+
showToastHandler?.('error', t('messages.invalidTitleForEdit'));
|
|
1542
|
+
return;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
try {
|
|
1546
|
+
await request({
|
|
1547
|
+
url: `/finance/accounts-receivable/installments/${titulo.id}`,
|
|
1548
|
+
method: 'PATCH',
|
|
1549
|
+
data: {
|
|
1550
|
+
document_number: values.documento,
|
|
1551
|
+
person_id: Number(values.clienteId),
|
|
1552
|
+
competence_date: values.competencia
|
|
1553
|
+
? `${values.competencia}-01`
|
|
1554
|
+
: undefined,
|
|
1555
|
+
due_date: values.vencimento,
|
|
1556
|
+
total_amount: values.valor,
|
|
1557
|
+
finance_category_id: values.categoriaId
|
|
1558
|
+
? Number(values.categoriaId)
|
|
1559
|
+
: undefined,
|
|
1560
|
+
cost_center_id: values.centroCustoId
|
|
1561
|
+
? Number(values.centroCustoId)
|
|
1562
|
+
: undefined,
|
|
1563
|
+
payment_channel: values.canal || undefined,
|
|
1564
|
+
description: values.descricao?.trim() || undefined,
|
|
1565
|
+
installments: values.installments.map((installment, index) => ({
|
|
1566
|
+
installment_number: index + 1,
|
|
1567
|
+
due_date: installment.dueDate || values.vencimento,
|
|
1568
|
+
amount: installment.amount,
|
|
1569
|
+
})),
|
|
1570
|
+
attachment_file_ids: uploadedFileId ? [uploadedFileId] : undefined,
|
|
1571
|
+
},
|
|
1572
|
+
});
|
|
1573
|
+
|
|
1574
|
+
await onUpdated();
|
|
1575
|
+
showToastHandler?.('success', t('messages.updateSuccess'));
|
|
1576
|
+
onOpenChange(false);
|
|
1577
|
+
} catch {
|
|
1578
|
+
showToastHandler?.('error', t('messages.updateError'));
|
|
1579
|
+
}
|
|
1580
|
+
};
|
|
1581
|
+
|
|
1582
|
+
return (
|
|
1583
|
+
<Sheet open={open} onOpenChange={onOpenChange}>
|
|
1584
|
+
<SheetContent className="w-full sm:max-w-lg overflow-y-auto">
|
|
1585
|
+
<SheetHeader>
|
|
1586
|
+
<SheetTitle>{t('table.actions.edit')}</SheetTitle>
|
|
1587
|
+
<SheetDescription>{t('editTitle.description')}</SheetDescription>
|
|
1588
|
+
</SheetHeader>
|
|
1589
|
+
<Form {...form}>
|
|
1590
|
+
<form className="px-4" onSubmit={form.handleSubmit(handleSubmit)}>
|
|
1591
|
+
<div className="grid gap-3">
|
|
1592
|
+
<div className="grid grid-cols-1 items-start gap-4 sm:grid-cols-2">
|
|
1593
|
+
<div className="grid gap-2">
|
|
1594
|
+
<FormLabel>{t('common.upload.label')}</FormLabel>
|
|
1595
|
+
<Input
|
|
1596
|
+
ref={fileInputRef}
|
|
1597
|
+
className="hidden"
|
|
1598
|
+
type="file"
|
|
1599
|
+
accept=".pdf,.png,.jpg,.jpeg,.xml,.txt"
|
|
1600
|
+
onChange={(event) => {
|
|
1601
|
+
const file = event.target.files?.[0];
|
|
1602
|
+
if (!file) {
|
|
1603
|
+
return;
|
|
1604
|
+
}
|
|
1605
|
+
|
|
1606
|
+
clearUploadedFile();
|
|
1607
|
+
void uploadRelatedFile(file);
|
|
1608
|
+
}}
|
|
1609
|
+
disabled={
|
|
1610
|
+
isUploadingFile ||
|
|
1611
|
+
isExtractingFileData ||
|
|
1612
|
+
form.formState.isSubmitting
|
|
1613
|
+
}
|
|
1614
|
+
/>
|
|
1615
|
+
|
|
1616
|
+
<div className="grid w-full grid-cols-2 gap-2">
|
|
1617
|
+
<Tooltip>
|
|
1618
|
+
<TooltipTrigger asChild>
|
|
1619
|
+
<Button
|
|
1620
|
+
type="button"
|
|
1621
|
+
variant="outline"
|
|
1622
|
+
className={
|
|
1623
|
+
uploadedFileId ? 'w-full' : 'col-span-2 w-full'
|
|
1624
|
+
}
|
|
1625
|
+
onClick={handleSelectFile}
|
|
1626
|
+
aria-label={
|
|
1627
|
+
uploadedFileId
|
|
1628
|
+
? t('common.upload.change')
|
|
1629
|
+
: t('common.upload.upload')
|
|
1630
|
+
}
|
|
1631
|
+
disabled={
|
|
1632
|
+
isUploadingFile ||
|
|
1633
|
+
isExtractingFileData ||
|
|
1634
|
+
form.formState.isSubmitting
|
|
1635
|
+
}
|
|
1636
|
+
>
|
|
1637
|
+
{uploadedFileId ? (
|
|
1638
|
+
<Upload className="h-4 w-4" />
|
|
1639
|
+
) : (
|
|
1640
|
+
<>
|
|
1641
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
1642
|
+
{t('common.upload.upload')}
|
|
1643
|
+
</>
|
|
1644
|
+
)}
|
|
1645
|
+
</Button>
|
|
1646
|
+
</TooltipTrigger>
|
|
1647
|
+
<TooltipContent>
|
|
1648
|
+
{uploadedFileId
|
|
1649
|
+
? t('common.upload.change')
|
|
1650
|
+
: t('common.upload.upload')}
|
|
1651
|
+
</TooltipContent>
|
|
1652
|
+
</Tooltip>
|
|
1653
|
+
|
|
1654
|
+
{uploadedFileId && (
|
|
1655
|
+
<Tooltip>
|
|
1656
|
+
<TooltipTrigger asChild>
|
|
1657
|
+
<Button
|
|
1658
|
+
type="button"
|
|
1659
|
+
variant="outline"
|
|
1660
|
+
className="w-full"
|
|
1661
|
+
onClick={clearUploadedFile}
|
|
1662
|
+
aria-label={t('common.upload.remove')}
|
|
1663
|
+
disabled={
|
|
1664
|
+
isUploadingFile ||
|
|
1665
|
+
isExtractingFileData ||
|
|
1666
|
+
form.formState.isSubmitting
|
|
1667
|
+
}
|
|
1668
|
+
>
|
|
1669
|
+
<Trash2 className="h-4 w-4" />
|
|
1670
|
+
</Button>
|
|
1671
|
+
</TooltipTrigger>
|
|
1672
|
+
<TooltipContent>
|
|
1673
|
+
{t('common.upload.remove')}
|
|
1674
|
+
</TooltipContent>
|
|
1675
|
+
</Tooltip>
|
|
1676
|
+
)}
|
|
1677
|
+
</div>
|
|
1678
|
+
|
|
1679
|
+
<div className="space-y-1">
|
|
1680
|
+
{(uploadedFileId || uploadedFileName) && (
|
|
1681
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1682
|
+
{t('common.upload.selectedPrefix')} {uploadedFileName}
|
|
1683
|
+
</p>
|
|
1684
|
+
)}
|
|
1685
|
+
|
|
1686
|
+
{isUploadingFile && !isExtractingFileData && (
|
|
1687
|
+
<div className="space-y-1">
|
|
1688
|
+
<Progress value={uploadProgress} className="h-2" />
|
|
1689
|
+
<p className="text-xs text-muted-foreground">
|
|
1690
|
+
{t('common.upload.uploadingProgress', {
|
|
1691
|
+
progress: uploadProgress,
|
|
1692
|
+
})}
|
|
1693
|
+
</p>
|
|
1694
|
+
</div>
|
|
1695
|
+
)}
|
|
1696
|
+
|
|
1697
|
+
{isExtractingFileData && (
|
|
1698
|
+
<p className="flex items-center gap-2 text-xs text-primary">
|
|
1699
|
+
<Loader2 className="h-4 w-4 animate-spin" />
|
|
1700
|
+
{t('common.upload.processingAi')}
|
|
1701
|
+
</p>
|
|
1702
|
+
)}
|
|
1703
|
+
|
|
1704
|
+
{!isExtractingFileData &&
|
|
1705
|
+
extractionConfidence !== null &&
|
|
1706
|
+
extractionConfidence < 70 && (
|
|
1707
|
+
<p className="text-xs text-destructive">
|
|
1708
|
+
{t('common.upload.lowConfidence', {
|
|
1709
|
+
confidence: Math.round(extractionConfidence),
|
|
1710
|
+
})}
|
|
1711
|
+
</p>
|
|
1712
|
+
)}
|
|
1713
|
+
|
|
1714
|
+
{!isExtractingFileData && extractionWarnings.length > 0 && (
|
|
1715
|
+
<p className="truncate text-xs text-muted-foreground">
|
|
1716
|
+
{extractionWarnings[0]}
|
|
1717
|
+
</p>
|
|
1718
|
+
)}
|
|
1719
|
+
</div>
|
|
1720
|
+
</div>
|
|
1721
|
+
|
|
1722
|
+
<FormField
|
|
1723
|
+
control={form.control}
|
|
1724
|
+
name="documento"
|
|
1725
|
+
render={({ field }) => (
|
|
1726
|
+
<FormItem>
|
|
1727
|
+
<FormLabel>{t('fields.document')}</FormLabel>
|
|
1728
|
+
<FormControl>
|
|
1729
|
+
<Input placeholder="FAT-00000" {...field} />
|
|
1730
|
+
</FormControl>
|
|
1731
|
+
<FormMessage />
|
|
1732
|
+
</FormItem>
|
|
1733
|
+
)}
|
|
1734
|
+
/>
|
|
1735
|
+
</div>
|
|
1736
|
+
|
|
1737
|
+
<PersonFieldWithCreate
|
|
1738
|
+
form={form}
|
|
1739
|
+
name="clienteId"
|
|
1740
|
+
label={t('fields.client')}
|
|
1741
|
+
entityLabel="cliente"
|
|
1742
|
+
selectPlaceholder={t('common.select')}
|
|
1743
|
+
/>
|
|
1744
|
+
|
|
1745
|
+
<div className="grid grid-cols-2 items-start gap-3">
|
|
1746
|
+
<FormField
|
|
1747
|
+
control={form.control}
|
|
1748
|
+
name="competencia"
|
|
1749
|
+
render={({ field }) => (
|
|
1750
|
+
<FormItem>
|
|
1751
|
+
<FormLabel>{t('fields.competency')}</FormLabel>
|
|
1752
|
+
<FormControl>
|
|
1753
|
+
<Input
|
|
1754
|
+
type="month"
|
|
1755
|
+
{...field}
|
|
1756
|
+
value={field.value || ''}
|
|
1757
|
+
/>
|
|
1758
|
+
</FormControl>
|
|
1759
|
+
<FormMessage />
|
|
1760
|
+
</FormItem>
|
|
1761
|
+
)}
|
|
1762
|
+
/>
|
|
1763
|
+
|
|
1764
|
+
<FormField
|
|
1765
|
+
control={form.control}
|
|
1766
|
+
name="vencimento"
|
|
1767
|
+
render={({ field }) => (
|
|
1768
|
+
<FormItem>
|
|
1769
|
+
<FormLabel>{t('fields.dueDate')}</FormLabel>
|
|
1770
|
+
<FormControl>
|
|
1771
|
+
<Input
|
|
1772
|
+
type="date"
|
|
1773
|
+
{...field}
|
|
1774
|
+
value={field.value || ''}
|
|
1775
|
+
/>
|
|
1776
|
+
</FormControl>
|
|
1777
|
+
<FormMessage />
|
|
1778
|
+
</FormItem>
|
|
1779
|
+
)}
|
|
1780
|
+
/>
|
|
1781
|
+
</div>
|
|
1782
|
+
|
|
1783
|
+
<div className="grid grid-cols-1 items-start gap-3 sm:grid-cols-2">
|
|
1784
|
+
<FormField
|
|
1785
|
+
control={form.control}
|
|
1786
|
+
name="valor"
|
|
1787
|
+
render={({ field }) => (
|
|
1788
|
+
<FormItem>
|
|
1789
|
+
<FormLabel>{t('fields.totalValue')}</FormLabel>
|
|
1790
|
+
<FormControl>
|
|
1791
|
+
<InputMoney
|
|
1792
|
+
ref={field.ref}
|
|
1793
|
+
name={field.name}
|
|
1794
|
+
value={field.value}
|
|
1795
|
+
onBlur={field.onBlur}
|
|
1796
|
+
onValueChange={(value) => field.onChange(value ?? 0)}
|
|
1797
|
+
placeholder="0,00"
|
|
1798
|
+
/>
|
|
1799
|
+
</FormControl>
|
|
1800
|
+
<FormMessage />
|
|
1801
|
+
</FormItem>
|
|
1802
|
+
)}
|
|
1803
|
+
/>
|
|
1804
|
+
|
|
1805
|
+
<FormField
|
|
1806
|
+
control={form.control}
|
|
1807
|
+
name="installmentsCount"
|
|
1808
|
+
render={({ field }) => (
|
|
1809
|
+
<FormItem>
|
|
1810
|
+
<FormLabel>
|
|
1811
|
+
{t('installmentsEditor.countLabel')}
|
|
1812
|
+
</FormLabel>
|
|
1813
|
+
<FormControl>
|
|
1814
|
+
<Input
|
|
1815
|
+
type="number"
|
|
1816
|
+
min={1}
|
|
1817
|
+
max={120}
|
|
1818
|
+
value={field.value}
|
|
1819
|
+
onChange={(event) => {
|
|
1820
|
+
const nextValue = Number(event.target.value || 1);
|
|
1821
|
+
field.onChange(
|
|
1822
|
+
Number.isNaN(nextValue) ? 1 : nextValue
|
|
1823
|
+
);
|
|
1824
|
+
setIsInstallmentsEdited(false);
|
|
1825
|
+
}}
|
|
1826
|
+
/>
|
|
1827
|
+
</FormControl>
|
|
1828
|
+
<FormMessage />
|
|
1829
|
+
</FormItem>
|
|
1830
|
+
)}
|
|
1831
|
+
/>
|
|
1832
|
+
</div>
|
|
1833
|
+
|
|
1834
|
+
<div className="space-y-3 rounded-md border p-3">
|
|
1835
|
+
<div className="flex items-center justify-between gap-2">
|
|
1836
|
+
<p className="text-sm font-medium">
|
|
1837
|
+
{t('installmentsEditor.title')}
|
|
1838
|
+
</p>
|
|
1839
|
+
<Button
|
|
1840
|
+
type="button"
|
|
1841
|
+
variant="outline"
|
|
1842
|
+
size="sm"
|
|
1843
|
+
onClick={() => {
|
|
1844
|
+
setIsInstallmentsEdited(false);
|
|
1845
|
+
replaceInstallments(
|
|
1846
|
+
buildEqualInstallments(
|
|
1847
|
+
form.getValues('installmentsCount'),
|
|
1848
|
+
form.getValues('valor'),
|
|
1849
|
+
form.getValues('vencimento')
|
|
1850
|
+
)
|
|
1851
|
+
);
|
|
1852
|
+
}}
|
|
1853
|
+
>
|
|
1854
|
+
{t('installmentsEditor.recalculate')}
|
|
1855
|
+
</Button>
|
|
1856
|
+
</div>
|
|
1857
|
+
|
|
1858
|
+
<div className="flex items-center gap-2">
|
|
1859
|
+
<Checkbox
|
|
1860
|
+
id="auto-redistribute-installments-edit-receivable"
|
|
1861
|
+
checked={autoRedistributeInstallments}
|
|
1862
|
+
onCheckedChange={(checked) =>
|
|
1863
|
+
setAutoRedistributeInstallments(checked === true)
|
|
1864
|
+
}
|
|
1865
|
+
/>
|
|
1866
|
+
<Label
|
|
1867
|
+
htmlFor="auto-redistribute-installments-edit-receivable"
|
|
1868
|
+
className="text-xs text-muted-foreground"
|
|
1869
|
+
>
|
|
1870
|
+
{t('installmentsEditor.autoRedistributeLabel')}
|
|
1871
|
+
</Label>
|
|
1872
|
+
</div>
|
|
1873
|
+
|
|
1874
|
+
<div className="space-y-2">
|
|
1875
|
+
{installmentFields.map((installment, index) => (
|
|
1876
|
+
<div
|
|
1877
|
+
key={installment.id}
|
|
1878
|
+
className="grid grid-cols-1 items-start gap-2 rounded-md border p-2 sm:grid-cols-[96px_1fr_160px]"
|
|
1879
|
+
>
|
|
1880
|
+
<div className="flex items-center text-sm text-muted-foreground">
|
|
1881
|
+
#{index + 1}
|
|
1882
|
+
</div>
|
|
1883
|
+
|
|
1884
|
+
<FormField
|
|
1885
|
+
control={form.control}
|
|
1886
|
+
name={`installments.${index}.dueDate` as const}
|
|
1887
|
+
render={({ field }) => (
|
|
1888
|
+
<FormItem>
|
|
1889
|
+
<FormLabel className="text-xs">
|
|
1890
|
+
{t('installmentsEditor.dueDateLabel')}
|
|
1891
|
+
</FormLabel>
|
|
1892
|
+
<FormControl>
|
|
1893
|
+
<Input
|
|
1894
|
+
type="date"
|
|
1895
|
+
{...field}
|
|
1896
|
+
value={field.value || ''}
|
|
1897
|
+
onChange={(event) => {
|
|
1898
|
+
setIsInstallmentsEdited(true);
|
|
1899
|
+
field.onChange(event);
|
|
1900
|
+
}}
|
|
1901
|
+
/>
|
|
1902
|
+
</FormControl>
|
|
1903
|
+
<FormMessage />
|
|
1904
|
+
</FormItem>
|
|
1905
|
+
)}
|
|
1906
|
+
/>
|
|
1907
|
+
|
|
1908
|
+
<FormField
|
|
1909
|
+
control={form.control}
|
|
1910
|
+
name={`installments.${index}.amount` as const}
|
|
1911
|
+
render={({ field }) => (
|
|
1912
|
+
<FormItem>
|
|
1913
|
+
<FormLabel className="text-xs">
|
|
1914
|
+
{t('installmentsEditor.amountLabel')}
|
|
1915
|
+
</FormLabel>
|
|
1916
|
+
<FormControl>
|
|
1917
|
+
<InputMoney
|
|
1918
|
+
ref={field.ref}
|
|
1919
|
+
name={field.name}
|
|
1920
|
+
value={field.value}
|
|
1921
|
+
onBlur={() => {
|
|
1922
|
+
field.onBlur();
|
|
1923
|
+
|
|
1924
|
+
if (!autoRedistributeInstallments) {
|
|
1925
|
+
return;
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
clearScheduledRedistribution(index);
|
|
1929
|
+
runInstallmentRedistribution(index);
|
|
1930
|
+
}}
|
|
1931
|
+
onValueChange={(value) => {
|
|
1932
|
+
setIsInstallmentsEdited(true);
|
|
1933
|
+
field.onChange(value ?? 0);
|
|
1934
|
+
|
|
1935
|
+
if (!autoRedistributeInstallments) {
|
|
1936
|
+
return;
|
|
1937
|
+
}
|
|
1938
|
+
|
|
1939
|
+
scheduleInstallmentRedistribution(index);
|
|
1940
|
+
}}
|
|
1941
|
+
placeholder="0,00"
|
|
1942
|
+
/>
|
|
1943
|
+
</FormControl>
|
|
1944
|
+
<FormMessage />
|
|
1945
|
+
</FormItem>
|
|
1946
|
+
)}
|
|
1947
|
+
/>
|
|
1948
|
+
</div>
|
|
1949
|
+
))}
|
|
1950
|
+
</div>
|
|
1951
|
+
|
|
1952
|
+
<p
|
|
1953
|
+
className={`text-xs ${
|
|
1954
|
+
installmentsDiffCents === 0
|
|
1955
|
+
? 'text-muted-foreground'
|
|
1956
|
+
: 'text-destructive'
|
|
1957
|
+
}`}
|
|
1958
|
+
>
|
|
1959
|
+
{t('installmentsEditor.totalPrefix', {
|
|
1960
|
+
total: installmentsTotal.toFixed(2),
|
|
1961
|
+
})}
|
|
1962
|
+
{installmentsDiffCents > 0 &&
|
|
1963
|
+
` ${t('installmentsEditor.adjustmentNeeded')}`}
|
|
1964
|
+
</p>
|
|
1965
|
+
{form.formState.errors.installments?.message && (
|
|
1966
|
+
<p className="text-xs text-destructive">
|
|
1967
|
+
{form.formState.errors.installments.message}
|
|
1968
|
+
</p>
|
|
1969
|
+
)}
|
|
1970
|
+
</div>
|
|
1971
|
+
|
|
1972
|
+
<CategoryFieldWithCreate
|
|
1973
|
+
form={form}
|
|
1974
|
+
name="categoriaId"
|
|
1975
|
+
label={t('fields.category')}
|
|
1976
|
+
selectPlaceholder={t('common.select')}
|
|
1977
|
+
categories={categorias}
|
|
1978
|
+
categoryKind="receita"
|
|
1979
|
+
onCreated={onOptionsUpdated}
|
|
1980
|
+
/>
|
|
1981
|
+
|
|
1982
|
+
<CostCenterFieldWithCreate
|
|
1983
|
+
form={form}
|
|
1984
|
+
name="centroCustoId"
|
|
1985
|
+
label={t('fields.costCenter')}
|
|
1986
|
+
selectPlaceholder={t('common.select')}
|
|
1987
|
+
costCenters={centrosCusto}
|
|
1988
|
+
onCreated={onOptionsUpdated}
|
|
1989
|
+
/>
|
|
1990
|
+
|
|
1991
|
+
<FormField
|
|
1992
|
+
control={form.control}
|
|
1993
|
+
name="canal"
|
|
1994
|
+
render={({ field }) => (
|
|
1995
|
+
<FormItem>
|
|
1996
|
+
<FormLabel>{t('fields.channel')}</FormLabel>
|
|
1997
|
+
<Select value={field.value} onValueChange={field.onChange}>
|
|
1998
|
+
<FormControl>
|
|
1999
|
+
<SelectTrigger className="w-full">
|
|
2000
|
+
<SelectValue placeholder={t('common.select')} />
|
|
2001
|
+
</SelectTrigger>
|
|
2002
|
+
</FormControl>
|
|
2003
|
+
<SelectContent>
|
|
2004
|
+
<SelectItem value="boleto">
|
|
2005
|
+
{t('channels.boleto')}
|
|
2006
|
+
</SelectItem>
|
|
2007
|
+
<SelectItem value="pix">PIX</SelectItem>
|
|
2008
|
+
<SelectItem value="cartao">
|
|
2009
|
+
{t('channels.card')}
|
|
2010
|
+
</SelectItem>
|
|
2011
|
+
<SelectItem value="transferencia">
|
|
2012
|
+
{t('channels.transfer')}
|
|
2013
|
+
</SelectItem>
|
|
2014
|
+
</SelectContent>
|
|
2015
|
+
</Select>
|
|
2016
|
+
<FormMessage />
|
|
2017
|
+
</FormItem>
|
|
2018
|
+
)}
|
|
2019
|
+
/>
|
|
2020
|
+
|
|
2021
|
+
<FormField
|
|
2022
|
+
control={form.control}
|
|
2023
|
+
name="descricao"
|
|
2024
|
+
render={({ field }) => (
|
|
2025
|
+
<FormItem>
|
|
2026
|
+
<FormLabel>{t('fields.description')}</FormLabel>
|
|
2027
|
+
<FormControl>
|
|
2028
|
+
<Textarea
|
|
2029
|
+
placeholder={t('newTitle.descriptionPlaceholder')}
|
|
2030
|
+
{...field}
|
|
2031
|
+
value={field.value || ''}
|
|
2032
|
+
/>
|
|
2033
|
+
</FormControl>
|
|
2034
|
+
<FormMessage />
|
|
2035
|
+
</FormItem>
|
|
2036
|
+
)}
|
|
2037
|
+
/>
|
|
2038
|
+
</div>
|
|
2039
|
+
|
|
2040
|
+
<div className="flex flex-col gap-2 py-4">
|
|
2041
|
+
<Button
|
|
2042
|
+
type="submit"
|
|
2043
|
+
disabled={
|
|
2044
|
+
form.formState.isSubmitting ||
|
|
2045
|
+
isUploadingFile ||
|
|
2046
|
+
isExtractingFileData
|
|
2047
|
+
}
|
|
2048
|
+
>
|
|
2049
|
+
{(isUploadingFile || isExtractingFileData) && (
|
|
2050
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
2051
|
+
)}
|
|
2052
|
+
{isExtractingFileData
|
|
2053
|
+
? t('common.upload.fillingWithAi')
|
|
2054
|
+
: isUploadingFile
|
|
2055
|
+
? t('common.upload.uploadingFile')
|
|
2056
|
+
: t('common.save')}
|
|
2057
|
+
</Button>
|
|
2058
|
+
</div>
|
|
2059
|
+
</form>
|
|
2060
|
+
</Form>
|
|
2061
|
+
</SheetContent>
|
|
2062
|
+
</Sheet>
|
|
2063
|
+
);
|
|
2064
|
+
}
|
|
2065
|
+
|
|
2066
|
+
export default function TitulosReceberPage() {
|
|
2067
|
+
const t = useTranslations('finance.ReceivableInstallmentsPage');
|
|
2068
|
+
const { request, showToastHandler } = useApp();
|
|
2069
|
+
const pathname = usePathname();
|
|
2070
|
+
const router = useRouter();
|
|
2071
|
+
const searchParams = useSearchParams();
|
|
2072
|
+
const { data, refetch: refetchFinanceData } = useFinanceData();
|
|
2073
|
+
const {
|
|
2074
|
+
titulosReceber: allTitulosReceber,
|
|
2075
|
+
pessoas,
|
|
2076
|
+
categorias,
|
|
2077
|
+
centrosCusto,
|
|
2078
|
+
} = data;
|
|
2079
|
+
|
|
2080
|
+
const getPessoaById = (id?: string) => pessoas.find((p) => p.id === id);
|
|
2081
|
+
|
|
2082
|
+
const [search, setSearch] = useState('');
|
|
2083
|
+
const [statusFilter, setStatusFilter] = useState<string>('');
|
|
2084
|
+
const [page, setPage] = useState(1);
|
|
2085
|
+
const pageSize = 10;
|
|
2086
|
+
|
|
2087
|
+
const normalizedStatusFilter =
|
|
2088
|
+
statusFilter && statusFilter !== 'all' ? statusFilter : undefined;
|
|
2089
|
+
|
|
2090
|
+
const {
|
|
2091
|
+
data: paginatedTitlesResponse,
|
|
2092
|
+
refetch: refetchTitles,
|
|
2093
|
+
isFetching: isFetchingTitles,
|
|
2094
|
+
} = useQuery<{
|
|
2095
|
+
data: any[];
|
|
2096
|
+
total: number;
|
|
2097
|
+
page: number;
|
|
2098
|
+
pageSize: number;
|
|
2099
|
+
prev: number | null;
|
|
2100
|
+
next: number | null;
|
|
2101
|
+
lastPage: number;
|
|
2102
|
+
}>({
|
|
2103
|
+
queryKey: [
|
|
2104
|
+
'finance-receivable-installments-list',
|
|
2105
|
+
search,
|
|
2106
|
+
normalizedStatusFilter,
|
|
2107
|
+
page,
|
|
2108
|
+
pageSize,
|
|
2109
|
+
],
|
|
2110
|
+
queryFn: async () => {
|
|
2111
|
+
const response = await request({
|
|
2112
|
+
url: '/finance/accounts-receivable/installments',
|
|
2113
|
+
method: 'GET',
|
|
2114
|
+
params: {
|
|
2115
|
+
page,
|
|
2116
|
+
pageSize,
|
|
2117
|
+
search: search.trim() || undefined,
|
|
2118
|
+
status: normalizedStatusFilter,
|
|
2119
|
+
},
|
|
2120
|
+
});
|
|
2121
|
+
|
|
2122
|
+
return response.data as {
|
|
2123
|
+
data: any[];
|
|
2124
|
+
total: number;
|
|
2125
|
+
page: number;
|
|
2126
|
+
pageSize: number;
|
|
2127
|
+
prev: number | null;
|
|
2128
|
+
next: number | null;
|
|
2129
|
+
lastPage: number;
|
|
2130
|
+
};
|
|
2131
|
+
},
|
|
2132
|
+
placeholderData: (old) => old,
|
|
2133
|
+
});
|
|
2134
|
+
|
|
2135
|
+
const titulosReceber = paginatedTitlesResponse?.data || [];
|
|
2136
|
+
const [editingTitleId, setEditingTitleId] = useState<string | null>(null);
|
|
2137
|
+
|
|
2138
|
+
const editingTitle = useMemo(
|
|
2139
|
+
() =>
|
|
2140
|
+
titulosReceber.find((item) => item.id === editingTitleId) ||
|
|
2141
|
+
allTitulosReceber.find((item) => item.id === editingTitleId),
|
|
2142
|
+
[allTitulosReceber, editingTitleId, titulosReceber]
|
|
2143
|
+
);
|
|
2144
|
+
|
|
2145
|
+
useEffect(() => {
|
|
2146
|
+
const editId = searchParams.get('editId');
|
|
2147
|
+
if (!editId || editingTitleId) {
|
|
2148
|
+
return;
|
|
2149
|
+
}
|
|
2150
|
+
|
|
2151
|
+
const foundTitle =
|
|
2152
|
+
titulosReceber.find((item) => item.id === editId) ||
|
|
2153
|
+
allTitulosReceber.find((item) => item.id === editId);
|
|
2154
|
+
if (!foundTitle) {
|
|
2155
|
+
return;
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
if (foundTitle.status !== 'rascunho') {
|
|
2159
|
+
showToastHandler?.('error', t('messages.editDraftOnly'));
|
|
2160
|
+
router.replace(pathname, { scroll: false });
|
|
2161
|
+
return;
|
|
2162
|
+
}
|
|
2163
|
+
|
|
2164
|
+
setEditingTitleId(editId);
|
|
2165
|
+
}, [
|
|
2166
|
+
allTitulosReceber,
|
|
2167
|
+
editingTitleId,
|
|
2168
|
+
pathname,
|
|
2169
|
+
router,
|
|
2170
|
+
searchParams,
|
|
2171
|
+
showToastHandler,
|
|
2172
|
+
titulosReceber,
|
|
2173
|
+
]);
|
|
2174
|
+
|
|
2175
|
+
const closeEditSheet = (isOpen: boolean) => {
|
|
2176
|
+
if (isOpen) {
|
|
2177
|
+
return;
|
|
2178
|
+
}
|
|
2179
|
+
|
|
2180
|
+
setEditingTitleId(null);
|
|
2181
|
+
|
|
2182
|
+
if (searchParams.get('editId')) {
|
|
2183
|
+
router.replace(pathname, { scroll: false });
|
|
2184
|
+
}
|
|
2185
|
+
};
|
|
2186
|
+
|
|
2187
|
+
useEffect(() => {
|
|
2188
|
+
setPage(1);
|
|
2189
|
+
}, [search, normalizedStatusFilter]);
|
|
2190
|
+
|
|
2191
|
+
const canalBadge = {
|
|
2192
|
+
boleto: {
|
|
2193
|
+
label: t('channels.boleto'),
|
|
2194
|
+
className: 'bg-blue-100 text-blue-700',
|
|
2195
|
+
},
|
|
2196
|
+
pix: { label: 'PIX', className: 'bg-green-100 text-green-700' },
|
|
2197
|
+
cartao: {
|
|
2198
|
+
label: t('channels.card'),
|
|
2199
|
+
className: 'bg-purple-100 text-purple-700',
|
|
2200
|
+
},
|
|
2201
|
+
transferencia: {
|
|
2202
|
+
label: t('channels.transfer'),
|
|
2203
|
+
className: 'bg-orange-100 text-orange-700',
|
|
2204
|
+
},
|
|
2205
|
+
};
|
|
2206
|
+
|
|
2207
|
+
const handleOpenAttachment = async (fileId?: string) => {
|
|
2208
|
+
if (!fileId) {
|
|
2209
|
+
return;
|
|
2210
|
+
}
|
|
2211
|
+
|
|
2212
|
+
try {
|
|
2213
|
+
const response = await request<{ url?: string }>({
|
|
2214
|
+
url: `/file/open/${fileId}`,
|
|
2215
|
+
method: 'PUT',
|
|
2216
|
+
});
|
|
2217
|
+
|
|
2218
|
+
const url = response?.data?.url;
|
|
2219
|
+
if (!url) {
|
|
2220
|
+
showToastHandler?.('error', t('messages.openAttachmentError'));
|
|
2221
|
+
return;
|
|
2222
|
+
}
|
|
2223
|
+
|
|
2224
|
+
window.open(url, '_blank', 'noopener,noreferrer');
|
|
2225
|
+
} catch {
|
|
2226
|
+
showToastHandler?.('error', t('messages.openAttachmentError'));
|
|
2227
|
+
}
|
|
2228
|
+
};
|
|
2229
|
+
|
|
2230
|
+
return (
|
|
2231
|
+
<Page>
|
|
2232
|
+
<PageHeader
|
|
2233
|
+
title={t('header.title')}
|
|
2234
|
+
description={t('header.description')}
|
|
2235
|
+
breadcrumbs={[
|
|
2236
|
+
{ label: t('breadcrumbs.home'), href: '/' },
|
|
2237
|
+
{ label: t('breadcrumbs.finance'), href: '/finance' },
|
|
2238
|
+
{ label: t('breadcrumbs.current') },
|
|
2239
|
+
]}
|
|
2240
|
+
actions={
|
|
2241
|
+
<>
|
|
2242
|
+
<NovoTituloSheet
|
|
2243
|
+
categorias={categorias}
|
|
2244
|
+
centrosCusto={centrosCusto}
|
|
2245
|
+
t={t}
|
|
2246
|
+
onCreated={async () => {
|
|
2247
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2248
|
+
}}
|
|
2249
|
+
onOptionsUpdated={refetchFinanceData}
|
|
2250
|
+
/>
|
|
2251
|
+
<EditarTituloSheet
|
|
2252
|
+
open={!!editingTitleId && !!editingTitle}
|
|
2253
|
+
onOpenChange={closeEditSheet}
|
|
2254
|
+
titulo={editingTitle}
|
|
2255
|
+
categorias={categorias}
|
|
2256
|
+
centrosCusto={centrosCusto}
|
|
2257
|
+
t={t}
|
|
2258
|
+
onUpdated={async () => {
|
|
2259
|
+
await Promise.all([refetchTitles(), refetchFinanceData()]);
|
|
2260
|
+
}}
|
|
2261
|
+
onOptionsUpdated={refetchFinanceData}
|
|
2262
|
+
/>
|
|
2263
|
+
</>
|
|
2264
|
+
}
|
|
2265
|
+
/>
|
|
1132
2266
|
|
|
1133
2267
|
<FilterBar
|
|
1134
2268
|
searchPlaceholder={t('filters.searchPlaceholder')}
|
|
@@ -1150,8 +2284,8 @@ export default function TitulosReceberPage() {
|
|
|
1150
2284
|
],
|
|
1151
2285
|
},
|
|
1152
2286
|
]}
|
|
1153
|
-
activeFilters={
|
|
1154
|
-
onClearFilters={() => setStatusFilter('')}
|
|
2287
|
+
activeFilters={normalizedStatusFilter ? 1 : 0}
|
|
2288
|
+
onClearFilters={() => setStatusFilter('all')}
|
|
1155
2289
|
/>
|
|
1156
2290
|
|
|
1157
2291
|
<div className="rounded-md border">
|
|
@@ -1171,7 +2305,7 @@ export default function TitulosReceberPage() {
|
|
|
1171
2305
|
</TableRow>
|
|
1172
2306
|
</TableHeader>
|
|
1173
2307
|
<TableBody>
|
|
1174
|
-
{
|
|
2308
|
+
{titulosReceber.map((titulo) => {
|
|
1175
2309
|
const cliente = getPessoaById(titulo.clienteId);
|
|
1176
2310
|
const canal =
|
|
1177
2311
|
canalBadge[titulo.canal as keyof typeof canalBadge] ||
|
|
@@ -1202,7 +2336,7 @@ export default function TitulosReceberPage() {
|
|
|
1202
2336
|
titulo.anexosDetalhes?.[0]?.id;
|
|
1203
2337
|
void handleOpenAttachment(firstAttachmentId);
|
|
1204
2338
|
}}
|
|
1205
|
-
aria-label=
|
|
2339
|
+
aria-label={t('table.actions.openAttachment')}
|
|
1206
2340
|
>
|
|
1207
2341
|
<Paperclip className="h-3 w-3" />
|
|
1208
2342
|
</Button>
|
|
@@ -1245,7 +2379,10 @@ export default function TitulosReceberPage() {
|
|
|
1245
2379
|
{t('table.actions.viewDetails')}
|
|
1246
2380
|
</Link>
|
|
1247
2381
|
</DropdownMenuItem>
|
|
1248
|
-
<DropdownMenuItem
|
|
2382
|
+
<DropdownMenuItem
|
|
2383
|
+
disabled={titulo.status !== 'rascunho'}
|
|
2384
|
+
onClick={() => setEditingTitleId(titulo.id)}
|
|
2385
|
+
>
|
|
1249
2386
|
<Edit className="mr-2 h-4 w-4" />
|
|
1250
2387
|
{t('table.actions.edit')}
|
|
1251
2388
|
</DropdownMenuItem>
|
|
@@ -1275,15 +2412,25 @@ export default function TitulosReceberPage() {
|
|
|
1275
2412
|
<div className="flex items-center justify-between">
|
|
1276
2413
|
<p className="text-sm text-muted-foreground">
|
|
1277
2414
|
{t('footer.showing', {
|
|
1278
|
-
filtered:
|
|
1279
|
-
total:
|
|
2415
|
+
filtered: titulosReceber.length,
|
|
2416
|
+
total: paginatedTitlesResponse?.total || 0,
|
|
1280
2417
|
})}
|
|
1281
2418
|
</p>
|
|
1282
2419
|
<div className="flex items-center gap-2">
|
|
1283
|
-
<Button
|
|
2420
|
+
<Button
|
|
2421
|
+
variant="outline"
|
|
2422
|
+
size="sm"
|
|
2423
|
+
disabled={!paginatedTitlesResponse?.prev || isFetchingTitles}
|
|
2424
|
+
onClick={() => setPage((current) => Math.max(1, current - 1))}
|
|
2425
|
+
>
|
|
1284
2426
|
{t('footer.previous')}
|
|
1285
2427
|
</Button>
|
|
1286
|
-
<Button
|
|
2428
|
+
<Button
|
|
2429
|
+
variant="outline"
|
|
2430
|
+
size="sm"
|
|
2431
|
+
disabled={!paginatedTitlesResponse?.next || isFetchingTitles}
|
|
2432
|
+
onClick={() => setPage((current) => current + 1)}
|
|
2433
|
+
>
|
|
1287
2434
|
{t('footer.next')}
|
|
1288
2435
|
</Button>
|
|
1289
2436
|
</div>
|