@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.
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 +4 -4
  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
@@ -6,6 +6,15 @@ export type AccountLifecycleStage =
6
6
  | 'churned'
7
7
  | 'inactive';
8
8
 
9
+ export type AddressTypeValue =
10
+ | 'residential'
11
+ | 'commercial'
12
+ | 'correspondence'
13
+ | 'alternative'
14
+ | 'work'
15
+ | 'billing'
16
+ | 'shipping';
17
+
9
18
  export type UserOption = {
10
19
  id: number;
11
20
  name: string;
@@ -18,6 +27,41 @@ export type AccountStats = {
18
27
  prospects: number;
19
28
  };
20
29
 
30
+ export type AccountContact = {
31
+ id?: number;
32
+ value: string;
33
+ is_primary: boolean;
34
+ contact_type_id: number;
35
+ contact_type?: {
36
+ id: number;
37
+ code: string;
38
+ name?: string;
39
+ };
40
+ };
41
+
42
+ export type AccountAddress = {
43
+ id?: number;
44
+ line1: string;
45
+ line2?: string;
46
+ city: string;
47
+ state: string;
48
+ is_primary: boolean;
49
+ address_type: AddressTypeValue;
50
+ postal_code?: string;
51
+ country_code?: string;
52
+ };
53
+
54
+ export type AccountDocument = {
55
+ id?: number;
56
+ value: string;
57
+ document_type_id: number;
58
+ document_type?: {
59
+ id: number;
60
+ code: string;
61
+ name?: string;
62
+ };
63
+ };
64
+
21
65
  export type Account = {
22
66
  id: number;
23
67
  name: string;
@@ -36,6 +80,9 @@ export type Account = {
36
80
  state?: string | null;
37
81
  created_at: string;
38
82
  last_interaction_at?: string | null;
83
+ contact?: AccountContact[];
84
+ address?: AccountAddress[];
85
+ document?: AccountDocument[];
39
86
  };
40
87
 
41
88
  export type PaginatedResult<T> = {
@@ -62,4 +109,8 @@ export type AccountFormValues = {
62
109
  lifecycle_stage?: AccountLifecycleStage | null;
63
110
  city?: string | null;
64
111
  state?: string | null;
112
+ collaborator_person_ids?: number[];
113
+ contacts?: AccountContact[];
114
+ addresses?: AccountAddress[];
115
+ documents?: AccountDocument[];
65
116
  };
@@ -39,6 +39,7 @@ import {
39
39
  TableRow,
40
40
  } from '@/components/ui/table';
41
41
  import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
42
+ import { getFormDraftOwnerKey } from '@/hooks/use-form-draft';
42
43
  import { formatDate } from '@/lib/format-date';
43
44
  import { cn } from '@/lib/utils';
44
45
  import { useApp, useQuery } from '@hed-hog/next-app-provider';
@@ -61,10 +62,16 @@ import {
61
62
  Phone,
62
63
  Plus,
63
64
  Trash2,
65
+ Upload,
64
66
  } from 'lucide-react';
65
67
  import { useTranslations } from 'next-intl';
66
68
  import { type ReactNode, useEffect, useMemo, useState } from 'react';
67
69
  import { toast } from 'sonner';
70
+ import { PersonImportSheet } from '../person/_components/person-import-sheet';
71
+ import type {
72
+ ContactTypeOption,
73
+ DocumentTypeOption,
74
+ } from '../person/_components/person-types';
68
75
  import { AccountFormSheet } from './_components/account-form-sheet';
69
76
  import type {
70
77
  Account,
@@ -75,6 +82,7 @@ import type {
75
82
  } from './_components/account-types';
76
83
 
77
84
  const ACCOUNT_VIEW_STORAGE_KEY = 'contact-account-view-mode';
85
+ const ACCOUNT_FORM_DRAFT_STORAGE_KEY = 'contact-account-form-draft';
78
86
 
79
87
  type AccountViewMode = 'table' | 'cards';
80
88
 
@@ -150,7 +158,8 @@ function AccountInfoTile({
150
158
  export default function AccountsPage() {
151
159
  const t = useTranslations('contact.AccountsPage');
152
160
  const crmT = useTranslations('contact.CrmMenu');
153
- const { request, currentLocaleCode, getSettingValue } = useApp();
161
+ const { request, currentLocaleCode, getSettingValue, accessToken, user } =
162
+ useApp();
154
163
 
155
164
  const [sorting, setSorting] = useState<SortingState>([]);
156
165
  const [page, setPage] = useState(1);
@@ -166,6 +175,13 @@ export default function AccountsPage() {
166
175
  const [isDeleting, setIsDeleting] = useState(false);
167
176
  const [isSubmitting, setIsSubmitting] = useState(false);
168
177
  const [viewMode, setViewMode] = useState<AccountViewMode>('table');
178
+ const [importSheetOpen, setImportSheetOpen] = useState(false);
179
+ const [importAccountId, setImportAccountId] = useState<number | null>(null);
180
+
181
+ const openImportSheet = (account: Account) => {
182
+ setImportAccountId(account.id);
183
+ setImportSheetOpen(true);
184
+ };
169
185
 
170
186
  useEffect(() => {
171
187
  const timeout = setTimeout(() => {
@@ -188,6 +204,47 @@ export default function AccountsPage() {
188
204
  }
189
205
  }, []);
190
206
 
207
+ useEffect(() => {
208
+ const ownerKey = getFormDraftOwnerKey(
209
+ accessToken,
210
+ user?.id ?? (user === null ? null : undefined)
211
+ );
212
+
213
+ if (accessToken && !ownerKey) {
214
+ return;
215
+ }
216
+
217
+ try {
218
+ const rawDraft = window.localStorage.getItem(
219
+ ACCOUNT_FORM_DRAFT_STORAGE_KEY
220
+ );
221
+
222
+ if (!rawDraft) {
223
+ return;
224
+ }
225
+
226
+ const parsedDraft = JSON.parse(rawDraft) as {
227
+ mode?: string;
228
+ ownerKey?: string | null;
229
+ } | null;
230
+
231
+ if (accessToken) {
232
+ if (!parsedDraft?.ownerKey || parsedDraft.ownerKey !== ownerKey) {
233
+ window.localStorage.removeItem(ACCOUNT_FORM_DRAFT_STORAGE_KEY);
234
+ return;
235
+ }
236
+ } else if (parsedDraft?.ownerKey && parsedDraft.ownerKey !== ownerKey) {
237
+ window.localStorage.removeItem(ACCOUNT_FORM_DRAFT_STORAGE_KEY);
238
+ return;
239
+ }
240
+
241
+ setAccountToEdit(null);
242
+ setFormSheetOpen(true);
243
+ } catch {
244
+ // Ignore draft restore failures.
245
+ }
246
+ }, [accessToken, user]);
247
+
191
248
  const currentSort = sorting[0];
192
249
  const sortField = currentSort?.id === 'created_at' ? 'created_at' : 'name';
193
250
  const sortOrder = currentSort?.desc ? 'desc' : 'asc';
@@ -234,6 +291,32 @@ export default function AccountsPage() {
234
291
  placeholderData: (previous) => previous ?? [],
235
292
  });
236
293
 
294
+ const { data: contactTypes = [] } = useQuery<ContactTypeOption[]>({
295
+ queryKey: ['contact-account-contact-types', currentLocaleCode],
296
+ queryFn: async () => {
297
+ const response = await request<{ data: ContactTypeOption[] }>({
298
+ url: '/person-contact-type?pageSize=100',
299
+ method: 'GET',
300
+ });
301
+
302
+ return response.data.data || [];
303
+ },
304
+ placeholderData: (previous) => previous ?? [],
305
+ });
306
+
307
+ const { data: documentTypes = [] } = useQuery<DocumentTypeOption[]>({
308
+ queryKey: ['contact-account-document-types', currentLocaleCode],
309
+ queryFn: async () => {
310
+ const response = await request<{ data: DocumentTypeOption[] }>({
311
+ url: '/person-document-type?pageSize=100',
312
+ method: 'GET',
313
+ });
314
+
315
+ return response.data.data || [];
316
+ },
317
+ placeholderData: (previous) => previous ?? [],
318
+ });
319
+
237
320
  const {
238
321
  data: paginate = {
239
322
  data: [],
@@ -314,29 +397,89 @@ export default function AccountsPage() {
314
397
  setFormSheetOpen(true);
315
398
  };
316
399
 
317
- const handleFormSubmit = async (data: AccountFormValues) => {
400
+ const handleFormSubmit = async (
401
+ data: AccountFormValues,
402
+ accountId?: number | null
403
+ ): Promise<{ id: number | null }> => {
318
404
  try {
319
405
  setIsSubmitting(true);
320
406
 
321
- if (accountToEdit) {
322
- await request({
323
- url: `/person/accounts/${accountToEdit.id}`,
324
- method: 'PATCH',
325
- data,
326
- });
407
+ const targetAccountId = accountToEdit?.id ?? accountId ?? null;
408
+
409
+ if (targetAccountId) {
410
+ const response = await request<{ id?: number; data?: { id?: number } }>(
411
+ {
412
+ url: `/person/accounts/${targetAccountId}`,
413
+ method: 'PATCH',
414
+ data,
415
+ }
416
+ );
417
+
418
+ const savedId = Number(
419
+ response?.data?.id ?? response?.data?.data?.id ?? targetAccountId
420
+ );
421
+
327
422
  toast.success(t('editSuccess'));
328
- } else {
329
- await request({
330
- url: '/person/accounts',
331
- method: 'POST',
332
- data,
423
+
424
+ setAccountToEdit((previous) =>
425
+ previous
426
+ ? {
427
+ ...previous,
428
+ ...data,
429
+ id: savedId,
430
+ owner_user:
431
+ owners.find((owner) => owner.id === data.owner_user_id) ??
432
+ previous.owner_user ??
433
+ null,
434
+ contact: data.contacts ?? previous.contact ?? [],
435
+ address: data.addresses ?? previous.address ?? [],
436
+ document: data.documents ?? previous.document ?? [],
437
+ }
438
+ : previous
439
+ );
440
+
441
+ await Promise.all([refetchAccounts(), refetchStats()]);
442
+ return { id: savedId || null };
443
+ }
444
+
445
+ const response = await request<{ id?: number; data?: { id?: number } }>({
446
+ url: '/person/accounts',
447
+ method: 'POST',
448
+ data,
449
+ });
450
+
451
+ const savedId = Number(response?.data?.id ?? response?.data?.data?.id);
452
+
453
+ toast.success(t('createSuccess'));
454
+
455
+ if (savedId) {
456
+ setAccountToEdit({
457
+ id: savedId,
458
+ name: data.name,
459
+ trade_name: data.trade_name ?? null,
460
+ status: data.status,
461
+ industry: data.industry ?? null,
462
+ website: data.website ?? null,
463
+ email: data.email ?? null,
464
+ phone: data.phone ?? null,
465
+ owner_user_id: data.owner_user_id ?? null,
466
+ owner_user:
467
+ owners.find((owner) => owner.id === data.owner_user_id) ?? null,
468
+ annual_revenue: data.annual_revenue ?? null,
469
+ employee_count: data.employee_count ?? null,
470
+ lifecycle_stage: data.lifecycle_stage ?? null,
471
+ city: data.city ?? null,
472
+ state: data.state ?? null,
473
+ created_at: new Date().toISOString(),
474
+ last_interaction_at: null,
475
+ contact: data.contacts ?? [],
476
+ address: data.addresses ?? [],
477
+ document: data.documents ?? [],
333
478
  });
334
- toast.success(t('createSuccess'));
335
479
  }
336
480
 
337
- setFormSheetOpen(false);
338
- setAccountToEdit(null);
339
481
  await Promise.all([refetchAccounts(), refetchStats()]);
482
+ return { id: savedId || null };
340
483
  } catch (error: unknown) {
341
484
  const message =
342
485
  typeof error === 'object' && error && 'message' in error
@@ -537,6 +680,10 @@ export default function AccountsPage() {
537
680
  <Pencil className="mr-2 h-4 w-4" />
538
681
  {t('edit')}
539
682
  </DropdownMenuItem>
683
+ <DropdownMenuItem onClick={() => openImportSheet(account)}>
684
+ <Upload className="mr-2 h-4 w-4" />
685
+ {t('importContacts')}
686
+ </DropdownMenuItem>
540
687
  <DropdownMenuSeparator />
541
688
  <DropdownMenuItem
542
689
  className="text-red-600"
@@ -867,6 +1014,12 @@ export default function AccountsPage() {
867
1014
  <Pencil className="mr-2 h-4 w-4" />
868
1015
  {t('edit')}
869
1016
  </DropdownMenuItem>
1017
+ <DropdownMenuItem
1018
+ onClick={() => openImportSheet(account)}
1019
+ >
1020
+ <Upload className="mr-2 h-4 w-4" />
1021
+ {t('importContacts')}
1022
+ </DropdownMenuItem>
870
1023
  <DropdownMenuSeparator />
871
1024
  <DropdownMenuItem
872
1025
  className="text-red-600"
@@ -937,10 +1090,22 @@ export default function AccountsPage() {
937
1090
  onOpenChange={setFormSheetOpen}
938
1091
  account={accountToEdit}
939
1092
  owners={owners}
1093
+ contactTypes={contactTypes}
1094
+ documentTypes={documentTypes}
940
1095
  onSubmit={handleFormSubmit}
941
1096
  isLoading={isSubmitting}
942
1097
  />
943
1098
 
1099
+ <PersonImportSheet
1100
+ open={importSheetOpen}
1101
+ onOpenChange={(nextOpen) => {
1102
+ setImportSheetOpen(nextOpen);
1103
+ if (!nextOpen) setImportAccountId(null);
1104
+ }}
1105
+ initialCompanyId={importAccountId}
1106
+ onSuccess={() => setImportSheetOpen(false)}
1107
+ />
1108
+
944
1109
  <AlertDialog open={deleteDialogOpen} onOpenChange={setDeleteDialogOpen}>
945
1110
  <AlertDialogContent>
946
1111
  <AlertDialogHeader>