@hed-hog/contact 0.0.304 → 0.0.306
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/README.md +225 -17
- package/dist/person/dto/account.dto.d.ts +5 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -1
- package/dist/person/dto/account.dto.js +29 -0
- package/dist/person/dto/account.dto.js.map +1 -1
- package/dist/person/dto/import-preview.dto.d.ts +7 -0
- package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
- package/dist/person/dto/import-preview.dto.js +7 -0
- package/dist/person/dto/import-preview.dto.js.map +1 -0
- package/dist/person/dto/import.dto.d.ts +15 -0
- package/dist/person/dto/import.dto.d.ts.map +1 -0
- package/dist/person/dto/import.dto.js +51 -0
- package/dist/person/dto/import.dto.js.map +1 -0
- package/dist/person/person.controller.d.ts +14 -0
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +53 -0
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +19 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +481 -67
- package/dist/person/person.service.js.map +1 -1
- package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
- package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
- package/hedhog/data/route.yaml +6 -0
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
- package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
- package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
- package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
- package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
- package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
- package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +160 -35
- package/hedhog/frontend/messages/en.json +104 -2
- package/hedhog/frontend/messages/pt.json +111 -9
- package/package.json +4 -4
- package/src/person/dto/account.dto.ts +31 -0
- package/src/person/dto/import-preview.dto.ts +6 -0
- package/src/person/dto/import.dto.ts +61 -0
- package/src/person/person.controller.ts +74 -12
- 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
|
-
}, [
|
|
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
|
|
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
|
-
|
|
539
|
-
|
|
540
|
-
|
|
541
|
-
|
|
542
|
-
|
|
543
|
-
|
|
544
|
-
|
|
545
|
-
|
|
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 (
|
|
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
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
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"
|