@hed-hog/contact 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.
Files changed (44) hide show
  1. package/README.md +225 -17
  2. package/dist/person/dto/account.dto.d.ts +5 -0
  3. package/dist/person/dto/account.dto.d.ts.map +1 -1
  4. package/dist/person/dto/account.dto.js +29 -0
  5. package/dist/person/dto/account.dto.js.map +1 -1
  6. package/dist/person/dto/import-preview.dto.d.ts +7 -0
  7. package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
  8. package/dist/person/dto/import-preview.dto.js +7 -0
  9. package/dist/person/dto/import-preview.dto.js.map +1 -0
  10. package/dist/person/dto/import.dto.d.ts +15 -0
  11. package/dist/person/dto/import.dto.d.ts.map +1 -0
  12. package/dist/person/dto/import.dto.js +51 -0
  13. package/dist/person/dto/import.dto.js.map +1 -0
  14. package/dist/person/person.controller.d.ts +14 -0
  15. package/dist/person/person.controller.d.ts.map +1 -1
  16. package/dist/person/person.controller.js +53 -0
  17. package/dist/person/person.controller.js.map +1 -1
  18. package/dist/person/person.service.d.ts +19 -0
  19. package/dist/person/person.service.d.ts.map +1 -1
  20. package/dist/person/person.service.js +481 -67
  21. package/dist/person/person.service.js.map +1 -1
  22. package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
  23. package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
  24. package/hedhog/data/route.yaml +6 -0
  25. package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
  26. package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
  27. package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
  28. package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
  29. package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
  30. package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
  31. package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
  32. package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
  33. package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
  34. package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
  35. package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
  36. package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +160 -35
  37. package/hedhog/frontend/messages/en.json +104 -2
  38. package/hedhog/frontend/messages/pt.json +111 -9
  39. package/package.json +5 -5
  40. package/src/person/dto/account.dto.ts +31 -0
  41. package/src/person/dto/import-preview.dto.ts +6 -0
  42. package/src/person/dto/import.dto.ts +61 -0
  43. package/src/person/person.controller.ts +74 -12
  44. package/src/person/person.service.ts +615 -68
@@ -26,12 +26,16 @@ import {
26
26
  SelectValue,
27
27
  } from '@/components/ui/select';
28
28
  import { Textarea } from '@/components/ui/textarea';
29
+ import { useFormDraft } from '@/hooks/use-form-draft';
30
+ import { formatDateTime } from '@/lib/format-date';
29
31
  import { useApp } from '@hed-hog/next-app-provider';
30
32
  import { zodResolver } from '@hookform/resolvers/zod';
33
+ import { formatDistanceToNow } from 'date-fns';
34
+ import { enUS, ptBR } from 'date-fns/locale';
31
35
  import { Loader2 } from 'lucide-react';
32
36
  import { useTranslations } from 'next-intl';
33
- import { useEffect, useState } from 'react';
34
- import { useForm } from 'react-hook-form';
37
+ import { useEffect, useMemo, useState } from 'react';
38
+ import { useForm, useWatch } from 'react-hook-form';
35
39
  import { toast } from 'sonner';
36
40
  import { z } from 'zod';
37
41
 
