@hed-hog/finance 0.0.303 → 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.
@@ -37,12 +37,16 @@ import {
37
37
  TableRow,
38
38
  } from '@/components/ui/table';
39
39
  import { Textarea } from '@/components/ui/textarea';
40
+ import { useFormDraft } from '@/hooks/use-form-draft';
41
+ import { formatDateTime } from '@/lib/format-date';
40
42
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
41
43
  import { zodResolver } from '@hookform/resolvers/zod';
44
+ import { formatDistanceToNow } from 'date-fns';
45
+ import { enUS, ptBR } from 'date-fns/locale';
42
46
  import { AlertTriangle, FileText, Send } from 'lucide-react';
43
47
  import { useTranslations } from 'next-intl';
44
- import { useState } from 'react';
45
- import { useForm } from 'react-hook-form';
48
+ import { useEffect, useMemo, useState } from 'react';
49
+ import { useForm, useWatch } from 'react-hook-form';
46
50
  import {
47
51
  Bar,
48
52
  BarChart,
@@ -94,6 +98,21 @@ const registerAgreementSchema = z.object({
94
98
  notes: z.string().optional(),
95
99
  });
96
100
 
101
+ type SendCollectionDraftPayload = {
102
+ clienteId: string;
103
+ values: z.infer<typeof sendCollectionSchema>;
104
+ };
105
+
106
+ type RegisterAgreementDraftPayload = {
107
+ clienteId: string;
108
+ values: z.infer<typeof registerAgreementSchema>;
109
+ };
110
+
111
+ const SEND_COLLECTION_DRAFT_STORAGE_KEY =
112
+ 'finance-collections-default-send-draft';
113
+ const REGISTER_AGREEMENT_DRAFT_STORAGE_KEY =
114
+ 'finance-collections-default-agreement-draft';
115
+
97
116
  function HistoricoContatosSheet({
98
117
  cliente,
99
118
  historicoContatos,
@@ -159,7 +178,8 @@ function EnviarCobrancaDialog({
159
178
  onSuccess: () => Promise<unknown>;
160
179
  t: ReturnType<typeof useTranslations>;
161
180
  }) {
162
- const { request, showToastHandler } = useApp();
181
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
182
+ useApp();
163
183
  const [open, setOpen] = useState(false);
164
184
 
165
185
  const form = useForm<z.infer<typeof sendCollectionSchema>>({
@@ -169,6 +189,70 @@ function EnviarCobrancaDialog({
169
189
  },
170
190
  });
171
191
 
192
+ const watchedValues = useWatch({
193
+ control: form.control,
194
+ });
195
+
196
+ const {
197
+ clearDraft,
198
+ loadDraft,
199
+ hasDraft,
200
+ savedAt: draftSavedAt,
201
+ } = useFormDraft<SendCollectionDraftPayload>({
202
+ storageKey: SEND_COLLECTION_DRAFT_STORAGE_KEY,
203
+ value: {
204
+ clienteId,
205
+ values: {
206
+ message: watchedValues.message ?? t('send.defaultMessage'),
207
+ },
208
+ },
209
+ hasData:
210
+ (watchedValues.message ?? '').trim() !== t('send.defaultMessage').trim(),
211
+ enabled: open,
212
+ });
213
+
214
+ const draftStatusContent = useMemo(() => {
215
+ if (!hasDraft || !draftSavedAt) {
216
+ return null;
217
+ }
218
+
219
+ const savedDate = new Date(draftSavedAt);
220
+ if (Number.isNaN(savedDate.getTime())) {
221
+ return null;
222
+ }
223
+
224
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
225
+ const relativeLabel = formatDistanceToNow(savedDate, {
226
+ addSuffix: true,
227
+ locale,
228
+ });
229
+ const absoluteLabel = formatDateTime(
230
+ savedDate,
231
+ getSettingValue,
232
+ currentLocaleCode
233
+ );
234
+
235
+ return currentLocaleCode.startsWith('pt')
236
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
237
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
238
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
239
+
240
+ useEffect(() => {
241
+ if (!open) {
242
+ return;
243
+ }
244
+
245
+ const storedDraft = loadDraft();
246
+
247
+ form.reset(
248
+ storedDraft?.payload.clienteId === clienteId
249
+ ? storedDraft.payload.values
250
+ : {
251
+ message: t('send.defaultMessage'),
252
+ }
253
+ );
254
+ }, [clienteId, form, loadDraft, open, t]);
255
+
172
256
  const onSubmit = async (values: z.infer<typeof sendCollectionSchema>) => {
173
257
  try {
174
258
  await request({
@@ -179,6 +263,7 @@ function EnviarCobrancaDialog({
179
263
  },
180
264
  });
181
265
 
266
+ clearDraft();
182
267
  await onSuccess();
183
268
  showToastHandler?.('success', t('send.submit'));
184
269
  setOpen(false);
@@ -240,6 +325,11 @@ function EnviarCobrancaDialog({
240
325
  </FinanceSheetBody>
241
326
 
242
327
  <div className="border-t border-border/50 px-4 py-4 sm:px-6">
328
+ {draftStatusContent ? (
329
+ <p className="mb-2 text-xs text-muted-foreground">
330
+ {draftStatusContent}
331
+ </p>
332
+ ) : null}
243
333
  <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
244
334
  <Button
245
335
  type="button"
@@ -271,7 +361,8 @@ function RegistrarAcordoDialog({
271
361
  onSuccess: () => Promise<unknown>;
272
362
  t: ReturnType<typeof useTranslations>;
273
363
  }) {
274
- const { request, showToastHandler } = useApp();
364
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
365
+ useApp();
275
366
  const [open, setOpen] = useState(false);
276
367
 
277
368
  const form = useForm<z.infer<typeof registerAgreementSchema>>({
@@ -284,6 +375,80 @@ function RegistrarAcordoDialog({
284
375
  },
285
376
  });
286
377
 
378
+ const watchedValues = useWatch({
379
+ control: form.control,
380
+ });
381
+
382
+ const {
383
+ clearDraft,
384
+ loadDraft,
385
+ hasDraft,
386
+ savedAt: draftSavedAt,
387
+ } = useFormDraft<RegisterAgreementDraftPayload>({
388
+ storageKey: REGISTER_AGREEMENT_DRAFT_STORAGE_KEY,
389
+ value: {
390
+ clienteId,
391
+ values: {
392
+ amount: watchedValues.amount ?? 0,
393
+ installments: watchedValues.installments ?? 1,
394
+ first_due_date: watchedValues.first_due_date ?? '',
395
+ notes: watchedValues.notes ?? '',
396
+ },
397
+ },
398
+ hasData: Boolean(
399
+ (watchedValues.amount ?? 0) > 0 ||
400
+ (watchedValues.installments ?? 1) !== 1 ||
401
+ (watchedValues.first_due_date ?? '').trim() ||
402
+ (watchedValues.notes ?? '').trim()
403
+ ),
404
+ enabled: open,
405
+ });
406
+
407
+ const draftStatusContent = useMemo(() => {
408
+ if (!hasDraft || !draftSavedAt) {
409
+ return null;
410
+ }
411
+
412
+ const savedDate = new Date(draftSavedAt);
413
+ if (Number.isNaN(savedDate.getTime())) {
414
+ return null;
415
+ }
416
+
417
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
418
+ const relativeLabel = formatDistanceToNow(savedDate, {
419
+ addSuffix: true,
420
+ locale,
421
+ });
422
+ const absoluteLabel = formatDateTime(
423
+ savedDate,
424
+ getSettingValue,
425
+ currentLocaleCode
426
+ );
427
+
428
+ return currentLocaleCode.startsWith('pt')
429
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
430
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
431
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
432
+
433
+ useEffect(() => {
434
+ if (!open) {
435
+ return;
436
+ }
437
+
438
+ const storedDraft = loadDraft();
439
+
440
+ form.reset(
441
+ storedDraft?.payload.clienteId === clienteId
442
+ ? storedDraft.payload.values
443
+ : {
444
+ amount: 0,
445
+ installments: 1,
446
+ first_due_date: '',
447
+ notes: '',
448
+ }
449
+ );
450
+ }, [clienteId, form, loadDraft, open]);
451
+
287
452
  const onSubmit = async (values: z.infer<typeof registerAgreementSchema>) => {
288
453
  try {
289
454
  await request({
@@ -292,6 +457,7 @@ function RegistrarAcordoDialog({
292
457
  data: values,
293
458
  });
294
459
 
460
+ clearDraft();
295
461
  await onSuccess();
296
462
  showToastHandler?.('success', t('agreement.submit'));
297
463
  setOpen(false);
@@ -410,6 +576,11 @@ function RegistrarAcordoDialog({
410
576
  </FinanceSheetBody>
411
577
 
412
578
  <div className="border-t border-border/50 px-4 py-4 sm:px-6">
579
+ {draftStatusContent ? (
580
+ <p className="mb-2 text-xs text-muted-foreground">
581
+ {draftStatusContent}
582
+ </p>
583
+ ) : null}
413
584
  <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
414
585
  <Button
415
586
  type="button"
@@ -71,8 +71,12 @@ import {
71
71
  TooltipContent,
72
72
  TooltipTrigger,
73
73
  } from '@/components/ui/tooltip';
74
+ import { useFormDraft } from '@/hooks/use-form-draft';
75
+ import { formatDateTime } from '@/lib/format-date';
74
76
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
75
77
  import { zodResolver } from '@hookform/resolvers/zod';
78
+ import { formatDistanceToNow } from 'date-fns';
79
+ import { enUS, ptBR } from 'date-fns/locale';
76
80
  import {
77
81
  AlertTriangle,
78
82
  Download,
@@ -92,7 +96,7 @@ import { useTranslations } from 'next-intl';
92
96
  import Link from 'next/link';
93
97
  import { usePathname, useRouter, useSearchParams } from 'next/navigation';
94
98
  import { useEffect, useMemo, useRef, useState } from 'react';
95
- import { useFieldArray, useForm } from 'react-hook-form';
99
+ import { useFieldArray, useForm, useWatch } from 'react-hook-form';
96
100
  import { z } from 'zod';
97
101
  import { formatarData } from '../../_lib/formatters';
98
102
  import { useFinanceData } from '../../_lib/use-finance-data';
@@ -250,6 +254,21 @@ const getNewTitleFormSchema = (t: ReturnType<typeof useTranslations>) =>
250
254
 
251
255
  type NewTitleFormValues = z.infer<ReturnType<typeof getNewTitleFormSchema>>;
252
256
 
257
+ type ReceivableInstallmentDraftPayload = {
258
+ mode: 'create' | 'edit';
259
+ titleId: string | null;
260
+ values: NewTitleFormValues;
261
+ uploadedFileId: number | null;
262
+ uploadedFileName: string;
263
+ isInstallmentsEdited: boolean;
264
+ autoRedistributeInstallments: boolean;
265
+ };
266
+
267
+ const RECEIVABLE_NEW_TITLE_DRAFT_STORAGE_KEY =
268
+ 'finance-accounts-receivable-installments-new-draft';
269
+ const RECEIVABLE_EDIT_TITLE_DRAFT_STORAGE_KEY =
270
+ 'finance-accounts-receivable-installments-edit-draft';
271
+
253
272
  function NovoTituloSheet({
254
273
  categorias,
255
274
  centrosCusto,
@@ -263,7 +282,8 @@ function NovoTituloSheet({
263
282
  onCreated: () => Promise<any> | void;
264
283
  onOptionsUpdated?: () => Promise<any> | void;
265
284
  }) {
266
- const { request, showToastHandler } = useApp();
285
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
286
+ useApp();
267
287
  const [open, setOpen] = useState(false);
268
288
  const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
269
289
  const [uploadedFileName, setUploadedFileName] = useState('');
@@ -325,13 +345,141 @@ function NovoTituloSheet({
325
345
  name: 'installments',
326
346
  });
327
347
 
348
+ const watchedFormValues = useWatch({
349
+ control: form.control,
350
+ });
351
+
352
+ const {
353
+ clearDraft,
354
+ loadDraft,
355
+ hasDraft,
356
+ savedAt: draftSavedAt,
357
+ } = useFormDraft<ReceivableInstallmentDraftPayload>({
358
+ storageKey: RECEIVABLE_NEW_TITLE_DRAFT_STORAGE_KEY,
359
+ value: {
360
+ mode: 'create',
361
+ titleId: null,
362
+ values: {
363
+ documento: watchedFormValues.documento ?? '',
364
+ clienteId: watchedFormValues.clienteId ?? '',
365
+ competencia: watchedFormValues.competencia ?? '',
366
+ vencimento: watchedFormValues.vencimento ?? '',
367
+ valor: watchedFormValues.valor ?? 0,
368
+ installmentsCount: watchedFormValues.installmentsCount ?? 1,
369
+ installments: watchedFormValues.installments?.map((installment) => ({
370
+ dueDate: installment?.dueDate ?? '',
371
+ amount: Number(installment?.amount ?? 0),
372
+ })) ?? [{ dueDate: '', amount: 0 }],
373
+ categoriaId: watchedFormValues.categoriaId ?? '',
374
+ centroCustoId: watchedFormValues.centroCustoId ?? '',
375
+ canal: watchedFormValues.canal ?? '',
376
+ descricao: watchedFormValues.descricao ?? '',
377
+ },
378
+ uploadedFileId,
379
+ uploadedFileName,
380
+ isInstallmentsEdited,
381
+ autoRedistributeInstallments,
382
+ },
383
+ hasData: Boolean(
384
+ (watchedFormValues.documento ?? '').trim() ||
385
+ (watchedFormValues.clienteId ?? '').trim() ||
386
+ (watchedFormValues.competencia ?? '').trim() ||
387
+ (watchedFormValues.vencimento ?? '').trim() ||
388
+ (watchedFormValues.categoriaId ?? '').trim() ||
389
+ (watchedFormValues.centroCustoId ?? '').trim() ||
390
+ (watchedFormValues.canal ?? '').trim() ||
391
+ (watchedFormValues.descricao ?? '').trim() ||
392
+ (watchedFormValues.valor ?? 0) > 0 ||
393
+ (watchedFormValues.installmentsCount ?? 1) !== 1 ||
394
+ (watchedFormValues.installments ?? []).some(
395
+ (installment) =>
396
+ (installment?.dueDate ?? '').trim() ||
397
+ Number(installment?.amount ?? 0) > 0
398
+ ) ||
399
+ uploadedFileId != null ||
400
+ uploadedFileName.trim()
401
+ ),
402
+ enabled: open,
403
+ });
404
+
405
+ const draftStatusContent = useMemo(() => {
406
+ if (!hasDraft || !draftSavedAt) {
407
+ return null;
408
+ }
409
+
410
+ const savedDate = new Date(draftSavedAt);
411
+ if (Number.isNaN(savedDate.getTime())) {
412
+ return null;
413
+ }
414
+
415
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
416
+ const relativeLabel = formatDistanceToNow(savedDate, {
417
+ addSuffix: true,
418
+ locale,
419
+ });
420
+ const absoluteLabel = formatDateTime(
421
+ savedDate,
422
+ getSettingValue,
423
+ currentLocaleCode
424
+ );
425
+
426
+ return currentLocaleCode.startsWith('pt')
427
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
428
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
429
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
430
+
328
431
  const watchedInstallmentsCount = form.watch('installmentsCount');
329
432
  const watchedTotalValue = form.watch('valor');
330
433
  const watchedDueDate = form.watch('vencimento');
331
434
  const watchedInstallments = form.watch('installments');
332
435
 
333
436
  useEffect(() => {
334
- if (isInstallmentsEdited) {
437
+ if (!open) {
438
+ return;
439
+ }
440
+
441
+ const storedDraft = loadDraft();
442
+
443
+ if (storedDraft?.payload.mode === 'create') {
444
+ form.reset(storedDraft.payload.values);
445
+ setUploadedFileId(storedDraft.payload.uploadedFileId ?? null);
446
+ setUploadedFileName(storedDraft.payload.uploadedFileName ?? '');
447
+ setExtractionConfidence(null);
448
+ setExtractionWarnings([]);
449
+ setUploadProgress(storedDraft.payload.uploadedFileId ? 100 : 0);
450
+ setIsInstallmentsEdited(
451
+ storedDraft.payload.isInstallmentsEdited ?? false
452
+ );
453
+ setAutoRedistributeInstallments(
454
+ storedDraft.payload.autoRedistributeInstallments ?? true
455
+ );
456
+ return;
457
+ }
458
+
459
+ form.reset({
460
+ documento: '',
461
+ clienteId: '',
462
+ competencia: '',
463
+ vencimento: '',
464
+ valor: 0,
465
+ installmentsCount: 1,
466
+ installments: [{ dueDate: '', amount: 0 }],
467
+ categoriaId: '',
468
+ centroCustoId: '',
469
+ canal: '',
470
+ descricao: '',
471
+ });
472
+ setUploadedFileId(null);
473
+ setUploadedFileName('');
474
+ setExtractionConfidence(null);
475
+ setExtractionWarnings([]);
476
+ setUploadProgress(0);
477
+ setIsInstallmentsEdited(false);
478
+ setAutoRedistributeInstallments(true);
479
+ }, [form, loadDraft, open]);
480
+
481
+ useEffect(() => {
482
+ if (isInstallmentsEdited || !open) {
335
483
  return;
336
484
  }
337
485
 
@@ -344,6 +492,7 @@ function NovoTituloSheet({
344
492
  );
345
493
  }, [
346
494
  isInstallmentsEdited,
495
+ open,
347
496
  replaceInstallments,
348
497
  watchedDueDate,
349
498
  watchedInstallmentsCount,
@@ -444,6 +593,7 @@ function NovoTituloSheet({
444
593
  },
445
594
  });
446
595
 
596
+ clearDraft();
447
597
  await onCreated();
448
598
  form.reset();
449
599
  setUploadedFileId(null);
@@ -460,14 +610,6 @@ function NovoTituloSheet({
460
610
  };
461
611
 
462
612
  const handleCancel = () => {
463
- form.reset();
464
- setUploadedFileId(null);
465
- setUploadedFileName('');
466
- setExtractionConfidence(null);
467
- setExtractionWarnings([]);
468
- setUploadProgress(0);
469
- setIsInstallmentsEdited(false);
470
- setAutoRedistributeInstallments(true);
471
613
  setOpen(false);
472
614
  };
473
615
 
@@ -1130,12 +1272,13 @@ function NovoTituloSheet({
1130
1272
  </FinanceSheetBody>
1131
1273
 
1132
1274
  <div className="border-t border-border/50 px-4 py-4 sm:px-6">
1275
+ {draftStatusContent ? (
1276
+ <p className="mb-2 text-xs text-muted-foreground">
1277
+ {draftStatusContent}
1278
+ </p>
1279
+ ) : null}
1133
1280
  <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
1134
- <Button
1135
- type="button"
1136
- variant="outline"
1137
- onClick={() => setOpen(false)}
1138
- >
1281
+ <Button type="button" variant="outline" onClick={handleCancel}>
1139
1282
  {t('common.cancel')}
1140
1283
  </Button>
1141
1284
  <Button
@@ -1183,7 +1326,8 @@ function EditarTituloSheet({
1183
1326
  onUpdated: () => Promise<any> | void;
1184
1327
  onOptionsUpdated?: () => Promise<any> | void;
1185
1328
  }) {
1186
- const { request, showToastHandler } = useApp();
1329
+ const { request, showToastHandler, currentLocaleCode, getSettingValue } =
1330
+ useApp();
1187
1331
  const [uploadedFileId, setUploadedFileId] = useState<number | null>(null);
1188
1332
  const [uploadedFileName, setUploadedFileName] = useState('');
1189
1333
  const [isUploadingFile, setIsUploadingFile] = useState(false);
@@ -1244,6 +1388,89 @@ function EditarTituloSheet({
1244
1388
  name: 'installments',
1245
1389
  });
1246
1390
 
1391
+ const watchedFormValues = useWatch({
1392
+ control: form.control,
1393
+ });
1394
+
1395
+ const {
1396
+ clearDraft,
1397
+ loadDraft,
1398
+ hasDraft,
1399
+ savedAt: draftSavedAt,
1400
+ } = useFormDraft<ReceivableInstallmentDraftPayload>({
1401
+ storageKey: RECEIVABLE_EDIT_TITLE_DRAFT_STORAGE_KEY,
1402
+ value: {
1403
+ mode: 'edit',
1404
+ titleId: titulo?.id ? String(titulo.id) : null,
1405
+ values: {
1406
+ documento: watchedFormValues.documento ?? '',
1407
+ clienteId: watchedFormValues.clienteId ?? '',
1408
+ competencia: watchedFormValues.competencia ?? '',
1409
+ vencimento: watchedFormValues.vencimento ?? '',
1410
+ valor: watchedFormValues.valor ?? 0,
1411
+ installmentsCount: watchedFormValues.installmentsCount ?? 1,
1412
+ installments: watchedFormValues.installments?.map((installment) => ({
1413
+ dueDate: installment?.dueDate ?? '',
1414
+ amount: Number(installment?.amount ?? 0),
1415
+ })) ?? [{ dueDate: '', amount: 0 }],
1416
+ categoriaId: watchedFormValues.categoriaId ?? '',
1417
+ centroCustoId: watchedFormValues.centroCustoId ?? '',
1418
+ canal: watchedFormValues.canal ?? '',
1419
+ descricao: watchedFormValues.descricao ?? '',
1420
+ },
1421
+ uploadedFileId,
1422
+ uploadedFileName,
1423
+ isInstallmentsEdited,
1424
+ autoRedistributeInstallments,
1425
+ },
1426
+ hasData: Boolean(
1427
+ titulo &&
1428
+ ((watchedFormValues.documento ?? '').trim() ||
1429
+ (watchedFormValues.clienteId ?? '').trim() ||
1430
+ (watchedFormValues.competencia ?? '').trim() ||
1431
+ (watchedFormValues.vencimento ?? '').trim() ||
1432
+ (watchedFormValues.categoriaId ?? '').trim() ||
1433
+ (watchedFormValues.centroCustoId ?? '').trim() ||
1434
+ (watchedFormValues.canal ?? '').trim() ||
1435
+ (watchedFormValues.descricao ?? '').trim() ||
1436
+ (watchedFormValues.valor ?? 0) > 0 ||
1437
+ (watchedFormValues.installments ?? []).some(
1438
+ (installment) =>
1439
+ (installment?.dueDate ?? '').trim() ||
1440
+ Number(installment?.amount ?? 0) > 0
1441
+ ) ||
1442
+ uploadedFileId != null ||
1443
+ uploadedFileName.trim())
1444
+ ),
1445
+ enabled: open,
1446
+ });
1447
+
1448
+ const draftStatusContent = useMemo(() => {
1449
+ if (!hasDraft || !draftSavedAt) {
1450
+ return null;
1451
+ }
1452
+
1453
+ const savedDate = new Date(draftSavedAt);
1454
+ if (Number.isNaN(savedDate.getTime())) {
1455
+ return null;
1456
+ }
1457
+
1458
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
1459
+ const relativeLabel = formatDistanceToNow(savedDate, {
1460
+ addSuffix: true,
1461
+ locale,
1462
+ });
1463
+ const absoluteLabel = formatDateTime(
1464
+ savedDate,
1465
+ getSettingValue,
1466
+ currentLocaleCode
1467
+ );
1468
+
1469
+ return currentLocaleCode.startsWith('pt')
1470
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
1471
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
1472
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
1473
+
1247
1474
  const watchedInstallmentsCount = form.watch('installmentsCount');
1248
1475
  const watchedTotalValue = form.watch('valor');
1249
1476
  const watchedDueDate = form.watch('vencimento');
@@ -1271,6 +1498,25 @@ function EditarTituloSheet({
1271
1498
  return;
1272
1499
  }
1273
1500
 
1501
+ const storedDraft = loadDraft();
1502
+ const shouldRestoreDraft =
1503
+ storedDraft?.payload.mode === 'edit' &&
1504
+ String(storedDraft.payload.titleId ?? '') === String(titulo.id ?? '');
1505
+
1506
+ if (shouldRestoreDraft) {
1507
+ form.reset(storedDraft.payload.values);
1508
+ setUploadedFileId(storedDraft.payload.uploadedFileId ?? null);
1509
+ setUploadedFileName(storedDraft.payload.uploadedFileName ?? '');
1510
+ setExtractionConfidence(null);
1511
+ setExtractionWarnings([]);
1512
+ setUploadProgress(storedDraft.payload.uploadedFileId ? 100 : 0);
1513
+ setIsInstallmentsEdited(storedDraft.payload.isInstallmentsEdited ?? true);
1514
+ setAutoRedistributeInstallments(
1515
+ storedDraft.payload.autoRedistributeInstallments ?? true
1516
+ );
1517
+ return;
1518
+ }
1519
+
1274
1520
  const installments = Array.isArray(titulo.parcelas) ? titulo.parcelas : [];
1275
1521
  const normalizedInstallments =
1276
1522
  installments.length > 0
@@ -1333,9 +1579,9 @@ function EditarTituloSheet({
1333
1579
  setExtractionConfidence(null);
1334
1580
  setExtractionWarnings([]);
1335
1581
  setUploadProgress(0);
1336
-
1582
+ setAutoRedistributeInstallments(true);
1337
1583
  setIsInstallmentsEdited(true);
1338
- }, [form, open, titulo]);
1584
+ }, [form, loadDraft, open, titulo]);
1339
1585
 
1340
1586
  useEffect(() => {
1341
1587
  if (isInstallmentsEdited || !open) {
@@ -1620,6 +1866,7 @@ function EditarTituloSheet({
1620
1866
  },
1621
1867
  });
1622
1868
 
1869
+ clearDraft();
1623
1870
  await onUpdated();
1624
1871
  showToastHandler?.('success', t('messages.updateSuccess'));
1625
1872
  onOpenChange(false);
@@ -2127,6 +2374,11 @@ function EditarTituloSheet({
2127
2374
  </FinanceSheetBody>
2128
2375
 
2129
2376
  <div className="border-t border-border/50 px-4 py-4 sm:px-6">
2377
+ {draftStatusContent ? (
2378
+ <p className="mb-2 text-xs text-muted-foreground">
2379
+ {draftStatusContent}
2380
+ </p>
2381
+ ) : null}
2130
2382
  <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2131
2383
  <Button type="button" variant="outline" onClick={handleCancel}>
2132
2384
  {t('common.cancel')}