@hed-hog/finance 0.0.304 → 0.0.305
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/hedhog/frontend/app/_components/finance-entity-field-with-create.tsx.ejs +130 -465
- package/hedhog/frontend/app/_components/finance-layout.tsx.ejs +135 -131
- package/hedhog/frontend/app/accounts-payable/installments/page.tsx.ejs +270 -14
- package/hedhog/frontend/app/accounts-receivable/collections-default/page.tsx.ejs +175 -4
- package/hedhog/frontend/app/accounts-receivable/installments/page.tsx.ejs +271 -19
- package/hedhog/frontend/app/administration/categories/page.tsx.ejs +97 -11
- package/hedhog/frontend/app/administration/cost-centers/page.tsx.ejs +88 -8
- package/hedhog/frontend/app/administration/period-close/page.tsx.ejs +94 -8
- package/hedhog/frontend/app/cash-and-banks/bank-accounts/page.tsx.ejs +163 -42
- package/hedhog/frontend/app/cash-and-banks/bank-reconciliation/page.tsx.ejs +135 -57
- package/hedhog/frontend/app/cash-and-banks/statements/page.tsx.ejs +202 -49
- package/hedhog/frontend/app/cash-and-banks/transfers/page.tsx.ejs +107 -22
- package/hedhog/frontend/messages/en.json +174 -221
- package/hedhog/frontend/messages/pt.json +174 -221
- package/package.json +7 -7
- package/hedhog/frontend/app/_components/person-field-with-create.tsx.ejs +0 -734
|
@@ -1,131 +1,135 @@
|
|
|
1
|
-
'use client';
|
|
2
|
-
|
|
3
|
-
import {
|
|
4
|
-
Card,
|
|
5
|
-
CardContent,
|
|
6
|
-
CardDescription,
|
|
7
|
-
CardHeader,
|
|
8
|
-
CardTitle,
|
|
9
|
-
} from '@/components/ui/card';
|
|
10
|
-
import { cn } from '@/lib/utils';
|
|
11
|
-
import type { ReactNode } from 'react';
|
|
12
|
-
|
|
13
|
-
type FinancePageSectionProps = {
|
|
14
|
-
title?: ReactNode;
|
|
15
|
-
description?: ReactNode;
|
|
16
|
-
actions?: ReactNode;
|
|
17
|
-
children: ReactNode;
|
|
18
|
-
className?: string;
|
|
19
|
-
contentClassName?: string;
|
|
20
|
-
variant?: 'card' | 'flat';
|
|
21
|
-
};
|
|
22
|
-
|
|
23
|
-
export function FinancePageSection({
|
|
24
|
-
title,
|
|
25
|
-
description,
|
|
26
|
-
actions,
|
|
27
|
-
children,
|
|
28
|
-
className,
|
|
29
|
-
contentClassName,
|
|
30
|
-
variant = 'card',
|
|
31
|
-
}: FinancePageSectionProps) {
|
|
32
|
-
const hasHeader = title || description || actions;
|
|
33
|
-
|
|
34
|
-
if (variant === 'flat') {
|
|
35
|
-
return (
|
|
36
|
-
<section className={cn('space-y-4', className)}>
|
|
37
|
-
{hasHeader ? (
|
|
38
|
-
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
39
|
-
<div className="space-y-1">
|
|
40
|
-
{title ? (
|
|
41
|
-
<h2 className="text-base font-semibold">{title}</h2>
|
|
42
|
-
) : null}
|
|
43
|
-
{description ? (
|
|
44
|
-
<p className="text-sm text-muted-foreground">{description}</p>
|
|
45
|
-
) : null}
|
|
46
|
-
</div>
|
|
47
|
-
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
48
|
-
</div>
|
|
49
|
-
) : null}
|
|
50
|
-
<div className={cn(contentClassName)}>{children}</div>
|
|
51
|
-
</section>
|
|
52
|
-
);
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
return (
|
|
56
|
-
<Card
|
|
57
|
-
className={cn(
|
|
58
|
-
'overflow-hidden border-border/60 py-0 shadow-sm',
|
|
59
|
-
className
|
|
60
|
-
)}
|
|
61
|
-
>
|
|
62
|
-
{hasHeader ? (
|
|
63
|
-
<CardHeader className="flex flex-col gap-3 border-b border-border/50 px-5 py-4 sm:flex-row sm:items-start sm:justify-between">
|
|
64
|
-
<div className="space-y-1">
|
|
65
|
-
{title ? (
|
|
66
|
-
<CardTitle className="text-base">{title}</CardTitle>
|
|
67
|
-
) : null}
|
|
68
|
-
{description ? (
|
|
69
|
-
<CardDescription>{description}</CardDescription>
|
|
70
|
-
) : null}
|
|
71
|
-
</div>
|
|
72
|
-
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
73
|
-
</CardHeader>
|
|
74
|
-
) : null}
|
|
75
|
-
<CardContent className={cn('p-0', contentClassName)}>
|
|
76
|
-
{children}
|
|
77
|
-
</CardContent>
|
|
78
|
-
</Card>
|
|
79
|
-
);
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
type FinanceSheetBodyProps = {
|
|
83
|
-
children: ReactNode;
|
|
84
|
-
className?: string;
|
|
85
|
-
};
|
|
86
|
-
|
|
87
|
-
export function FinanceSheetBody({
|
|
88
|
-
children,
|
|
89
|
-
className,
|
|
90
|
-
}: FinanceSheetBodyProps) {
|
|
91
|
-
return (
|
|
92
|
-
<div
|
|
93
|
-
className={cn(
|
|
94
|
-
'flex-1 space-y-5 overflow-y-auto px-4 py-4 sm:px-6 sm:py-5',
|
|
95
|
-
className
|
|
96
|
-
)}
|
|
97
|
-
>
|
|
98
|
-
{children}
|
|
99
|
-
</div>
|
|
100
|
-
);
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
type FinanceSheetSectionProps = {
|
|
104
|
-
title
|
|
105
|
-
description?: ReactNode;
|
|
106
|
-
children: ReactNode;
|
|
107
|
-
className?: string;
|
|
108
|
-
contentClassName?: string;
|
|
109
|
-
};
|
|
110
|
-
|
|
111
|
-
export function FinanceSheetSection({
|
|
112
|
-
title,
|
|
113
|
-
description,
|
|
114
|
-
children,
|
|
115
|
-
className,
|
|
116
|
-
contentClassName,
|
|
117
|
-
}: FinanceSheetSectionProps) {
|
|
118
|
-
return (
|
|
119
|
-
<section className={cn('', className)}>
|
|
120
|
-
|
|
121
|
-
<
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
131
|
-
}
|
|
1
|
+
'use client';
|
|
2
|
+
|
|
3
|
+
import {
|
|
4
|
+
Card,
|
|
5
|
+
CardContent,
|
|
6
|
+
CardDescription,
|
|
7
|
+
CardHeader,
|
|
8
|
+
CardTitle,
|
|
9
|
+
} from '@/components/ui/card';
|
|
10
|
+
import { cn } from '@/lib/utils';
|
|
11
|
+
import type { ReactNode } from 'react';
|
|
12
|
+
|
|
13
|
+
type FinancePageSectionProps = {
|
|
14
|
+
title?: ReactNode;
|
|
15
|
+
description?: ReactNode;
|
|
16
|
+
actions?: ReactNode;
|
|
17
|
+
children: ReactNode;
|
|
18
|
+
className?: string;
|
|
19
|
+
contentClassName?: string;
|
|
20
|
+
variant?: 'card' | 'flat';
|
|
21
|
+
};
|
|
22
|
+
|
|
23
|
+
export function FinancePageSection({
|
|
24
|
+
title,
|
|
25
|
+
description,
|
|
26
|
+
actions,
|
|
27
|
+
children,
|
|
28
|
+
className,
|
|
29
|
+
contentClassName,
|
|
30
|
+
variant = 'card',
|
|
31
|
+
}: FinancePageSectionProps) {
|
|
32
|
+
const hasHeader = title || description || actions;
|
|
33
|
+
|
|
34
|
+
if (variant === 'flat') {
|
|
35
|
+
return (
|
|
36
|
+
<section className={cn('space-y-4', className)}>
|
|
37
|
+
{hasHeader ? (
|
|
38
|
+
<div className="flex flex-col gap-3 sm:flex-row sm:items-start sm:justify-between">
|
|
39
|
+
<div className="space-y-1">
|
|
40
|
+
{title ? (
|
|
41
|
+
<h2 className="text-base font-semibold">{title}</h2>
|
|
42
|
+
) : null}
|
|
43
|
+
{description ? (
|
|
44
|
+
<p className="text-sm text-muted-foreground">{description}</p>
|
|
45
|
+
) : null}
|
|
46
|
+
</div>
|
|
47
|
+
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
48
|
+
</div>
|
|
49
|
+
) : null}
|
|
50
|
+
<div className={cn(contentClassName)}>{children}</div>
|
|
51
|
+
</section>
|
|
52
|
+
);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
return (
|
|
56
|
+
<Card
|
|
57
|
+
className={cn(
|
|
58
|
+
'overflow-hidden border-border/60 py-0 shadow-sm',
|
|
59
|
+
className
|
|
60
|
+
)}
|
|
61
|
+
>
|
|
62
|
+
{hasHeader ? (
|
|
63
|
+
<CardHeader className="flex flex-col gap-3 border-b border-border/50 px-5 py-4 sm:flex-row sm:items-start sm:justify-between">
|
|
64
|
+
<div className="space-y-1">
|
|
65
|
+
{title ? (
|
|
66
|
+
<CardTitle className="text-base">{title}</CardTitle>
|
|
67
|
+
) : null}
|
|
68
|
+
{description ? (
|
|
69
|
+
<CardDescription>{description}</CardDescription>
|
|
70
|
+
) : null}
|
|
71
|
+
</div>
|
|
72
|
+
{actions ? <div className="shrink-0">{actions}</div> : null}
|
|
73
|
+
</CardHeader>
|
|
74
|
+
) : null}
|
|
75
|
+
<CardContent className={cn('p-0', contentClassName)}>
|
|
76
|
+
{children}
|
|
77
|
+
</CardContent>
|
|
78
|
+
</Card>
|
|
79
|
+
);
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
type FinanceSheetBodyProps = {
|
|
83
|
+
children: ReactNode;
|
|
84
|
+
className?: string;
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
export function FinanceSheetBody({
|
|
88
|
+
children,
|
|
89
|
+
className,
|
|
90
|
+
}: FinanceSheetBodyProps) {
|
|
91
|
+
return (
|
|
92
|
+
<div
|
|
93
|
+
className={cn(
|
|
94
|
+
'flex-1 space-y-5 overflow-y-auto px-4 py-4 sm:px-6 sm:py-5',
|
|
95
|
+
className
|
|
96
|
+
)}
|
|
97
|
+
>
|
|
98
|
+
{children}
|
|
99
|
+
</div>
|
|
100
|
+
);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
type FinanceSheetSectionProps = {
|
|
104
|
+
title?: ReactNode;
|
|
105
|
+
description?: ReactNode;
|
|
106
|
+
children: ReactNode;
|
|
107
|
+
className?: string;
|
|
108
|
+
contentClassName?: string;
|
|
109
|
+
};
|
|
110
|
+
|
|
111
|
+
export function FinanceSheetSection({
|
|
112
|
+
title,
|
|
113
|
+
description,
|
|
114
|
+
children,
|
|
115
|
+
className,
|
|
116
|
+
contentClassName,
|
|
117
|
+
}: FinanceSheetSectionProps) {
|
|
118
|
+
return (
|
|
119
|
+
<section className={cn('', className)}>
|
|
120
|
+
{title || description ? (
|
|
121
|
+
<div className="mb-4 space-y-1">
|
|
122
|
+
{title ? (
|
|
123
|
+
<h3 className="text-sm font-semibold text-foreground">{title}</h3>
|
|
124
|
+
) : null}
|
|
125
|
+
{description ? (
|
|
126
|
+
<p className="text-sm leading-relaxed text-muted-foreground">
|
|
127
|
+
{description}
|
|
128
|
+
</p>
|
|
129
|
+
) : null}
|
|
130
|
+
</div>
|
|
131
|
+
) : null}
|
|
132
|
+
<div className={cn('space-y-4', contentClassName)}>{children}</div>
|
|
133
|
+
</section>
|
|
134
|
+
);
|
|
135
|
+
}
|
|
@@ -64,8 +64,12 @@ import {
|
|
|
64
64
|
TooltipContent,
|
|
65
65
|
TooltipTrigger,
|
|
66
66
|
} from '@/components/ui/tooltip';
|
|
67
|
+
import { useFormDraft } from '@/hooks/use-form-draft';
|
|
68
|
+
import { formatDateTime } from '@/lib/format-date';
|
|
67
69
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
68
70
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
71
|
+
import { formatDistanceToNow } from 'date-fns';
|
|
72
|
+
import { enUS, ptBR } from 'date-fns/locale';
|
|
69
73
|
import {
|
|
70
74
|
AlertTriangle,
|
|
71
75
|
FileText,
|
|
@@ -80,7 +84,7 @@ import { useTranslations } from 'next-intl';
|
|
|
80
84
|
import Link from 'next/link';
|
|
81
85
|
import { usePathname, useRouter, useSearchParams } from 'next/navigation';
|
|
82
86
|
import { useEffect, useMemo, useRef, useState } from 'react';
|
|
83
|
-
import { useFieldArray, useForm } from 'react-hook-form';
|
|
87
|
+
import { useFieldArray, useForm, useWatch } from 'react-hook-form';
|
|
84
88
|
import { z } from 'zod';
|
|
85
89
|
import { formatarData } from '../../_lib/formatters';
|
|
86
90
|
import {
|
|
@@ -246,6 +250,21 @@ const getNewTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
|
|
|
246
250
|
|
|
247
251
|
type NewTitleFormValues = z.infer<ReturnType<typeof getNewTitleFormSchema>>;
|
|
248
252
|
|
|
253
|
+
type PayableInstallmentDraftPayload = {
|
|
254
|
+
mode: 'create' | 'edit';
|
|
255
|
+
titleId: string | null;
|
|
256
|
+
values: NewTitleFormValues;
|
|
257
|
+
uploadedFileId: number | null;
|
|
258
|
+
uploadedFileName: string;
|
|
259
|
+
isInstallmentsEdited: boolean;
|
|
260
|
+
autoRedistributeInstallments: boolean;
|
|
261
|
+
};
|
|
262
|
+
|
|
263
|
+
const PAYABLE_NEW_TITLE_DRAFT_STORAGE_KEY =
|
|
264
|
+
'finance-accounts-payable-installments-new-draft';
|
|
265
|
+
const PAYABLE_EDIT_TITLE_DRAFT_STORAGE_KEY =
|
|
266
|
+
'finance-accounts-payable-installments-edit-draft';
|
|
267
|
+
|
|
249
268
|
const getSettleTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
|
|
250
269
|
z.object({
|
|
251
270
|
installmentId: z.string().min(1, t('validation.installmentRequired')),
|
|
@@ -276,7 +295,8 @@ function NovoTituloSheet({
|
|
|
276
295
|
open?: boolean;
|
|
277
296
|
onOpenChange?: (open: boolean) => void;
|
|
278
297
|
}) {
|
|
279
|
-
const { request, showToastHandler } =
|
|
298
|
+
const { request, showToastHandler, currentLocaleCode, getSettingValue } =
|
|
299
|
+
useApp();
|
|
280
300
|
const [internalOpen, setInternalOpen] = useState(false);
|
|
281
301
|
const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
|
|
282
302
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
@@ -345,13 +365,141 @@ function NovoTituloSheet({
|
|
|
345
365
|
name: 'installments',
|
|
346
366
|
});
|
|
347
367
|
|
|
368
|
+
const watchedFormValues = useWatch({
|
|
369
|
+
control: form.control,
|
|
370
|
+
});
|
|
371
|
+
|
|
372
|
+
const {
|
|
373
|
+
clearDraft,
|
|
374
|
+
loadDraft,
|
|
375
|
+
hasDraft,
|
|
376
|
+
savedAt: draftSavedAt,
|
|
377
|
+
} = useFormDraft<PayableInstallmentDraftPayload>({
|
|
378
|
+
storageKey: PAYABLE_NEW_TITLE_DRAFT_STORAGE_KEY,
|
|
379
|
+
value: {
|
|
380
|
+
mode: 'create',
|
|
381
|
+
titleId: null,
|
|
382
|
+
values: {
|
|
383
|
+
documento: watchedFormValues.documento ?? '',
|
|
384
|
+
fornecedorId: watchedFormValues.fornecedorId ?? '',
|
|
385
|
+
competencia: watchedFormValues.competencia ?? '',
|
|
386
|
+
vencimento: watchedFormValues.vencimento ?? '',
|
|
387
|
+
valor: watchedFormValues.valor ?? 0,
|
|
388
|
+
installmentsCount: watchedFormValues.installmentsCount ?? 1,
|
|
389
|
+
installments: watchedFormValues.installments?.map((installment) => ({
|
|
390
|
+
dueDate: installment?.dueDate ?? '',
|
|
391
|
+
amount: Number(installment?.amount ?? 0),
|
|
392
|
+
})) ?? [{ dueDate: '', amount: 0 }],
|
|
393
|
+
categoriaId: watchedFormValues.categoriaId ?? '',
|
|
394
|
+
centroCustoId: watchedFormValues.centroCustoId ?? '',
|
|
395
|
+
metodo: watchedFormValues.metodo ?? '',
|
|
396
|
+
descricao: watchedFormValues.descricao ?? '',
|
|
397
|
+
},
|
|
398
|
+
uploadedFileId,
|
|
399
|
+
uploadedFileName,
|
|
400
|
+
isInstallmentsEdited,
|
|
401
|
+
autoRedistributeInstallments,
|
|
402
|
+
},
|
|
403
|
+
hasData: Boolean(
|
|
404
|
+
(watchedFormValues.documento ?? '').trim() ||
|
|
405
|
+
(watchedFormValues.fornecedorId ?? '').trim() ||
|
|
406
|
+
(watchedFormValues.competencia ?? '').trim() ||
|
|
407
|
+
(watchedFormValues.vencimento ?? '').trim() ||
|
|
408
|
+
(watchedFormValues.categoriaId ?? '').trim() ||
|
|
409
|
+
(watchedFormValues.centroCustoId ?? '').trim() ||
|
|
410
|
+
(watchedFormValues.metodo ?? '').trim() ||
|
|
411
|
+
(watchedFormValues.descricao ?? '').trim() ||
|
|
412
|
+
(watchedFormValues.valor ?? 0) > 0 ||
|
|
413
|
+
(watchedFormValues.installmentsCount ?? 1) !== 1 ||
|
|
414
|
+
(watchedFormValues.installments ?? []).some(
|
|
415
|
+
(installment) =>
|
|
416
|
+
(installment?.dueDate ?? '').trim() ||
|
|
417
|
+
Number(installment?.amount ?? 0) > 0
|
|
418
|
+
) ||
|
|
419
|
+
uploadedFileId != null ||
|
|
420
|
+
uploadedFileName.trim()
|
|
421
|
+
),
|
|
422
|
+
enabled: isOpen,
|
|
423
|
+
});
|
|
424
|
+
|
|
425
|
+
const draftStatusContent = useMemo(() => {
|
|
426
|
+
if (!hasDraft || !draftSavedAt) {
|
|
427
|
+
return null;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const savedDate = new Date(draftSavedAt);
|
|
431
|
+
if (Number.isNaN(savedDate.getTime())) {
|
|
432
|
+
return null;
|
|
433
|
+
}
|
|
434
|
+
|
|
435
|
+
const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
|
|
436
|
+
const relativeLabel = formatDistanceToNow(savedDate, {
|
|
437
|
+
addSuffix: true,
|
|
438
|
+
locale,
|
|
439
|
+
});
|
|
440
|
+
const absoluteLabel = formatDateTime(
|
|
441
|
+
savedDate,
|
|
442
|
+
getSettingValue,
|
|
443
|
+
currentLocaleCode
|
|
444
|
+
);
|
|
445
|
+
|
|
446
|
+
return currentLocaleCode.startsWith('pt')
|
|
447
|
+
? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
|
|
448
|
+
: `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
|
|
449
|
+
}, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
|
|
450
|
+
|
|
348
451
|
const watchedInstallmentsCount = form.watch('installmentsCount');
|
|
349
452
|
const watchedTotalValue = form.watch('valor');
|
|
350
453
|
const watchedDueDate = form.watch('vencimento');
|
|
351
454
|
const watchedInstallments = form.watch('installments');
|
|
352
455
|
|
|
353
456
|
useEffect(() => {
|
|
354
|
-
if (
|
|
457
|
+
if (!isOpen) {
|
|
458
|
+
return;
|
|
459
|
+
}
|
|
460
|
+
|
|
461
|
+
const storedDraft = loadDraft();
|
|
462
|
+
|
|
463
|
+
if (storedDraft?.payload.mode === 'create') {
|
|
464
|
+
form.reset(storedDraft.payload.values);
|
|
465
|
+
setUploadedFileId(storedDraft.payload.uploadedFileId ?? null);
|
|
466
|
+
setUploadedFileName(storedDraft.payload.uploadedFileName ?? '');
|
|
467
|
+
setExtractionConfidence(null);
|
|
468
|
+
setExtractionWarnings([]);
|
|
469
|
+
setUploadProgress(storedDraft.payload.uploadedFileId ? 100 : 0);
|
|
470
|
+
setIsInstallmentsEdited(
|
|
471
|
+
storedDraft.payload.isInstallmentsEdited ?? false
|
|
472
|
+
);
|
|
473
|
+
setAutoRedistributeInstallments(
|
|
474
|
+
storedDraft.payload.autoRedistributeInstallments ?? true
|
|
475
|
+
);
|
|
476
|
+
return;
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
form.reset({
|
|
480
|
+
documento: '',
|
|
481
|
+
fornecedorId: '',
|
|
482
|
+
competencia: '',
|
|
483
|
+
vencimento: '',
|
|
484
|
+
valor: 0,
|
|
485
|
+
installmentsCount: 1,
|
|
486
|
+
installments: [{ dueDate: '', amount: 0 }],
|
|
487
|
+
categoriaId: '',
|
|
488
|
+
centroCustoId: '',
|
|
489
|
+
metodo: '',
|
|
490
|
+
descricao: '',
|
|
491
|
+
});
|
|
492
|
+
setUploadedFileId(null);
|
|
493
|
+
setUploadedFileName('');
|
|
494
|
+
setExtractionConfidence(null);
|
|
495
|
+
setExtractionWarnings([]);
|
|
496
|
+
setUploadProgress(0);
|
|
497
|
+
setIsInstallmentsEdited(false);
|
|
498
|
+
setAutoRedistributeInstallments(true);
|
|
499
|
+
}, [form, isOpen, loadDraft]);
|
|
500
|
+
|
|
501
|
+
useEffect(() => {
|
|
502
|
+
if (isInstallmentsEdited || !isOpen) {
|
|
355
503
|
return;
|
|
356
504
|
}
|
|
357
505
|
|
|
@@ -364,6 +512,7 @@ function NovoTituloSheet({
|
|
|
364
512
|
);
|
|
365
513
|
}, [
|
|
366
514
|
isInstallmentsEdited,
|
|
515
|
+
isOpen,
|
|
367
516
|
replaceInstallments,
|
|
368
517
|
watchedDueDate,
|
|
369
518
|
watchedInstallmentsCount,
|
|
@@ -464,6 +613,7 @@ function NovoTituloSheet({
|
|
|
464
613
|
},
|
|
465
614
|
});
|
|
466
615
|
|
|
616
|
+
clearDraft();
|
|
467
617
|
await onCreated();
|
|
468
618
|
form.reset();
|
|
469
619
|
setUploadedFileId(null);
|
|
@@ -480,14 +630,6 @@ function NovoTituloSheet({
|
|
|
480
630
|
};
|
|
481
631
|
|
|
482
632
|
const handleCancel = () => {
|
|
483
|
-
form.reset();
|
|
484
|
-
setUploadedFileId(null);
|
|
485
|
-
setUploadedFileName('');
|
|
486
|
-
setExtractionConfidence(null);
|
|
487
|
-
setExtractionWarnings([]);
|
|
488
|
-
setUploadProgress(0);
|
|
489
|
-
setIsInstallmentsEdited(false);
|
|
490
|
-
setAutoRedistributeInstallments(true);
|
|
491
633
|
handleOpenChange(false);
|
|
492
634
|
};
|
|
493
635
|
|
|
@@ -1158,6 +1300,11 @@ function NovoTituloSheet({
|
|
|
1158
1300
|
</FinanceSheetBody>
|
|
1159
1301
|
|
|
1160
1302
|
<div className="border-t border-border/50 px-4 py-4 sm:px-6">
|
|
1303
|
+
{draftStatusContent ? (
|
|
1304
|
+
<p className="mb-2 text-xs text-muted-foreground">
|
|
1305
|
+
{draftStatusContent}
|
|
1306
|
+
</p>
|
|
1307
|
+
) : null}
|
|
1161
1308
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
1162
1309
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
1163
1310
|
{t('common.cancel')}
|
|
@@ -1209,7 +1356,8 @@ function EditarTituloSheet({
|
|
|
1209
1356
|
onCategoriesUpdated?: () => Promise<any> | void;
|
|
1210
1357
|
onCostCentersUpdated?: () => Promise<any> | void;
|
|
1211
1358
|
}) {
|
|
1212
|
-
const { request, showToastHandler } =
|
|
1359
|
+
const { request, showToastHandler, currentLocaleCode, getSettingValue } =
|
|
1360
|
+
useApp();
|
|
1213
1361
|
const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
|
|
1214
1362
|
const [uploadedFileName, setUploadedFileName] = useState('');
|
|
1215
1363
|
const [isUploadingFile, setIsUploadingFile] = useState(false);
|
|
@@ -1270,6 +1418,89 @@ function EditarTituloSheet({
|
|
|
1270
1418
|
name: 'installments',
|
|
1271
1419
|
});
|
|
1272
1420
|
|
|
1421
|
+
const watchedFormValues = useWatch({
|
|
1422
|
+
control: form.control,
|
|
1423
|
+
});
|
|
1424
|
+
|
|
1425
|
+
const {
|
|
1426
|
+
clearDraft,
|
|
1427
|
+
loadDraft,
|
|
1428
|
+
hasDraft,
|
|
1429
|
+
savedAt: draftSavedAt,
|
|
1430
|
+
} = useFormDraft<PayableInstallmentDraftPayload>({
|
|
1431
|
+
storageKey: PAYABLE_EDIT_TITLE_DRAFT_STORAGE_KEY,
|
|
1432
|
+
value: {
|
|
1433
|
+
mode: 'edit',
|
|
1434
|
+
titleId: titulo?.id ? String(titulo.id) : null,
|
|
1435
|
+
values: {
|
|
1436
|
+
documento: watchedFormValues.documento ?? '',
|
|
1437
|
+
fornecedorId: watchedFormValues.fornecedorId ?? '',
|
|
1438
|
+
competencia: watchedFormValues.competencia ?? '',
|
|
1439
|
+
vencimento: watchedFormValues.vencimento ?? '',
|
|
1440
|
+
valor: watchedFormValues.valor ?? 0,
|
|
1441
|
+
installmentsCount: watchedFormValues.installmentsCount ?? 1,
|
|
1442
|
+
installments: watchedFormValues.installments?.map((installment) => ({
|
|
1443
|
+
dueDate: installment?.dueDate ?? '',
|
|
1444
|
+
amount: Number(installment?.amount ?? 0),
|
|
1445
|
+
})) ?? [{ dueDate: '', amount: 0 }],
|
|
1446
|
+
categoriaId: watchedFormValues.categoriaId ?? '',
|
|
1447
|
+
centroCustoId: watchedFormValues.centroCustoId ?? '',
|
|
1448
|
+
metodo: watchedFormValues.metodo ?? '',
|
|
1449
|
+
descricao: watchedFormValues.descricao ?? '',
|
|
1450
|
+
},
|
|
1451
|
+
uploadedFileId,
|
|
1452
|
+
uploadedFileName,
|
|
1453
|
+
isInstallmentsEdited,
|
|
1454
|
+
autoRedistributeInstallments,
|
|
1455
|
+
},
|
|
1456
|
+
hasData: Boolean(
|
|
1457
|
+
titulo &&
|
|
1458
|
+
((watchedFormValues.documento ?? '').trim() ||
|
|
1459
|
+
(watchedFormValues.fornecedorId ?? '').trim() ||
|
|
1460
|
+
(watchedFormValues.competencia ?? '').trim() ||
|
|
1461
|
+
(watchedFormValues.vencimento ?? '').trim() ||
|
|
1462
|
+
(watchedFormValues.categoriaId ?? '').trim() ||
|
|
1463
|
+
(watchedFormValues.centroCustoId ?? '').trim() ||
|
|
1464
|
+
(watchedFormValues.metodo ?? '').trim() ||
|
|
1465
|
+
(watchedFormValues.descricao ?? '').trim() ||
|
|
1466
|
+
(watchedFormValues.valor ?? 0) > 0 ||
|
|
1467
|
+
(watchedFormValues.installments ?? []).some(
|
|
1468
|
+
(installment) =>
|
|
1469
|
+
(installment?.dueDate ?? '').trim() ||
|
|
1470
|
+
Number(installment?.amount ?? 0) > 0
|
|
1471
|
+
) ||
|
|
1472
|
+
uploadedFileId != null ||
|
|
1473
|
+
uploadedFileName.trim())
|
|
1474
|
+
),
|
|
1475
|
+
enabled: open,
|
|
1476
|
+
});
|
|
1477
|
+
|
|
1478
|
+
const draftStatusContent = useMemo(() => {
|
|
1479
|
+
if (!hasDraft || !draftSavedAt) {
|
|
1480
|
+
return null;
|
|
1481
|
+
}
|
|
1482
|
+
|
|
1483
|
+
const savedDate = new Date(draftSavedAt);
|
|
1484
|
+
if (Number.isNaN(savedDate.getTime())) {
|
|
1485
|
+
return null;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
|
|
1489
|
+
const relativeLabel = formatDistanceToNow(savedDate, {
|
|
1490
|
+
addSuffix: true,
|
|
1491
|
+
locale,
|
|
1492
|
+
});
|
|
1493
|
+
const absoluteLabel = formatDateTime(
|
|
1494
|
+
savedDate,
|
|
1495
|
+
getSettingValue,
|
|
1496
|
+
currentLocaleCode
|
|
1497
|
+
);
|
|
1498
|
+
|
|
1499
|
+
return currentLocaleCode.startsWith('pt')
|
|
1500
|
+
? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
|
|
1501
|
+
: `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
|
|
1502
|
+
}, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
|
|
1503
|
+
|
|
1273
1504
|
const watchedInstallmentsCount = form.watch('installmentsCount');
|
|
1274
1505
|
const watchedTotalValue = form.watch('valor');
|
|
1275
1506
|
const watchedDueDate = form.watch('vencimento');
|
|
@@ -1297,6 +1528,25 @@ function EditarTituloSheet({
|
|
|
1297
1528
|
return;
|
|
1298
1529
|
}
|
|
1299
1530
|
|
|
1531
|
+
const storedDraft = loadDraft();
|
|
1532
|
+
const shouldRestoreDraft =
|
|
1533
|
+
storedDraft?.payload.mode === 'edit' &&
|
|
1534
|
+
String(storedDraft.payload.titleId ?? '') === String(titulo.id ?? '');
|
|
1535
|
+
|
|
1536
|
+
if (shouldRestoreDraft) {
|
|
1537
|
+
form.reset(storedDraft.payload.values);
|
|
1538
|
+
setUploadedFileId(storedDraft.payload.uploadedFileId ?? null);
|
|
1539
|
+
setUploadedFileName(storedDraft.payload.uploadedFileName ?? '');
|
|
1540
|
+
setExtractionConfidence(null);
|
|
1541
|
+
setExtractionWarnings([]);
|
|
1542
|
+
setUploadProgress(storedDraft.payload.uploadedFileId ? 100 : 0);
|
|
1543
|
+
setIsInstallmentsEdited(storedDraft.payload.isInstallmentsEdited ?? true);
|
|
1544
|
+
setAutoRedistributeInstallments(
|
|
1545
|
+
storedDraft.payload.autoRedistributeInstallments ?? true
|
|
1546
|
+
);
|
|
1547
|
+
return;
|
|
1548
|
+
}
|
|
1549
|
+
|
|
1300
1550
|
const installments = Array.isArray(titulo.parcelas) ? titulo.parcelas : [];
|
|
1301
1551
|
const normalizedInstallments =
|
|
1302
1552
|
installments.length > 0
|
|
@@ -1359,9 +1609,9 @@ function EditarTituloSheet({
|
|
|
1359
1609
|
setExtractionConfidence(null);
|
|
1360
1610
|
setExtractionWarnings([]);
|
|
1361
1611
|
setUploadProgress(0);
|
|
1362
|
-
|
|
1612
|
+
setAutoRedistributeInstallments(true);
|
|
1363
1613
|
setIsInstallmentsEdited(true);
|
|
1364
|
-
}, [form, open, titulo]);
|
|
1614
|
+
}, [form, loadDraft, open, titulo]);
|
|
1365
1615
|
|
|
1366
1616
|
useEffect(() => {
|
|
1367
1617
|
if (isInstallmentsEdited || !open) {
|
|
@@ -1483,6 +1733,7 @@ function EditarTituloSheet({
|
|
|
1483
1733
|
},
|
|
1484
1734
|
});
|
|
1485
1735
|
|
|
1736
|
+
clearDraft();
|
|
1486
1737
|
await onUpdated();
|
|
1487
1738
|
showToastHandler?.('success', t('messages.updateSuccess'));
|
|
1488
1739
|
onOpenChange(false);
|
|
@@ -2160,6 +2411,11 @@ function EditarTituloSheet({
|
|
|
2160
2411
|
</FinanceSheetBody>
|
|
2161
2412
|
|
|
2162
2413
|
<div className="border-t border-border/50 px-4 py-4 sm:px-6">
|
|
2414
|
+
{draftStatusContent ? (
|
|
2415
|
+
<p className="mb-2 text-xs text-muted-foreground">
|
|
2416
|
+
{draftStatusContent}
|
|
2417
|
+
</p>
|
|
2418
|
+
) : null}
|
|
2163
2419
|
<div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
|
|
2164
2420
|
<Button type="button" variant="outline" onClick={handleCancel}>
|
|
2165
2421
|
{t('common.cancel')}
|