@hed-hog/contact 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.
- 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 +696 -82
- package/hedhog/frontend/messages/en.json +140 -2
- package/hedhog/frontend/messages/pt.json +147 -9
- package/package.json +5 -5
- 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}
|