@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.
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 +696 -82
  37. package/hedhog/frontend/messages/en.json +140 -2
  38. package/hedhog/frontend/messages/pt.json +147 -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}