@@ -50,6 +54,21 @@ const followupSchema = z.object({
50
54
  type InteractionFormValues = z.infer<typeof interactionSchema>;
51
55
  type FollowupFormValues = z.infer<typeof followupSchema>;
52
56
 
57
+ type InteractionDraftPayload = {
58
+ personId: number | null;
59
+ values: InteractionFormValues;
60
+ };
61
+
62
+ type FollowupDraftPayload = {
63
+ personId: number | null;
64
+ values: FollowupFormValues;
65
+ };
66
+
67
+ const PERSON_INTERACTION_DRAFT_STORAGE_KEY =
68
+ 'contact-person-interaction-dialog-draft';
69
+ const PERSON_FOLLOWUP_DRAFT_STORAGE_KEY =
70
+ 'contact-person-followup-dialog-draft';
71
+
53
72
  type PersonInteractionDialogProps = {
54
73
  open: boolean;
55
74
  person: Person | null;
@@ -74,7 +93,7 @@ export function PersonInteractionDialog({
74
93
  onSuccess,
75
94
  }: PersonInteractionDialogProps) {
76
95
  const t = useTranslations('contact.ContactPage');
77
- const { request } = useApp();
96
+ const { request, getSettingValue, currentLocaleCode } = useApp();
78
97
  const [isSubmitting, setIsSubmitting] = useState(false);
79
98
 
80
99
  const interactionForm = useForm<InteractionFormValues>({
@@ -87,12 +106,148 @@ export function PersonInteractionDialog({
87
106
  defaultValues: { next_action_at: '', notes: '' },
88
107
  });
89
108
 
109
+ const watchedInteractionValues = useWatch({
110
+ control: interactionForm.control,
111
+ });
112
+ const watchedFollowupValues = useWatch({
113
+ control: followupForm.control,
114
+ });
115
+
116
+ const {
117
+ clearDraft: clearInteractionDraft,
118
+ loadDraft: loadInteractionDraft,
119
+ hasDraft: hasInteractionDraft,
120
+ savedAt: interactionDraftSavedAt,
121
+ } = useFormDraft<InteractionDraftPayload>({
122
+ storageKey: PERSON_INTERACTION_DRAFT_STORAGE_KEY,
123
+ value: {
124
+ personId: person?.id ?? null,
125
+ values: {
126
+ type: watchedInteractionValues.type ?? 'call',
127
+ notes: watchedInteractionValues.notes ?? '',
128
+ },
129
+ },
130
+ hasData:
131
+ (watchedInteractionValues.notes ?? '').trim().length > 0 ||
132
+ (watchedInteractionValues.type ?? 'call') !== 'call',
133
+ enabled: open && mode === 'interaction',
134
+ });
135
+
136
+ const {
137
+ clearDraft: clearFollowupDraft,
138
+ loadDraft: loadFollowupDraft,
139
+ hasDraft: hasFollowupDraft,
140
+ savedAt: followupDraftSavedAt,
141
+ } = useFormDraft<FollowupDraftPayload>({
142
+ storageKey: PERSON_FOLLOWUP_DRAFT_STORAGE_KEY,
143
+ value: {
144
+ personId: person?.id ?? null,
145
+ values: {
146
+ next_action_at: watchedFollowupValues.next_action_at ?? '',
147
+ notes: watchedFollowupValues.notes ?? '',
148
+ },
149
+ },
150
+ hasData:
151
+ (watchedFollowupValues.next_action_at ?? '').trim().length > 0 ||
152
+ (watchedFollowupValues.notes ?? '').trim().length > 0,
153
+ enabled: open && mode === 'followup',
154
+ });
155
+
156
+ const interactionDraftStatus = useMemo(() => {
157
+ if (!hasInteractionDraft || !interactionDraftSavedAt) {
158
+ return null;
159
+ }
160
+
161
+ const savedDate = new Date(interactionDraftSavedAt);
162
+ if (Number.isNaN(savedDate.getTime())) {
163
+ return null;
164
+ }
165
+
166
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
167
+ const relativeLabel = formatDistanceToNow(savedDate, {
168
+ addSuffix: true,
169
+ locale,
170
+ });
171
+ const absoluteLabel = formatDateTime(
172
+ savedDate,
173
+ getSettingValue,
174
+ currentLocaleCode
175
+ );
176
+
177
+ return currentLocaleCode.startsWith('pt')
178
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
179
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
180
+ }, [
181
+ currentLocaleCode,
182
+ getSettingValue,
183
+ hasInteractionDraft,
184
+ interactionDraftSavedAt,
185
+ ]);
186
+
187
+ const followupDraftStatus = useMemo(() => {
188
+ if (!hasFollowupDraft || !followupDraftSavedAt) {
189
+ return null;
190
+ }
191
+
192
+ const savedDate = new Date(followupDraftSavedAt);
193
+ if (Number.isNaN(savedDate.getTime())) {
194
+ return null;
195
+ }
196
+
197
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
198
+ const relativeLabel = formatDistanceToNow(savedDate, {
199
+ addSuffix: true,
200
+ locale,
201
+ });
202
+ const absoluteLabel = formatDateTime(
203
+ savedDate,
204
+ getSettingValue,
205
+ currentLocaleCode
206
+ );
207
+
208
+ return currentLocaleCode.startsWith('pt')
209
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
210
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
211
+ }, [
212
+ currentLocaleCode,
213
+ followupDraftSavedAt,
214
+ getSettingValue,
215
+ hasFollowupDraft,
216
+ ]);
217
+
90
218
  useEffect(() => {
91
219
  if (!open) {
92
220
  interactionForm.reset({ type: 'call', notes: '' });
93
221
  followupForm.reset({ next_action_at: '', notes: '' });
222
+ return;
223
+ }
224
+
225
+ if (mode === 'interaction') {
226
+ const storedDraft = loadInteractionDraft();
227
+ interactionForm.reset(
228
+ storedDraft?.payload.personId === (person?.id ?? null)
229
+ ? storedDraft.payload.values
230
+ : { type: 'call', notes: '' }
231
+ );
232
+ }
233
+
234
+ if (mode === 'followup') {
235
+ const storedDraft = loadFollowupDraft();
236
+ followupForm.reset(
237
+ storedDraft?.payload.personId === (person?.id ?? null)
238
+ ? storedDraft.payload.values
239
+ : { next_action_at: '', notes: '' }
240
+ );
94
241
  }
95
- }, [open, interactionForm, followupForm]);
242
+ }, [
243
+ followupForm,
244
+ interactionForm,
245
+ loadFollowupDraft,
246
+ loadInteractionDraft,
247
+ mode,
248
+ open,
249
+ person?.id,
250
+ ]);
96
251
 
97
252
  const handleInteractionSubmit = async (values: InteractionFormValues) => {
98
253
  if (!person) return;
@@ -104,6 +259,7 @@ export function PersonInteractionDialog({
104
259
  data: values,
105
260
  });
106
261
  toast.success(t('interactionSuccess'));
262
+ clearInteractionDraft();
107
263
  onOpenChange(false);
108
264
  onSuccess();
109
265
  } catch {
@@ -123,6 +279,7 @@ export function PersonInteractionDialog({
123
279
  data: values,
124
280
  });
125
281
  toast.success(t('followupSuccess'));
282
+ clearFollowupDraft();
126
283
  onOpenChange(false);
127
284
  onSuccess();
128
285
  } catch {
@@ -195,6 +352,11 @@ export function PersonInteractionDialog({
195
352
  </FormItem>
196
353
  )}
197
354
  />
355
+ {interactionDraftStatus ? (
356
+ <p className="text-xs text-muted-foreground">
357
+ {interactionDraftStatus}
358
+ </p>
359
+ ) : null}
198
360
  <DialogFooter>
199
361
  <Button
200
362
  type="button"
@@ -249,6 +411,11 @@ export function PersonInteractionDialog({
249
411
  </FormItem>
250
412
  )}
251
413
  />
414
+ {followupDraftStatus ? (
415
+ <p className="text-xs text-muted-foreground">
416
+ {followupDraftStatus}
417
+ </p>
418
+ ) : null}
252
419
  <DialogFooter>
253
420
  <Button
254
421
  type="button"
@@ -57,6 +57,7 @@ import {
57
57
  Phone,
58
58
  Plus,
59
59
  Trash2,
60
+ Upload,
60
61
  User,
61
62
  UserCheck,
62
63
  Users,
@@ -68,6 +69,7 @@ import { toast } from 'sonner';
68
69
 
69
70
  import { DeletePersonDialog } from './_components/delete-person-dialog';
70
71
  import { PersonFormSheet } from './_components/person-form-sheet';
72
+ import { PersonImportSheet } from './_components/person-import-sheet';
71
73
  import { PersonInteractionDialog } from './_components/person-interaction-dialog';
72
74
  import type {
73
75
  ContactTypeOption,
@@ -337,6 +339,7 @@ export default function PeoplePage() {
337
339
  const [personToDelete, setPersonToDelete] = useState<Person | null>(null);
338
340
  const [isDeleting, setIsDeleting] = useState(false);
339
341
  const [viewMode, setViewMode] = useState<PersonViewMode>('table');
342
+ const [importSheetOpen, setImportSheetOpen] = useState(false);
340
343
  const [interactionDialogOpen, setInteractionDialogOpen] = useState(false);
341
344
  const [interactionMode, setInteractionMode] = useState<
342
345
  'interaction' | 'followup'
@@ -803,6 +806,12 @@ export default function PeoplePage() {
803
806
  : t('heroDescriptionIndividualOnly')
804
807
  }
805
808
  actions={[
809
+ {
810
+ label: t('importLeads'),
811
+ onClick: () => setImportSheetOpen(true),
812
+ icon: <Upload className="h-4 w-4" />,
813
+ variant: 'outline',
814
+ },
806
815
  {
807
816
  label: t('newPerson'),
808
817
  onClick: openCreateSheet,
@@ -1231,6 +1240,14 @@ export default function PeoplePage() {
1231
1240
  </>
1232
1241
  )}
1233
1242
 
1243
+ <PersonImportSheet
1244
+ open={importSheetOpen}
1245
+ onOpenChange={setImportSheetOpen}
1246
+ onSuccess={() => {
1247
+ void refetch();
1248
+ }}
1249
+ />
1250
+
1234
1251
  <DeletePersonDialog
1235
1252
  open={deleteDialogOpen}
1236
1253
  personName={personToDelete?.name}
@@ -51,9 +51,13 @@ import {
51
51
  SheetTitle,
52
52
  } from '@/components/ui/sheet';
53
53
  import { Textarea } from '@/components/ui/textarea';
54
+ import { useFormDraft } from '@/hooks/use-form-draft';
55
+ import { formatDateTime } from '@/lib/format-date';
54
56
  import { cn } from '@/lib/utils';
55
57
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
56
58
  import { zodResolver } from '@hookform/resolvers/zod';
59
+ import { formatDistanceToNow } from 'date-fns';
60
+ import { enUS, ptBR } from 'date-fns/locale';
57
61
  import {
58
62
  CheckCircle2,
59
63
  Download,
@@ -68,7 +72,7 @@ import {
68
72
  } from 'lucide-react';
69
73
  import { useTranslations } from 'next-intl';
70
74
  import { useEffect, useMemo, useState } from 'react';
71
- import { useFieldArray, useForm } from 'react-hook-form';
75
+ import { useFieldArray, useForm, useWatch } from 'react-hook-form';
72
76
  import { toast } from 'sonner';
73
77
  import { z } from 'zod';
74
78
 
@@ -265,6 +269,16 @@ const proposalFormSchema = z.object({
265
269
  type ProposalFormValues = z.infer<typeof proposalFormSchema>;
266
270
  type ProposalFormItemValues = z.infer<typeof proposalItemFormSchema>;
267
271
 
272
+ type ProposalDraftPayload = {
273
+ leadId: number;
274
+ proposalId: number | null;
275
+ mode: 'create' | 'edit';
276
+ values: ProposalFormValues;
277
+ };
278
+
279
+ const LEAD_PROPOSAL_DRAFT_STORAGE_KEY =
280
+ 'contact-pipeline-lead-proposal-form-draft';
281
+
268
282
  const CONTRACT_CATEGORY_OPTIONS: ProposalContractCategory[] = [
269
283
  'client',
270
284
  'supplier',
@@ -328,6 +342,20 @@ function createEmptyProposalItem(
328
342
  };
329
343
  }
330
344
 
345
+ function createDefaultProposalFormValues(): ProposalFormValues {
346
+ return {
347
+ code: '',
348
+ title: '',
349
+ validUntil: '',
350
+ contractCategory: 'client',
351
+ contractType: 'service_agreement',
352
+ billingModel: 'fixed_price',
353
+ summary: '',
354
+ notes: '',
355
+ items: [createEmptyProposalItem()],
356
+ };
357
+ }
358
+
331
359
  function toDateInputValue(value?: string | null) {
332
360
  if (!value) return '';
333
361
  const parsed = new Date(value);
@@ -402,7 +430,7 @@ export function LeadProposalsTab({
402
430
  onLeadUpdated,
403
431
  }: LeadProposalsTabProps) {
404
432
  const t = useTranslations('contact.CrmPipeline');
405
- const { request, currentLocaleCode } = useApp();
433
+ const { request, currentLocaleCode, getSettingValue } = useApp();
406
434
  const locale = currentLocaleCode?.startsWith('pt') ? 'pt-BR' : 'en-US';
407
435
 
408
436
  const [selectedProposalId, setSelectedProposalId] = useState<number | null>(
@@ -418,17 +446,7 @@ export function LeadProposalsTab({
418
446
 
419
447
  const form = useForm<ProposalFormValues>({
420
448
  resolver: zodResolver(proposalFormSchema),
421
- defaultValues: {
422
- code: '',
423
- title: '',
424
- validUntil: '',
425
- contractCategory: 'client',
426
- contractType: 'service_agreement',
427
- billingModel: 'fixed_price',
428
- summary: '',
429
- notes: '',
430
- items: [createEmptyProposalItem()],
431
- },
449
+ defaultValues: createDefaultProposalFormValues(),
432
450
  });
433
451
 
434
452
  const {
@@ -440,7 +458,89 @@ export function LeadProposalsTab({
440
458
  name: 'items',
441
459
  });
442
460
 
443
- const watchedItems = form.watch('items');
461
+ const watchedValues = useWatch({
462
+ control: form.control,
463
+ });
464
+ const watchedItems = useMemo(
465
+ () => watchedValues.items ?? [],
466
+ [watchedValues.items]
467
+ );
468
+
469
+ const {
470
+ clearDraft,
471
+ loadDraft,
472
+ hasDraft,
473
+ savedAt: draftSavedAt,
474
+ } = useFormDraft<ProposalDraftPayload>({
475
+ storageKey: LEAD_PROPOSAL_DRAFT_STORAGE_KEY,
476
+ value: {
477
+ leadId: lead.id,
478
+ proposalId: editingProposal?.id ?? null,
479
+ mode: editingProposal ? 'edit' : 'create',
480
+ values: {
481
+ code: watchedValues.code ?? '',
482
+ title: watchedValues.title ?? '',
483
+ validUntil: watchedValues.validUntil ?? '',
484
+ contractCategory: watchedValues.contractCategory ?? 'client',
485
+ contractType: watchedValues.contractType ?? 'service_agreement',
486
+ billingModel: watchedValues.billingModel ?? 'fixed_price',
487
+ summary: watchedValues.summary ?? '',
488
+ notes: watchedValues.notes ?? '',
489
+ items: watchedValues.items?.map((item) =>
490
+ createEmptyProposalItem(item)
491
+ ) ?? [createEmptyProposalItem()],
492
+ },
493
+ },
494
+ hasData:
495
+ (watchedValues.code ?? '').trim().length > 0 ||
496
+ (watchedValues.title ?? '').trim().length > 0 ||
497
+ (watchedValues.validUntil ?? '').trim().length > 0 ||
498
+ (watchedValues.summary ?? '').trim().length > 0 ||
499
+ (watchedValues.notes ?? '').trim().length > 0 ||
500
+ (watchedValues.contractCategory ?? 'client') !== 'client' ||
501
+ (watchedValues.contractType ?? 'service_agreement') !==
502
+ 'service_agreement' ||
503
+ (watchedValues.billingModel ?? 'fixed_price') !== 'fixed_price' ||
504
+ (watchedValues.items ?? []).some(
505
+ (item) =>
506
+ (item.name ?? '').trim().length > 0 ||
507
+ (item.description ?? '').trim().length > 0 ||
508
+ Number(item.quantity ?? 1) !== 1 ||
509
+ Number(item.unitAmount ?? 0) !== 0 ||
510
+ (item.recurrence ?? 'one_time') !== 'one_time' ||
511
+ (item.startDate ?? '').trim().length > 0 ||
512
+ (item.endDate ?? '').trim().length > 0 ||
513
+ (item.itemType ?? 'service') !== 'service'
514
+ ),
515
+ enabled: formOpen,
516
+ });
517
+
518
+ const draftStatusContent = useMemo(() => {
519
+ if (!hasDraft || !draftSavedAt) {
520
+ return null;
521
+ }
522
+
523
+ const savedDate = new Date(draftSavedAt);
524
+ if (Number.isNaN(savedDate.getTime())) {
525
+ return null;
526
+ }
527
+
528
+ const localeValue = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
529
+ const relativeLabel = formatDistanceToNow(savedDate, {
530
+ addSuffix: true,
531
+ locale: localeValue,
532
+ });
533
+ const absoluteLabel = formatDateTime(
534
+ savedDate,
535
+ getSettingValue,
536
+ currentLocaleCode
537
+ );
538
+
539
+ return currentLocaleCode.startsWith('pt')
540
+ ? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
541
+ : `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
542
+ }, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
543
+
444
544
  const pricingSummary = useMemo(() => {
445
545
  const subtotalCents = (watchedItems ?? []).reduce((sum, item) => {
446
546
  if (item?.itemType === 'discount') return sum;
@@ -534,36 +634,55 @@ export function LeadProposalsTab({
534
634
 
535
635
  useEffect(() => {
536
636
  if (!formOpen) {
637
+ form.reset(createDefaultProposalFormValues());
638
+ setEditingProposal(null);
639
+ return;
640
+ }
641
+
642
+ const storedDraft = loadDraft();
643
+
644
+ if (
645
+ !editingProposal &&
646
+ storedDraft?.payload.mode === 'create' &&
647
+ storedDraft.payload.leadId === lead.id
648
+ ) {
537
649
  form.reset({
538
- code: '',
539
- title: '',
540
- validUntil: '',
541
- contractCategory: 'client',
542
- contractType: 'service_agreement',
543
- billingModel: 'fixed_price',
544
- summary: '',
545
- notes: '',
546
- items: [createEmptyProposalItem()],
650
+ ...createDefaultProposalFormValues(),
651
+ ...storedDraft.payload.values,
652
+ items:
653
+ storedDraft.payload.values.items?.length > 0
654
+ ? storedDraft.payload.values.items.map((item) =>
655
+ createEmptyProposalItem(item)
656
+ )
657
+ : [createEmptyProposalItem()],
547
658
  });
548
- setEditingProposal(null);
549
659
  return;
550
660
  }
551
661
 
552
- if (!editingProposal) {
662
+ if (
663
+ editingProposal &&
664
+ storedDraft?.payload.mode === 'edit' &&
665
+ storedDraft.payload.leadId === lead.id &&
666
+ storedDraft.payload.proposalId === editingProposal.id
667
+ ) {
553
668
  form.reset({
554
- code: '',
555
- title: '',
556
- validUntil: '',
557
- contractCategory: 'client',
558
- contractType: 'service_agreement',
559
- billingModel: 'fixed_price',
560
- summary: '',
561
- notes: '',
562
- items: [createEmptyProposalItem()],
669
+ ...createDefaultProposalFormValues(),
670
+ ...storedDraft.payload.values,
671
+ items:
672
+ storedDraft.payload.values.items?.length > 0
673
+ ? storedDraft.payload.values.items.map((item) =>
674
+ createEmptyProposalItem(item)
675
+ )
676
+ : [createEmptyProposalItem()],
563
677
  });
564
678
  return;
565
679
  }
566
680
 
681
+ if (!editingProposal) {
682
+ form.reset(createDefaultProposalFormValues());
683
+ return;
684
+ }
685
+
567
686
  const currentRevision = getCurrentRevision(editingProposal);
568
687
  const revisionItems = currentRevision?.proposal_item?.length
569
688
  ? currentRevision.proposal_item.map((item) =>
@@ -589,7 +708,7 @@ export function LeadProposalsTab({
589
708
  notes: editingProposal.notes ?? '',
590
709
  items: revisionItems,
591
710
  });
592
- }, [editingProposal, form, formOpen]);
711
+ }, [editingProposal, form, formOpen, lead.id, loadDraft]);
593
712
 
594
713
  const selectedProposal = useMemo(() => {
595
714
  const matchingDetail =
@@ -818,6 +937,7 @@ export function LeadProposalsTab({
818
937
  await refreshAll(createdProposal.id);
819
938
  }
820
939
 
940
+ clearDraft();
821
941
  setFormOpen(false);
822
942
  } catch (error) {
823
943
  const message =
@@ -2077,6 +2197,11 @@ export function LeadProposalsTab({
2077
2197
  </div>
2078
2198
 
2079
2199
  <div className="shrink-0 border-t px-5 py-3">
2200
+ {draftStatusContent ? (
2201
+ <p className="mb-2 text-xs text-muted-foreground">
2202
+ {draftStatusContent}
2203
+ </p>
2204
+ ) : null}
2080
2205
  <div className="flex flex-col-reverse gap-2 sm:flex-row sm:justify-end">
2081
2206
  <Button
2082
2207
  type="button"