@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
@@ -1,11 +1,22 @@
1
1
  'use client';
2
2
 
3
+ import { PersonFormSheet } from '@/app/(app)/(libraries)/contact/person/_components/person-form-sheet';
4
+ import {
5
+ ADDRESS_TYPE_OPTIONS,
6
+ type ContactTypeOption,
7
+ type DocumentTypeOption,
8
+ type Person,
9
+ } from '@/app/(app)/(libraries)/contact/person/_components/person-types';
10
+ import { CopyButton } from '@/components/copy-button';
11
+ import { Avatar, AvatarFallback } from '@/components/ui/avatar';
12
+ import { Badge } from '@/components/ui/badge';
13
+ import { Button } from '@/components/ui/button';
3
14
  import {
4
15
  Collapsible,
5
16
  CollapsibleContent,
6
17
  CollapsibleTrigger,
7
18
  } from '@/components/ui/collapsible';
8
- import { FormActions } from '@/components/ui/form-actions';
19
+ import { EntityPicker } from '@/components/ui/entity-picker';
9
20
  import {
10
21
  Form,
11
22
  FormControl,
@@ -14,6 +25,7 @@ import {
14
25
  FormLabel,
15
26
  FormMessage,
16
27
  } from '@/components/ui/form';
28
+ import { FormActions } from '@/components/ui/form-actions';
17
29
  import { Input } from '@/components/ui/input';
18
30
  import {
19
31
  Select,
@@ -22,7 +34,6 @@ import {
22
34
  SelectTrigger,
23
35
  SelectValue,
24
36
  } from '@/components/ui/select';
25
- import { Separator } from '@/components/ui/separator';
26
37
  import {
27
38
  Sheet,
28
39
  SheetContent,
@@ -30,13 +41,61 @@ import {
30
41
  SheetHeader,
31
42
  SheetTitle,
32
43
  } from '@/components/ui/sheet';
44
+ import { ToggleGroup, ToggleGroupItem } from '@/components/ui/toggle-group';
45
+ import {
46
+ Tooltip,
47
+ TooltipContent,
48
+ TooltipTrigger,
49
+ } from '@/components/ui/tooltip';
50
+ import { useFormDraft } from '@/hooks/use-form-draft';
51
+ import { formatDateTime } from '@/lib/format-date';
52
+ import { cn } from '@/lib/utils';
53
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
33
54
  import { zodResolver } from '@hookform/resolvers/zod';
34
- import { Building2, ChevronDown, Globe, MapPin, Users } from 'lucide-react';
55
+ import { formatDistanceToNow } from 'date-fns';
56
+ import { enUS, ptBR } from 'date-fns/locale';
57
+ import {
58
+ Building2,
59
+ CheckCircle2,
60
+ ChevronDown,
61
+ ChevronUp,
62
+ FileText,
63
+ Globe,
64
+ LayoutGrid,
65
+ List,
66
+ Loader2,
67
+ Mail,
68
+ MapPin,
69
+ Pencil,
70
+ Phone,
71
+ Plus,
72
+ Star,
73
+ Trash2,
74
+ Users,
75
+ XCircle,
76
+ } from 'lucide-react';
35
77
  import { useTranslations } from 'next-intl';
36
- import { type ReactNode, useEffect, useMemo } from 'react';
78
+ import {
79
+ type ChangeEvent,
80
+ type ReactNode,
81
+ useCallback,
82
+ useEffect,
83
+ useMemo,
84
+ useRef,
85
+ useState,
86
+ } from 'react';
37
87
  import { useForm, useWatch } from 'react-hook-form';
38
88
  import { z } from 'zod';
39
- import type { Account, AccountFormValues, UserOption } from './account-types';
89
+ import type {
90
+ Account,
91
+ AccountAddress,
92
+ AccountContact,
93
+ AccountDocument,
94
+ AccountFormValues,
95
+ AccountLifecycleStage,
96
+ PaginatedResult,
97
+ UserOption,
98
+ } from './account-types';
40
99
 
41
100
  type AccountFormData = {
42
101
  name: string;
@@ -44,14 +103,24 @@ type AccountFormData = {
44
103
  status: 'active' | 'inactive';
45
104
  industry: string | null;
46
105
  website: string | null;
47
- email: string | null;
48
- phone: string | null;
49
106
  owner_user_id: number | null;
50
107
  annual_revenue: number | null;
51
108
  employee_count: number | null;
52
- lifecycle_stage: 'prospect' | 'customer' | 'churned' | 'inactive' | null;
53
- city: string | null;
54
- state: string | null;
109
+ lifecycle_stage: AccountLifecycleStage | null;
110
+ };
111
+
112
+ type EditableAccountContact = Omit<AccountContact, 'contact_type_id'> & {
113
+ clientId: string;
114
+ contact_type_id: number | null;
115
+ };
116
+
117
+ type EditableAccountAddress = AccountAddress & {
118
+ clientId: string;
119
+ };
120
+
121
+ type EditableAccountDocument = Omit<AccountDocument, 'document_type_id'> & {
122
+ clientId: string;
123
+ document_type_id: number | null;
55
124
  };
56
125
 
57
126
  interface AccountFormSheetProps {
@@ -59,7 +128,12 @@ interface AccountFormSheetProps {
59
128
  onOpenChange: (open: boolean) => void;
60
129
  account?: Account | null;
61
130
  owners: UserOption[];
62
- onSubmit: (data: AccountFormValues) => Promise<void>;
131
+ contactTypes: ContactTypeOption[];
132
+ documentTypes: DocumentTypeOption[];
133
+ onSubmit: (
134
+ data: AccountFormValues,
135
+ accountId?: number | null
136
+ ) => Promise<{ id: number | null }>;
63
137
  isLoading?: boolean;
64
138
  }
65
139
 
@@ -68,6 +142,86 @@ function emptyToNull(value: string | null | undefined) {
68
142
  return normalized ? normalized : null;
69
143
  }
70
144
 
145
+ function createClientId(prefix: string) {
146
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
147
+ }
148
+
149
+ function onlyDigits(value: string) {
150
+ return value.replace(/\D/g, '');
151
+ }
152
+
153
+ function applyPhoneMask(value: string) {
154
+ const digits = onlyDigits(value).slice(0, 11);
155
+
156
+ if (digits.length <= 2) {
157
+ return digits.length ? `(${digits}` : '';
158
+ }
159
+
160
+ if (digits.length <= 6) {
161
+ return `(${digits.slice(0, 2)}) ${digits.slice(2)}`;
162
+ }
163
+
164
+ if (digits.length <= 10) {
165
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
166
+ }
167
+
168
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
169
+ }
170
+
171
+ function applyCpfMask(value: string) {
172
+ const digits = onlyDigits(value).slice(0, 11);
173
+
174
+ if (digits.length <= 3) return digits;
175
+ if (digits.length <= 6) return `${digits.slice(0, 3)}.${digits.slice(3)}`;
176
+ if (digits.length <= 9) {
177
+ return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
178
+ }
179
+
180
+ return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`;
181
+ }
182
+
183
+ function applyCnpjMask(value: string) {
184
+ const digits = onlyDigits(value).slice(0, 14);
185
+
186
+ if (digits.length <= 2) return digits;
187
+ if (digits.length <= 5) return `${digits.slice(0, 2)}.${digits.slice(2)}`;
188
+ if (digits.length <= 8) {
189
+ return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5)}`;
190
+ }
191
+ if (digits.length <= 12) {
192
+ return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}/${digits.slice(8)}`;
193
+ }
194
+
195
+ return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}/${digits.slice(8, 12)}-${digits.slice(12)}`;
196
+ }
197
+
198
+ async function fetchViaCep(cep: string) {
199
+ try {
200
+ const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
201
+ if (!response.ok) return null;
202
+
203
+ const data = await response.json();
204
+ if (data.erro) return null;
205
+
206
+ return data as {
207
+ logradouro?: string;
208
+ localidade?: string;
209
+ uf?: string;
210
+ };
211
+ } catch {
212
+ return null;
213
+ }
214
+ }
215
+
216
+ function getPersonInitials(name: string) {
217
+ return name
218
+ .split(' ')
219
+ .filter(Boolean)
220
+ .slice(0, 2)
221
+ .map((part) => part[0]?.toUpperCase() || '')
222
+ .join('');
223
+ }
224
+
71
225
  function SectionHeader({
72
226
  title,
73
227
  description,
@@ -90,15 +244,74 @@ function SectionHeader({
90
244
  );
91
245
  }
92
246
 
247
+ const ACCOUNT_FORM_DRAFT_STORAGE_KEY = 'contact-account-form-draft';
248
+
249
+ type AccountDraftPayload = {
250
+ mode: 'create' | 'edit';
251
+ savedAccountId: number | null;
252
+ values: AccountFormData;
253
+ contacts: EditableAccountContact[];
254
+ addresses: EditableAccountAddress[];
255
+ documents: EditableAccountDocument[];
256
+ localCollaborators: Person[];
257
+ removedCollaboratorIds: number[];
258
+ };
259
+
93
260
  export function AccountFormSheet({
94
261
  open,
95
262
  onOpenChange,
96
263
  account,
97
264
  owners,
265
+ contactTypes,
266
+ documentTypes,
98
267
  onSubmit,
99
268
  isLoading = false,
100
269
  }: AccountFormSheetProps) {
101
270
  const t = useTranslations('contact.AccountsPage');
271
+ const personT = useTranslations('contact.ContactPage');
272
+ const { request, currentLocaleCode, getSettingValue } = useApp();
273
+
274
+ const [contacts, setContacts] = useState<EditableAccountContact[]>([]);
275
+ const [addresses, setAddresses] = useState<EditableAccountAddress[]>([]);
276
+ const [documents, setDocuments] = useState<EditableAccountDocument[]>([]);
277
+ const [contactsOpen, setContactsOpen] = useState(true);
278
+ const [addressesOpen, setAddressesOpen] = useState(true);
279
+ const [documentsOpen, setDocumentsOpen] = useState(true);
280
+ const [loadingCEP, setLoadingCEP] = useState<Record<string, boolean>>({});
281
+ const [savedAccountId, setSavedAccountId] = useState<number | null>(
282
+ account?.id ?? null
283
+ );
284
+ const [collaboratorSheetOpen, setCollaboratorSheetOpen] = useState(false);
285
+ const [collaboratorToEdit, setCollaboratorToEdit] = useState<Person | null>(
286
+ null
287
+ );
288
+ const [localCollaborators, setLocalCollaborators] = useState<Person[]>([]);
289
+ const [removedCollaboratorIds, setRemovedCollaboratorIds] = useState<
290
+ number[]
291
+ >([]);
292
+ const [collaboratorPickerKey, setCollaboratorPickerKey] = useState(0);
293
+ const [collaboratorsViewMode, setCollaboratorsViewMode] = useState<
294
+ 'cards' | 'table'
295
+ >('cards');
296
+ const contactValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
297
+ const addressLine1Refs = useRef<Record<string, HTMLInputElement | null>>({});
298
+ const documentValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
299
+
300
+ const emptyCollaboratorPage = useMemo<PaginatedResult<Person>>(
301
+ () => ({
302
+ data: [],
303
+ total: 0,
304
+ page: 1,
305
+ pageSize: 100,
306
+ lastPage: 1,
307
+ prev: null,
308
+ next: null,
309
+ }),
310
+ []
311
+ );
312
+
313
+ const currentAccountId = account?.id ?? savedAccountId ?? null;
314
+ const isPersistedAccount = Boolean(currentAccountId);
102
315
 
103
316
  const accountFormSchema = useMemo(
104
317
  () =>
@@ -108,14 +321,6 @@ export function AccountFormSheet({
108
321
  status: z.enum(['active', 'inactive']),
109
322
  industry: z.string().nullable(),
110
323
  website: z.string().nullable(),
111
- email: z
112
- .string()
113
- .nullable()
114
- .refine(
115
- (value) => !value || z.string().email().safeParse(value).success,
116
- t('form.validation.invalidEmail')
117
- ),
118
- phone: z.string().nullable(),
119
324
  owner_user_id: z.number().nullable(),
120
325
  annual_revenue: z
121
326
  .number()
@@ -135,14 +340,6 @@ export function AccountFormSheet({
135
340
  lifecycle_stage: z
136
341
  .enum(['prospect', 'customer', 'churned', 'inactive'])
137
342
  .nullable(),
138
- city: z.string().nullable(),
139
- state: z
140
- .string()
141
- .nullable()
142
- .refine(
143
- (value) => !value || value.trim().length <= 2,
144
- t('form.validation.stateMaxLength')
145
- ),
146
343
  }),
147
344
  [t]
148
345
  );
@@ -155,500 +352,1645 @@ export function AccountFormSheet({
155
352
  status: 'active',
156
353
  industry: null,
157
354
  website: null,
158
- email: null,
159
- phone: null,
160
355
  owner_user_id: null,
161
356
  annual_revenue: null,
162
357
  employee_count: null,
163
358
  lifecycle_stage: 'prospect',
164
- city: null,
165
- state: null,
166
359
  },
167
360
  });
168
361
 
169
- useEffect(() => {
170
- if (!open) {
171
- return;
172
- }
362
+ const {
363
+ data: collaboratorPage = emptyCollaboratorPage,
364
+ refetch: refetchCollaborators,
365
+ } = useQuery<PaginatedResult<Person>>({
366
+ queryKey: [
367
+ 'contact-account-collaborators',
368
+ currentAccountId,
369
+ currentLocaleCode,
370
+ ],
371
+ queryFn: async () => {
372
+ if (!currentAccountId) {
373
+ return emptyCollaboratorPage;
374
+ }
173
375
 
174
- if (account) {
175
- form.reset({
176
- name: account.name,
177
- trade_name: account.trade_name ?? null,
178
- status: account.status,
179
- industry: account.industry ?? null,
180
- website: account.website ?? null,
181
- email: account.email ?? null,
182
- phone: account.phone ?? null,
183
- owner_user_id: account.owner_user_id ?? null,
184
- annual_revenue: account.annual_revenue ?? null,
185
- employee_count: account.employee_count ?? null,
186
- lifecycle_stage: account.lifecycle_stage ?? 'prospect',
187
- city: account.city ?? null,
188
- state: account.state ?? null,
189
- });
190
- return;
191
- }
376
+ const params = new URLSearchParams();
377
+ params.set('page', '1');
378
+ params.set('pageSize', '100');
379
+ params.set('type', 'individual');
380
+ params.set('employer_company_id', String(currentAccountId));
192
381
 
193
- form.reset({
194
- name: '',
195
- trade_name: null,
196
- status: 'active',
197
- industry: null,
198
- website: null,
199
- email: null,
200
- phone: null,
201
- owner_user_id: null,
202
- annual_revenue: null,
203
- employee_count: null,
204
- lifecycle_stage: 'prospect',
205
- city: null,
206
- state: null,
207
- });
208
- }, [account, form, open]);
382
+ const response = await request<PaginatedResult<Person>>({
383
+ url: `/person?${params.toString()}`,
384
+ method: 'GET',
385
+ });
209
386
 
210
- const handleSubmit = async (data: AccountFormData) => {
211
- await onSubmit({
212
- name: data.name.trim(),
213
- trade_name: emptyToNull(data.trade_name),
214
- status: data.status,
215
- industry: emptyToNull(data.industry),
216
- website: emptyToNull(data.website),
217
- email: emptyToNull(data.email),
218
- phone: emptyToNull(data.phone),
219
- owner_user_id: data.owner_user_id ?? null,
220
- annual_revenue: data.annual_revenue ?? null,
221
- employee_count: data.employee_count ?? null,
222
- lifecycle_stage: data.lifecycle_stage ?? null,
223
- city: emptyToNull(data.city),
224
- state: emptyToNull(data.state)?.toUpperCase() ?? null,
225
- });
226
- };
387
+ return response.data;
388
+ },
389
+ placeholderData: (previous) => previous ?? emptyCollaboratorPage,
390
+ });
227
391
 
228
392
  const watchedName = useWatch({
229
393
  control: form.control,
230
394
  name: 'name',
231
395
  });
396
+
397
+ const watchedFormValues = useWatch({
398
+ control: form.control,
399
+ });
400
+
232
401
  const additionalDetailsDefaultOpen = Boolean(
233
402
  account?.industry ||
234
403
  account?.annual_revenue != null ||
235
404
  account?.employee_count != null
236
405
  );
237
406
 
407
+ const upsertLocalCollaborator = useCallback(
408
+ (person: Person | null | undefined) => {
409
+ if (!person?.id) {
410
+ return;
411
+ }
412
+
413
+ setRemovedCollaboratorIds((previous) =>
414
+ previous.filter((item) => item !== Number(person.id))
415
+ );
416
+
417
+ setLocalCollaborators((previous) => {
418
+ const next = previous.filter((item) => item.id !== person.id);
419
+ next.push(person);
420
+ return next.sort((left, right) => left.name.localeCompare(right.name));
421
+ });
422
+ },
423
+ []
424
+ );
425
+
426
+ const removeCollaborator = useCallback((personId: number) => {
427
+ setRemovedCollaboratorIds((previous) =>
428
+ previous.includes(personId) ? previous : [...previous, personId]
429
+ );
430
+ setLocalCollaborators((previous) =>
431
+ previous.filter((item) => item.id !== personId)
432
+ );
433
+ }, []);
434
+
435
+ const collaborators = useMemo(() => {
436
+ const merged = new Map<number, Person>();
437
+ const removedIds = new Set(removedCollaboratorIds);
438
+
439
+ for (const person of collaboratorPage.data ?? []) {
440
+ if (!removedIds.has(Number(person.id))) {
441
+ merged.set(person.id, person);
442
+ }
443
+ }
444
+
445
+ for (const person of localCollaborators) {
446
+ if (!removedIds.has(Number(person.id))) {
447
+ merged.set(person.id, person);
448
+ }
449
+ }
450
+
451
+ return Array.from(merged.values()).sort((left, right) =>
452
+ left.name.localeCompare(right.name)
453
+ );
454
+ }, [collaboratorPage.data, localCollaborators, removedCollaboratorIds]);
455
+
456
+ const collaboratorPersonIds = useMemo(
457
+ () =>
458
+ collaborators
459
+ .map((person) => Number(person.id))
460
+ .filter((personId) => Number.isInteger(personId) && personId > 0),
461
+ [collaborators]
462
+ );
463
+
464
+ const loadCollaboratorOptions = useCallback(
465
+ async ({
466
+ page,
467
+ pageSize,
468
+ search,
469
+ }: {
470
+ page: number;
471
+ pageSize: number;
472
+ search: string;
473
+ }) => {
474
+ const params = new URLSearchParams();
475
+ params.set('page', String(page));
476
+ params.set('pageSize', String(pageSize));
477
+ params.set('type', 'individual');
478
+
479
+ if (search.trim()) {
480
+ params.set('search', search.trim());
481
+ }
482
+
483
+ const response = await request<PaginatedResult<Person>>({
484
+ url: `/person?${params.toString()}`,
485
+ method: 'GET',
486
+ });
487
+
488
+ const payload = response?.data ?? emptyCollaboratorPage;
489
+ const items = (payload.data ?? []).filter((person) => {
490
+ const personId = Number(person.id);
491
+ return !collaboratorPersonIds.includes(personId);
492
+ });
493
+
494
+ return {
495
+ items,
496
+ hasMore: Boolean(payload.next) || items.length >= pageSize,
497
+ };
498
+ },
499
+ [collaboratorPersonIds, emptyCollaboratorPage, request]
500
+ );
501
+
238
502
  const companyPreviewName =
239
503
  watchedName?.trim() || account?.name || t('form.summary.newCompany');
240
504
 
505
+ const hasDraftContent = useMemo(
506
+ () =>
507
+ Boolean(
508
+ (watchedFormValues?.name ?? '').trim() ||
509
+ (watchedFormValues?.trade_name ?? '').trim() ||
510
+ (watchedFormValues?.industry ?? '').trim() ||
511
+ (watchedFormValues?.website ?? '').trim() ||
512
+ watchedFormValues?.owner_user_id != null ||
513
+ watchedFormValues?.annual_revenue != null ||
514
+ watchedFormValues?.employee_count != null ||
515
+ contacts.length > 0 ||
516
+ addresses.length > 0 ||
517
+ documents.length > 0 ||
518
+ localCollaborators.length > 0 ||
519
+ removedCollaboratorIds.length > 0
520
+ ),
521
+ [
522
+ watchedFormValues,
523
+ contacts,
524
+ addresses,
525
+ documents,
526
+ localCollaborators,
527
+ removedCollaboratorIds,
528
+ ]
529
+ );
530
+
531
+ const draftValue = useMemo<AccountDraftPayload>(
532
+ () => ({
533
+ mode: currentAccountId ? 'edit' : 'create',
534
+ savedAccountId: currentAccountId,
535
+ values: {
536
+ name: watchedFormValues?.name ?? '',
537
+ trade_name: watchedFormValues?.trade_name ?? null,
538
+ status: watchedFormValues?.status ?? 'active',
539
+ industry: watchedFormValues?.industry ?? null,
540
+ website: watchedFormValues?.website ?? null,
541
+ owner_user_id: watchedFormValues?.owner_user_id ?? null,
542
+ annual_revenue: watchedFormValues?.annual_revenue ?? null,
543
+ employee_count: watchedFormValues?.employee_count ?? null,
544
+ lifecycle_stage: watchedFormValues?.lifecycle_stage ?? 'prospect',
545
+ },
546
+ contacts,
547
+ addresses,
548
+ documents,
549
+ localCollaborators,
550
+ removedCollaboratorIds,
551
+ }),
552
+ [
553
+ addresses,
554
+ contacts,
555
+ currentAccountId,
556
+ documents,
557
+ localCollaborators,
558
+ removedCollaboratorIds,
559
+ watchedFormValues,
560
+ ]
561
+ );
562
+
563
+ const {
564
+ clearDraft,
565
+ loadDraft,
566
+ hasDraft,
567
+ savedAt: draftSavedAt,
568
+ } = useFormDraft<AccountDraftPayload>({
569
+ storageKey: ACCOUNT_FORM_DRAFT_STORAGE_KEY,
570
+ value: draftValue,
571
+ hasData: hasDraftContent,
572
+ enabled: open,
573
+ });
574
+
575
+ const [draftStatusTick, setDraftStatusTick] = useState(() => Date.now());
576
+
577
+ useEffect(() => {
578
+ if (!draftSavedAt) {
579
+ return;
580
+ }
581
+
582
+ const interval = window.setInterval(() => {
583
+ setDraftStatusTick(Date.now());
584
+ }, 60_000);
585
+
586
+ return () => window.clearInterval(interval);
587
+ }, [draftSavedAt]);
588
+
589
+ const draftStatusContent = useMemo(() => {
590
+ void draftStatusTick;
591
+
592
+ if (!hasDraft || !draftSavedAt) {
593
+ return null;
594
+ }
595
+
596
+ const savedDate = new Date(draftSavedAt);
597
+
598
+ if (Number.isNaN(savedDate.getTime())) {
599
+ return null;
600
+ }
601
+
602
+ const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
603
+ const relativeLabel = formatDistanceToNow(savedDate, {
604
+ addSuffix: true,
605
+ locale,
606
+ });
607
+ const absoluteLabel = formatDateTime(
608
+ savedDate,
609
+ getSettingValue,
610
+ currentLocaleCode
611
+ );
612
+
613
+ if (currentLocaleCode.startsWith('pt')) {
614
+ return `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`;
615
+ }
616
+
617
+ return `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
618
+ }, [
619
+ draftSavedAt,
620
+ currentLocaleCode,
621
+ draftStatusTick,
622
+ getSettingValue,
623
+ hasDraft,
624
+ ]);
625
+
626
+ const focusWhenAvailable = (getElement: () => HTMLInputElement | null) => {
627
+ requestAnimationFrame(() => {
628
+ const element = getElement();
629
+ if (!element) {
630
+ setTimeout(() => {
631
+ getElement()?.focus();
632
+ }, 0);
633
+ return;
634
+ }
635
+
636
+ element.focus();
637
+ });
638
+ };
639
+
640
+ const getContactTypeCode = useCallback(
641
+ (contactTypeId?: number | null) =>
642
+ String(
643
+ contactTypes.find(
644
+ (contactType) => contactType.contact_type_id === contactTypeId
645
+ )?.code || ''
646
+ ).toUpperCase(),
647
+ [contactTypes]
648
+ );
649
+
650
+ const getDocumentTypeCode = useCallback(
651
+ (documentTypeId?: number | null) =>
652
+ String(
653
+ documentTypes.find(
654
+ (documentType) => documentType.document_type_id === documentTypeId
655
+ )?.code || ''
656
+ ).toUpperCase(),
657
+ [documentTypes]
658
+ );
659
+
660
+ const resolveContactTypeId = useCallback(
661
+ (code: string) =>
662
+ contactTypes.find(
663
+ (contactType) => String(contactType.code).toUpperCase() === code
664
+ )?.contact_type_id ?? null,
665
+ [contactTypes]
666
+ );
667
+
668
+ const resolveDocumentTypeId = useCallback(
669
+ (code: string) =>
670
+ documentTypes.find(
671
+ (documentType) => String(documentType.code).toUpperCase() === code
672
+ )?.document_type_id ?? null,
673
+ [documentTypes]
674
+ );
675
+
676
+ const maskContactValueByType = useCallback(
677
+ (value: string, contactTypeId?: number | null) => {
678
+ const contactTypeCode = getContactTypeCode(contactTypeId);
679
+ if (['PHONE', 'MOBILE', 'WHATSAPP'].includes(contactTypeCode)) {
680
+ return applyPhoneMask(value);
681
+ }
682
+
683
+ return value;
684
+ },
685
+ [getContactTypeCode]
686
+ );
687
+
688
+ const maskDocumentValueByType = useCallback(
689
+ (value: string, documentTypeId?: number | null) => {
690
+ const documentTypeCode = getDocumentTypeCode(documentTypeId);
691
+ if (documentTypeCode === 'CPF') {
692
+ return applyCpfMask(value);
693
+ }
694
+ if (documentTypeCode === 'CNPJ') {
695
+ return applyCnpjMask(value);
696
+ }
697
+
698
+ return value;
699
+ },
700
+ [getDocumentTypeCode]
701
+ );
702
+
703
+ const getPersonPrimaryContact = (
704
+ person: Person,
705
+ codes: string[]
706
+ ): string | null => {
707
+ const upperCodes = codes.map((code) => code.toUpperCase());
708
+ const contactTypeIds = contactTypes
709
+ .filter((contactType) =>
710
+ upperCodes.includes(String(contactType.code).toUpperCase())
711
+ )
712
+ .map((contactType) => contactType.contact_type_id);
713
+
714
+ const matches =
715
+ person.contact?.filter((contact) => {
716
+ const nestedCode = String(
717
+ contact.contact_type?.code || ''
718
+ ).toUpperCase();
719
+ return (
720
+ contactTypeIds.includes(contact.contact_type_id) ||
721
+ upperCodes.includes(nestedCode)
722
+ );
723
+ }) ?? [];
724
+
725
+ const primary = matches.find((contact) => contact.is_primary) ?? matches[0];
726
+ if (!primary?.value) {
727
+ return null;
728
+ }
729
+
730
+ return upperCodes.some((code) =>
731
+ ['PHONE', 'MOBILE', 'WHATSAPP'].includes(code)
732
+ )
733
+ ? applyPhoneMask(primary.value)
734
+ : primary.value;
735
+ };
736
+
737
+ useEffect(() => {
738
+ if (!open) {
739
+ return;
740
+ }
741
+
742
+ const storedDraft = !account ? loadDraft() : null;
743
+
744
+ form.reset({
745
+ name: storedDraft?.payload.values?.name ?? account?.name ?? '',
746
+ trade_name:
747
+ storedDraft?.payload.values?.trade_name ?? account?.trade_name ?? null,
748
+ status:
749
+ storedDraft?.payload.values?.status ?? account?.status ?? 'active',
750
+ industry:
751
+ storedDraft?.payload.values?.industry ?? account?.industry ?? null,
752
+ website: storedDraft?.payload.values?.website ?? account?.website ?? null,
753
+ owner_user_id:
754
+ storedDraft?.payload.values?.owner_user_id ??
755
+ account?.owner_user_id ??
756
+ null,
757
+ annual_revenue:
758
+ storedDraft?.payload.values?.annual_revenue ??
759
+ account?.annual_revenue ??
760
+ null,
761
+ employee_count:
762
+ storedDraft?.payload.values?.employee_count ??
763
+ account?.employee_count ??
764
+ null,
765
+ lifecycle_stage:
766
+ storedDraft?.payload.values?.lifecycle_stage ??
767
+ account?.lifecycle_stage ??
768
+ 'prospect',
769
+ });
770
+
771
+ const nextContacts: EditableAccountContact[] = (account?.contact ?? []).map(
772
+ (contact, index) => ({
773
+ ...contact,
774
+ clientId: `contact-${contact.id ?? index}`,
775
+ contact_type_id: contact.contact_type_id ?? null,
776
+ value: maskContactValueByType(
777
+ contact.value || '',
778
+ contact.contact_type_id
779
+ ),
780
+ })
781
+ );
782
+
783
+ if (nextContacts.length === 0) {
784
+ if (account?.email) {
785
+ nextContacts.push({
786
+ clientId: createClientId('contact'),
787
+ value: account.email,
788
+ is_primary: true,
789
+ contact_type_id: resolveContactTypeId('EMAIL'),
790
+ });
791
+ }
792
+
793
+ if (account?.phone) {
794
+ nextContacts.push({
795
+ clientId: createClientId('contact'),
796
+ value: applyPhoneMask(account.phone),
797
+ is_primary: true,
798
+ contact_type_id:
799
+ resolveContactTypeId('PHONE') ?? resolveContactTypeId('MOBILE'),
800
+ });
801
+ }
802
+ }
803
+
804
+ const nextAddresses: EditableAccountAddress[] = (
805
+ account?.address ?? []
806
+ ).map((address, index) => ({
807
+ ...address,
808
+ clientId: `address-${address.id ?? index}`,
809
+ }));
810
+
811
+ if (nextAddresses.length === 0 && (account?.city || account?.state)) {
812
+ nextAddresses.push({
813
+ clientId: createClientId('address'),
814
+ line1: '',
815
+ line2: '',
816
+ city: account.city ?? '',
817
+ state: account.state ?? '',
818
+ country_code: 'BRA',
819
+ postal_code: '',
820
+ is_primary: true,
821
+ address_type: 'commercial',
822
+ });
823
+ }
824
+
825
+ const nextDocuments: EditableAccountDocument[] = (
826
+ account?.document ?? []
827
+ ).map((document, index) => ({
828
+ ...document,
829
+ clientId: `document-${document.id ?? index}`,
830
+ document_type_id: document.document_type_id ?? null,
831
+ value: maskDocumentValueByType(
832
+ document.value || '',
833
+ document.document_type_id
834
+ ),
835
+ }));
836
+
837
+ const frame = window.requestAnimationFrame(() => {
838
+ setSavedAccountId(
839
+ storedDraft?.payload.savedAccountId ?? account?.id ?? null
840
+ );
841
+ setContacts(storedDraft?.payload.contacts ?? nextContacts);
842
+ setAddresses(storedDraft?.payload.addresses ?? nextAddresses);
843
+ setDocuments(storedDraft?.payload.documents ?? nextDocuments);
844
+ setContactsOpen(true);
845
+ setAddressesOpen(true);
846
+ setDocumentsOpen(true);
847
+ setLoadingCEP({});
848
+ setLocalCollaborators(storedDraft?.payload.localCollaborators ?? []);
849
+ setRemovedCollaboratorIds(
850
+ storedDraft?.payload.removedCollaboratorIds ?? []
851
+ );
852
+ setCollaboratorPickerKey(0);
853
+ setCollaboratorSheetOpen(false);
854
+ setCollaboratorToEdit(null);
855
+ });
856
+
857
+ return () => window.cancelAnimationFrame(frame);
858
+ }, [
859
+ account,
860
+ form,
861
+ loadDraft,
862
+ maskContactValueByType,
863
+ maskDocumentValueByType,
864
+ open,
865
+ resolveContactTypeId,
866
+ ]);
867
+
868
+ const addContact = (kind: 'email' | 'phone' | 'blank' = 'blank') => {
869
+ const nextContact: EditableAccountContact = {
870
+ clientId: createClientId('contact'),
871
+ value: '',
872
+ is_primary: true,
873
+ contact_type_id:
874
+ kind === 'email'
875
+ ? resolveContactTypeId('EMAIL')
876
+ : kind === 'phone'
877
+ ? (resolveContactTypeId('PHONE') ?? resolveContactTypeId('MOBILE'))
878
+ : null,
879
+ };
880
+
881
+ setContactsOpen(true);
882
+ setContacts((previous) => [...previous, nextContact]);
883
+ focusWhenAvailable(
884
+ () => contactValueRefs.current[nextContact.clientId] ?? null
885
+ );
886
+ };
887
+
888
+ const updateContact = (
889
+ clientId: string,
890
+ updates: Partial<EditableAccountContact>
891
+ ) => {
892
+ setContacts((previous) =>
893
+ previous.map((contact) =>
894
+ contact.clientId === clientId ? { ...contact, ...updates } : contact
895
+ )
896
+ );
897
+ };
898
+
899
+ const setPrimaryContact = (clientId: string) => {
900
+ const targetContact = contacts.find(
901
+ (contact) => contact.clientId === clientId
902
+ );
903
+ if (!targetContact) return;
904
+
905
+ setContacts((previous) =>
906
+ previous.map((contact) => ({
907
+ ...contact,
908
+ is_primary:
909
+ contact.clientId === clientId
910
+ ? true
911
+ : contact.contact_type_id === targetContact.contact_type_id
912
+ ? false
913
+ : contact.is_primary,
914
+ }))
915
+ );
916
+ };
917
+
918
+ const removeContact = (clientId: string) => {
919
+ delete contactValueRefs.current[clientId];
920
+ setContacts((previous) =>
921
+ previous.filter((contact) => contact.clientId !== clientId)
922
+ );
923
+ };
924
+
925
+ const addAddress = () => {
926
+ const nextAddress: EditableAccountAddress = {
927
+ clientId: createClientId('address'),
928
+ line1: '',
929
+ line2: '',
930
+ city: '',
931
+ state: '',
932
+ country_code: 'BRA',
933
+ postal_code: '',
934
+ is_primary: addresses.length === 0,
935
+ address_type: 'commercial',
936
+ };
937
+
938
+ setAddressesOpen(true);
939
+ setAddresses((previous) => [...previous, nextAddress]);
940
+ focusWhenAvailable(
941
+ () => addressLine1Refs.current[nextAddress.clientId] ?? null
942
+ );
943
+ };
944
+
945
+ const updateAddress = (
946
+ clientId: string,
947
+ updates: Partial<EditableAccountAddress>
948
+ ) => {
949
+ setAddresses((previous) =>
950
+ previous.map((address) =>
951
+ address.clientId === clientId ? { ...address, ...updates } : address
952
+ )
953
+ );
954
+ };
955
+
956
+ const setPrimaryAddress = (clientId: string) => {
957
+ setAddresses((previous) =>
958
+ previous.map((address) => ({
959
+ ...address,
960
+ is_primary: address.clientId === clientId,
961
+ }))
962
+ );
963
+ };
964
+
965
+ const removeAddress = (clientId: string) => {
966
+ delete addressLine1Refs.current[clientId];
967
+ setAddresses((previous) =>
968
+ previous.filter((address) => address.clientId !== clientId)
969
+ );
970
+ };
971
+
972
+ const addDocument = (kind: 'cnpj' | 'cpf' | 'blank' = 'blank') => {
973
+ const nextDocument: EditableAccountDocument = {
974
+ clientId: createClientId('document'),
975
+ value: '',
976
+ document_type_id:
977
+ kind === 'cnpj'
978
+ ? resolveDocumentTypeId('CNPJ')
979
+ : kind === 'cpf'
980
+ ? resolveDocumentTypeId('CPF')
981
+ : null,
982
+ };
983
+
984
+ setDocumentsOpen(true);
985
+ setDocuments((previous) => [...previous, nextDocument]);
986
+ focusWhenAvailable(
987
+ () => documentValueRefs.current[nextDocument.clientId] ?? null
988
+ );
989
+ };
990
+
991
+ const updateDocument = (
992
+ clientId: string,
993
+ updates: Partial<EditableAccountDocument>
994
+ ) => {
995
+ setDocuments((previous) =>
996
+ previous.map((document) =>
997
+ document.clientId === clientId ? { ...document, ...updates } : document
998
+ )
999
+ );
1000
+ };
1001
+
1002
+ const removeDocument = (clientId: string) => {
1003
+ delete documentValueRefs.current[clientId];
1004
+ setDocuments((previous) =>
1005
+ previous.filter((document) => document.clientId !== clientId)
1006
+ );
1007
+ };
1008
+
1009
+ const handleCEP = async (
1010
+ event: ChangeEvent<HTMLInputElement>,
1011
+ clientId: string
1012
+ ) => {
1013
+ let value = event.target.value.replace(/\D/g, '');
1014
+ if (value.length > 5) {
1015
+ value = `${value.slice(0, 5)}-${value.slice(5, 8)}`;
1016
+ }
1017
+
1018
+ updateAddress(clientId, { postal_code: value });
1019
+
1020
+ const rawCep = value.replace(/\D/g, '');
1021
+ if (rawCep.length !== 8) return;
1022
+
1023
+ setLoadingCEP((previous) => ({ ...previous, [clientId]: true }));
1024
+ const data = await fetchViaCep(rawCep);
1025
+
1026
+ if (data) {
1027
+ updateAddress(clientId, {
1028
+ line1: data.logradouro || '',
1029
+ city: data.localidade || '',
1030
+ state: data.uf || '',
1031
+ });
1032
+ }
1033
+
1034
+ setLoadingCEP((previous) => ({ ...previous, [clientId]: false }));
1035
+ };
1036
+
1037
+ const handleSubmit = async (data: AccountFormData) => {
1038
+ const normalizedContacts = contacts
1039
+ .filter(
1040
+ (contact) =>
1041
+ contact.value.trim().length > 0 && !!contact.contact_type_id
1042
+ )
1043
+ .map((contact) => ({
1044
+ id: contact.id,
1045
+ value: contact.value.trim(),
1046
+ is_primary: contact.is_primary,
1047
+ contact_type_id: contact.contact_type_id as number,
1048
+ }));
1049
+
1050
+ const normalizedAddresses = addresses
1051
+ .filter(
1052
+ (address) =>
1053
+ address.line1.trim().length > 0 ||
1054
+ address.city.trim().length > 0 ||
1055
+ address.state.trim().length > 0 ||
1056
+ (address.postal_code || '').trim().length > 0
1057
+ )
1058
+ .map((address) => ({
1059
+ id: address.id,
1060
+ line1: address.line1.trim(),
1061
+ line2: address.line2?.trim() || '',
1062
+ city: address.city.trim(),
1063
+ state: address.state.trim(),
1064
+ country_code: address.country_code || 'BRA',
1065
+ postal_code: address.postal_code?.trim() || '',
1066
+ is_primary: address.is_primary,
1067
+ address_type: address.address_type,
1068
+ }));
1069
+
1070
+ const normalizedDocuments = documents
1071
+ .filter(
1072
+ (document) =>
1073
+ document.value.trim().length > 0 && !!document.document_type_id
1074
+ )
1075
+ .map((document) => ({
1076
+ id: document.id,
1077
+ value: document.value.trim(),
1078
+ document_type_id: document.document_type_id as number,
1079
+ }));
1080
+
1081
+ const primaryEmail =
1082
+ normalizedContacts.find(
1083
+ (contact) => getContactTypeCode(contact.contact_type_id) === 'EMAIL'
1084
+ )?.value ?? null;
1085
+
1086
+ const primaryPhone =
1087
+ normalizedContacts.find((contact) =>
1088
+ ['PHONE', 'MOBILE', 'WHATSAPP'].includes(
1089
+ getContactTypeCode(contact.contact_type_id)
1090
+ )
1091
+ )?.value ?? null;
1092
+
1093
+ const primaryAddress =
1094
+ normalizedAddresses.find((address) => address.is_primary) ||
1095
+ normalizedAddresses[0] ||
1096
+ null;
1097
+
1098
+ const result = await onSubmit(
1099
+ {
1100
+ name: data.name.trim(),
1101
+ trade_name: emptyToNull(data.trade_name),
1102
+ status: data.status,
1103
+ industry: emptyToNull(data.industry),
1104
+ website: emptyToNull(data.website),
1105
+ email: emptyToNull(primaryEmail),
1106
+ phone: emptyToNull(primaryPhone),
1107
+ owner_user_id: data.owner_user_id ?? null,
1108
+ annual_revenue: data.annual_revenue ?? null,
1109
+ employee_count: data.employee_count ?? null,
1110
+ lifecycle_stage: data.lifecycle_stage ?? null,
1111
+ city: emptyToNull(primaryAddress?.city),
1112
+ state: emptyToNull(primaryAddress?.state)?.toUpperCase() ?? null,
1113
+ collaborator_person_ids: collaboratorPersonIds,
1114
+ contacts: normalizedContacts,
1115
+ addresses: normalizedAddresses,
1116
+ documents: normalizedDocuments,
1117
+ },
1118
+ currentAccountId
1119
+ );
1120
+
1121
+ const nextAccountId = result?.id ?? currentAccountId;
1122
+ if (!currentAccountId) {
1123
+ if (nextAccountId) {
1124
+ setSavedAccountId(nextAccountId);
1125
+ }
1126
+ return;
1127
+ }
1128
+
1129
+ clearDraft();
1130
+ onOpenChange(false);
1131
+ };
1132
+
241
1133
  const handleSheetOpenChange = (nextOpen: boolean) => {
242
1134
  if (isLoading && !nextOpen) {
243
1135
  return;
244
1136
  }
245
1137
 
1138
+ if (!nextOpen) {
1139
+ setSavedAccountId(null);
1140
+ setLocalCollaborators([]);
1141
+ setRemovedCollaboratorIds([]);
1142
+ setCollaboratorPickerKey(0);
1143
+ }
1144
+
246
1145
  onOpenChange(nextOpen);
247
1146
  };
248
1147
 
249
1148
  return (
250
- <Sheet open={open} onOpenChange={handleSheetOpenChange}>
251
- <SheetContent className="flex h-full w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-lg">
252
- <SheetHeader className="shrink-0 border-b px-4 py-4 text-left">
253
- <div className="flex items-start gap-3">
254
- <div
255
- className={`flex h-10 w-10 shrink-0 items-center justify-center rounded-xl ${
256
- account
257
- ? 'bg-amber-500/10 text-amber-600'
258
- : 'bg-blue-500/10 text-blue-600'
259
- }`}
260
- >
261
- <Building2 className="h-5 w-5" />
262
- </div>
263
- <div className="min-w-0 space-y-1">
264
- <SheetTitle>
265
- {account ? t('form.editTitle') : t('form.createTitle')}
266
- </SheetTitle>
267
- <SheetDescription>
268
- {account
269
- ? t('form.editDescription')
270
- : t('form.createDescription')}
271
- </SheetDescription>
1149
+ <>
1150
+ <Sheet open={open} onOpenChange={handleSheetOpenChange}>
1151
+ <SheetContent className="flex h-full min-h-0 w-full flex-col gap-0 overflow-hidden p-0 sm:max-w-4xl xl:max-w-6xl 2xl:max-w-7xl">
1152
+ <SheetHeader className="shrink-0 border-b px-4 py-4 text-left sm:px-6">
1153
+ <div className="flex items-start gap-3">
1154
+ <div
1155
+ className={cn(
1156
+ 'flex h-10 w-10 shrink-0 items-center justify-center rounded-xl',
1157
+ isPersistedAccount
1158
+ ? 'bg-amber-500/10 text-amber-600'
1159
+ : 'bg-blue-500/10 text-blue-600'
1160
+ )}
1161
+ >
1162
+ <Building2 className="h-5 w-5" />
1163
+ </div>
1164
+ <div className="min-w-0 space-y-1">
1165
+ <SheetTitle>
1166
+ {isPersistedAccount
1167
+ ? t('form.editTitle')
1168
+ : t('form.createTitle')}
1169
+ </SheetTitle>
1170
+ <SheetDescription>
1171
+ {isPersistedAccount
1172
+ ? t('form.editDescription')
1173
+ : t('form.createDescription')}
1174
+ </SheetDescription>
1175
+ </div>
272
1176
  </div>
273
- </div>
274
- </SheetHeader>
275
-
276
- <Form {...form}>
277
- <form
278
- onSubmit={form.handleSubmit(handleSubmit)}
279
- className="flex h-full flex-col [&_button[role='combobox']]:h-9 [&_input]:h-9 [&_label]:text-xs [&_label]:font-medium"
280
- >
281
- <div className="flex-1 overflow-y-auto">
282
- <div className="space-y-5 px-4 py-4">
283
- <div className="rounded-xl border bg-muted/20 p-3">
284
- <p className="truncate text-sm font-semibold text-foreground">
285
- {companyPreviewName}
286
- </p>
287
- <p className="mt-1 text-xs text-muted-foreground">
288
- {account
289
- ? t('form.summary.editHint')
290
- : t('form.summary.createHint')}
291
- </p>
292
- </div>
1177
+ </SheetHeader>
293
1178
 
294
- <section className="space-y-3">
295
- <SectionHeader
296
- title={t('form.sections.identityTitle')}
297
- description={t('form.sections.identityDescription')}
298
- icon={<Building2 className="h-4 w-4" />}
299
- />
300
-
301
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
302
- <FormField
303
- control={form.control}
304
- name="name"
305
- render={({ field }) => (
306
- <FormItem className="space-y-1.5 sm:col-span-2">
307
- <FormLabel>{t('form.companyName')}</FormLabel>
308
- <FormControl>
309
- <Input
310
- placeholder={t('form.placeholders.companyName')}
311
- autoFocus={open}
312
- autoComplete="organization"
313
- {...field}
314
- disabled={isLoading}
315
- />
316
- </FormControl>
317
- <FormMessage />
318
- </FormItem>
319
- )}
1179
+ <Form {...form}>
1180
+ <form
1181
+ onSubmit={form.handleSubmit(handleSubmit)}
1182
+ className="flex h-full min-h-0 flex-col [&_button[role='combobox']]:h-9 [&_input]:h-9 [&_label]:text-xs [&_label]:font-medium"
1183
+ >
1184
+ <div className="min-h-0 flex-1 overflow-y-auto">
1185
+ <div className="space-y-5 px-4 py-4 sm:px-6">
1186
+ <section className="space-y-3">
1187
+ <SectionHeader
1188
+ title={t('form.sections.identityTitle')}
1189
+ description={t('form.sections.identityDescription')}
1190
+ icon={<Building2 className="h-4 w-4" />}
320
1191
  />
321
1192
 
322
- <FormField
323
- control={form.control}
324
- name="trade_name"
325
- render={({ field }) => (
326
- <FormItem className="space-y-1.5 sm:col-span-2">
327
- <FormLabel>{t('form.tradeName')}</FormLabel>
328
- <FormControl>
329
- <Input
330
- placeholder={t('form.placeholders.tradeName')}
331
- autoComplete="organization-title"
332
- value={field.value ?? ''}
333
- onChange={(event) =>
334
- field.onChange(event.target.value || null)
335
- }
336
- disabled={isLoading}
337
- />
338
- </FormControl>
339
- <FormMessage />
340
- </FormItem>
341
- )}
342
- />
343
- </div>
344
- </section>
345
-
346
- <section className="space-y-3">
347
- <SectionHeader
348
- title={t('form.sections.relationshipTitle')}
349
- description={t('form.sections.relationshipDescription')}
350
- icon={<Users className="h-4 w-4" />}
351
- />
352
-
353
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
354
- <FormField
355
- control={form.control}
356
- name="status"
357
- render={({ field }) => (
358
- <FormItem className="space-y-1.5">
359
- <FormLabel>{t('form.status')}</FormLabel>
360
- <Select
361
- value={field.value}
362
- onValueChange={field.onChange}
363
- disabled={isLoading}
364
- >
1193
+ <div className="grid grid-cols-2 gap-3">
1194
+ <FormField
1195
+ control={form.control}
1196
+ name="name"
1197
+ render={({ field }) => (
1198
+ <FormItem className="space-y-1.5 ">
1199
+ <FormLabel>{t('form.companyName')}</FormLabel>
365
1200
  <FormControl>
366
- <SelectTrigger className="w-full">
367
- <SelectValue />
368
- </SelectTrigger>
1201
+ <Input
1202
+ placeholder={t('form.placeholders.companyName')}
1203
+ autoFocus={open}
1204
+ autoComplete="organization"
1205
+ {...field}
1206
+ disabled={isLoading}
1207
+ />
369
1208
  </FormControl>
370
- <SelectContent>
371
- <SelectItem value="active">
372
- {t('status_active')}
373
- </SelectItem>
374
- <SelectItem value="inactive">
375
- {t('status_inactive')}
376
- </SelectItem>
377
- </SelectContent>
378
- </Select>
379
- <FormMessage />
380
- </FormItem>
381
- )}
382
- />
1209
+ <FormMessage />
1210
+ </FormItem>
1211
+ )}
1212
+ />
383
1213
 
384
- <FormField
385
- control={form.control}
386
- name="lifecycle_stage"
387
- render={({ field }) => (
388
- <FormItem className="space-y-1.5">
389
- <FormLabel>{t('form.lifecycleStage')}</FormLabel>
390
- <Select
391
- value={field.value ?? 'prospect'}
392
- onValueChange={(value) =>
393
- field.onChange(value || null)
394
- }
395
- disabled={isLoading}
396
- >
1214
+ <FormField
1215
+ control={form.control}
1216
+ name="trade_name"
1217
+ render={({ field }) => (
1218
+ <FormItem className="space-y-1.5">
1219
+ <FormLabel>{t('form.tradeName')}</FormLabel>
397
1220
  <FormControl>
398
- <SelectTrigger className="w-full">
399
- <SelectValue />
400
- </SelectTrigger>
1221
+ <Input
1222
+ placeholder={t('form.placeholders.tradeName')}
1223
+ autoComplete="organization-title"
1224
+ value={field.value ?? ''}
1225
+ onChange={(event) =>
1226
+ field.onChange(event.target.value || null)
1227
+ }
1228
+ disabled={isLoading}
1229
+ />
401
1230
  </FormControl>
402
- <SelectContent>
403
- <SelectItem value="prospect">
404
- {t('stage_prospect')}
405
- </SelectItem>
406
- <SelectItem value="customer">
407
- {t('stage_customer')}
408
- </SelectItem>
409
- <SelectItem value="churned">
410
- {t('stage_churned')}
411
- </SelectItem>
412
- <SelectItem value="inactive">
413
- {t('stage_inactive')}
414
- </SelectItem>
415
- </SelectContent>
416
- </Select>
417
- <FormMessage />
418
- </FormItem>
419
- )}
420
- />
1231
+ <FormMessage />
1232
+ </FormItem>
1233
+ )}
1234
+ />
1235
+ </div>
1236
+ </section>
421
1237
 
422
- <FormField
423
- control={form.control}
424
- name="owner_user_id"
425
- render={({ field }) => (
426
- <FormItem className="space-y-1.5 sm:col-span-2">
427
- <FormLabel>{t('form.owner')}</FormLabel>
428
- <Select
429
- value={
430
- field.value != null
431
- ? String(field.value)
432
- : 'unassigned'
433
- }
434
- onValueChange={(value) =>
435
- field.onChange(
436
- value === 'unassigned' ? null : Number(value)
437
- )
438
- }
439
- disabled={isLoading}
440
- >
441
- <FormControl>
442
- <SelectTrigger className="w-full">
443
- <SelectValue placeholder={t('unassigned')} />
444
- </SelectTrigger>
445
- </FormControl>
446
- <SelectContent>
447
- <SelectItem value="unassigned">
448
- {t('unassigned')}
449
- </SelectItem>
450
- {owners.map((owner) => (
451
- <SelectItem
452
- key={owner.id}
453
- value={String(owner.id)}
454
- >
455
- {owner.name}
456
- </SelectItem>
457
- ))}
458
- </SelectContent>
459
- </Select>
460
- <FormMessage />
461
- </FormItem>
462
- )}
463
- />
464
- </div>
465
- </section>
466
-
467
- <section className="space-y-3">
468
- <SectionHeader
469
- title={t('form.sections.contactTitle')}
470
- description={t('form.sections.contactDescription')}
471
- icon={<Globe className="h-4 w-4" />}
472
- />
473
-
474
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
475
- <FormField
476
- control={form.control}
477
- name="email"
478
- render={({ field }) => (
479
- <FormItem className="space-y-1.5">
480
- <FormLabel>{t('form.email')}</FormLabel>
481
- <FormControl>
482
- <Input
483
- type="email"
484
- placeholder={t('form.placeholders.email')}
485
- autoComplete="email"
486
- value={field.value ?? ''}
487
- onChange={(event) =>
488
- field.onChange(event.target.value || null)
489
- }
490
- disabled={isLoading}
491
- />
492
- </FormControl>
493
- <FormMessage />
494
- </FormItem>
495
- )}
1238
+ <section className="space-y-3">
1239
+ <SectionHeader
1240
+ title={t('form.sections.relationshipTitle')}
1241
+ description={t('form.sections.relationshipDescription')}
1242
+ icon={<Users className="h-4 w-4" />}
496
1243
  />
497
1244
 
498
- <FormField
499
- control={form.control}
500
- name="phone"
501
- render={({ field }) => (
502
- <FormItem className="space-y-1.5">
503
- <FormLabel>{t('form.phone')}</FormLabel>
504
- <FormControl>
505
- <Input
506
- type="tel"
507
- placeholder={t('form.placeholders.phone')}
508
- autoComplete="tel"
509
- value={field.value ?? ''}
510
- onChange={(event) =>
511
- field.onChange(event.target.value || null)
512
- }
1245
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-3">
1246
+ <FormField
1247
+ control={form.control}
1248
+ name="status"
1249
+ render={({ field }) => (
1250
+ <FormItem className="space-y-1.5">
1251
+ <FormLabel>{t('form.status')}</FormLabel>
1252
+ <Select
1253
+ value={field.value}
1254
+ onValueChange={field.onChange}
513
1255
  disabled={isLoading}
514
- />
515
- </FormControl>
516
- <FormMessage />
517
- </FormItem>
518
- )}
519
- />
1256
+ >
1257
+ <FormControl>
1258
+ <SelectTrigger className="w-full">
1259
+ <SelectValue />
1260
+ </SelectTrigger>
1261
+ </FormControl>
1262
+ <SelectContent>
1263
+ <SelectItem value="active">
1264
+ {t('status_active')}
1265
+ </SelectItem>
1266
+ <SelectItem value="inactive">
1267
+ {t('status_inactive')}
1268
+ </SelectItem>
1269
+ </SelectContent>
1270
+ </Select>
1271
+ <FormMessage />
1272
+ </FormItem>
1273
+ )}
1274
+ />
520
1275
 
521
- <FormField
522
- control={form.control}
523
- name="website"
524
- render={({ field }) => (
525
- <FormItem className="space-y-1.5 sm:col-span-2">
526
- <FormLabel>{t('form.website')}</FormLabel>
527
- <FormControl>
528
- <Input
529
- placeholder={t('form.placeholders.website')}
530
- autoCapitalize="none"
531
- autoCorrect="off"
532
- value={field.value ?? ''}
533
- onChange={(event) =>
534
- field.onChange(event.target.value || null)
535
- }
536
- disabled={isLoading}
537
- />
538
- </FormControl>
539
- <FormMessage />
540
- </FormItem>
541
- )}
542
- />
543
- </div>
544
- </section>
545
-
546
- <section className="space-y-3">
547
- <SectionHeader
548
- title={t('form.sections.locationTitle')}
549
- description={t('form.sections.locationDescription')}
550
- icon={<MapPin className="h-4 w-4" />}
551
- />
552
-
553
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
554
- <FormField
555
- control={form.control}
556
- name="city"
557
- render={({ field }) => (
558
- <FormItem className="space-y-1.5">
559
- <FormLabel>{t('form.city')}</FormLabel>
560
- <FormControl>
561
- <Input
562
- placeholder={t('form.placeholders.city')}
563
- autoComplete="address-level2"
564
- value={field.value ?? ''}
565
- onChange={(event) =>
566
- field.onChange(event.target.value || null)
1276
+ <FormField
1277
+ control={form.control}
1278
+ name="lifecycle_stage"
1279
+ render={({ field }) => (
1280
+ <FormItem className="space-y-1.5">
1281
+ <FormLabel>{t('form.lifecycleStage')}</FormLabel>
1282
+ <Select
1283
+ value={field.value ?? 'prospect'}
1284
+ onValueChange={(value) =>
1285
+ field.onChange(value || null)
567
1286
  }
568
1287
  disabled={isLoading}
569
- />
570
- </FormControl>
571
- <FormMessage />
572
- </FormItem>
573
- )}
574
- />
1288
+ >
1289
+ <FormControl>
1290
+ <SelectTrigger className="w-full">
1291
+ <SelectValue />
1292
+ </SelectTrigger>
1293
+ </FormControl>
1294
+ <SelectContent>
1295
+ <SelectItem value="prospect">
1296
+ {t('stage_prospect')}
1297
+ </SelectItem>
1298
+ <SelectItem value="customer">
1299
+ {t('stage_customer')}
1300
+ </SelectItem>
1301
+ <SelectItem value="churned">
1302
+ {t('stage_churned')}
1303
+ </SelectItem>
1304
+ <SelectItem value="inactive">
1305
+ {t('stage_inactive')}
1306
+ </SelectItem>
1307
+ </SelectContent>
1308
+ </Select>
1309
+ <FormMessage />
1310
+ </FormItem>
1311
+ )}
1312
+ />
575
1313
 
576
- <FormField
577
- control={form.control}
578
- name="state"
579
- render={({ field }) => (
580
- <FormItem className="space-y-1.5">
581
- <FormLabel>{t('form.state')}</FormLabel>
582
- <FormControl>
583
- <Input
584
- placeholder={t('form.placeholders.state')}
585
- autoComplete="address-level1"
586
- className="uppercase"
587
- maxLength={2}
588
- value={field.value ?? ''}
589
- onChange={(event) =>
1314
+ <FormField
1315
+ control={form.control}
1316
+ name="owner_user_id"
1317
+ render={({ field }) => (
1318
+ <FormItem className="space-y-1.5 xl:col-span-1 md:col-span-2">
1319
+ <FormLabel>{t('form.owner')}</FormLabel>
1320
+ <Select
1321
+ value={
1322
+ field.value != null
1323
+ ? String(field.value)
1324
+ : 'unassigned'
1325
+ }
1326
+ onValueChange={(value) =>
590
1327
  field.onChange(
591
- event.target.value.toUpperCase() || null
1328
+ value === 'unassigned' ? null : Number(value)
592
1329
  )
593
1330
  }
594
1331
  disabled={isLoading}
595
- />
596
- </FormControl>
597
- <FormMessage />
598
- </FormItem>
599
- )}
600
- />
601
- </div>
602
- </section>
603
-
604
- <Separator />
605
-
606
- <Collapsible defaultOpen={additionalDetailsDefaultOpen}>
607
- <div className="rounded-xl border border-dashed bg-muted/10">
608
- <CollapsibleTrigger asChild>
609
- <button
610
- type="button"
611
- className="group flex w-full items-center justify-between gap-3 px-3 py-3 text-left transition-colors hover:bg-muted/20"
612
- >
613
- <div>
614
- <p className="text-sm font-semibold text-foreground">
615
- {t('form.sections.additionalTitle')}
616
- </p>
617
- <p className="text-xs text-muted-foreground">
618
- {t('form.sections.additionalDescription')}
619
- </p>
1332
+ >
1333
+ <FormControl>
1334
+ <SelectTrigger className="w-full">
1335
+ <SelectValue placeholder={t('unassigned')} />
1336
+ </SelectTrigger>
1337
+ </FormControl>
1338
+ <SelectContent>
1339
+ <SelectItem value="unassigned">
1340
+ {t('unassigned')}
1341
+ </SelectItem>
1342
+ {owners.map((owner) => (
1343
+ <SelectItem
1344
+ key={owner.id}
1345
+ value={String(owner.id)}
1346
+ >
1347
+ {owner.name}
1348
+ </SelectItem>
1349
+ ))}
1350
+ </SelectContent>
1351
+ </Select>
1352
+ <FormMessage />
1353
+ </FormItem>
1354
+ )}
1355
+ />
1356
+ </div>
1357
+ </section>
1358
+
1359
+ <Collapsible
1360
+ open={contactsOpen}
1361
+ onOpenChange={setContactsOpen}
1362
+ >
1363
+ <div className="rounded-xl border bg-background p-3">
1364
+ <CollapsibleTrigger asChild>
1365
+ <div className="group flex w-full cursor-pointer flex-wrap items-center gap-2 text-left">
1366
+ <div className="flex min-w-0 items-center gap-2">
1367
+ <Globe className="h-4 w-4 text-blue-600" />
1368
+ <div>
1369
+ <p className="text-sm font-semibold text-foreground">
1370
+ {t('form.sections.contactsTitle')}
1371
+ </p>
1372
+ <p className="text-xs text-muted-foreground">
1373
+ {t('form.sections.contactsDescription')}
1374
+ </p>
1375
+ </div>
1376
+ {contacts.length > 0 ? (
1377
+ <Badge variant="secondary">
1378
+ {contacts.length}
1379
+ </Badge>
1380
+ ) : null}
1381
+ </div>
1382
+
1383
+ <div className="ml-auto flex max-w-full flex-wrap items-center justify-end gap-1 sm:flex-nowrap">
1384
+ <Button
1385
+ type="button"
1386
+ variant="ghost"
1387
+ size="sm"
1388
+ className="h-7 px-2 text-xs"
1389
+ onClick={(event) => {
1390
+ event.stopPropagation();
1391
+ addContact('email');
1392
+ }}
1393
+ >
1394
+ <Plus className="mr-1 h-3.5 w-3.5" />
1395
+ {personT('addEmail')}
1396
+ </Button>
1397
+ <Button
1398
+ type="button"
1399
+ variant="ghost"
1400
+ size="sm"
1401
+ className="h-7 px-2 text-xs"
1402
+ onClick={(event) => {
1403
+ event.stopPropagation();
1404
+ addContact('phone');
1405
+ }}
1406
+ >
1407
+ <Plus className="mr-1 h-3.5 w-3.5" />
1408
+ {personT('addPhone')}
1409
+ </Button>
1410
+ <Button
1411
+ type="button"
1412
+ variant="ghost"
1413
+ size="sm"
1414
+ className="h-7 px-2 text-xs"
1415
+ onClick={(event) => {
1416
+ event.stopPropagation();
1417
+ addContact('blank');
1418
+ }}
1419
+ >
1420
+ <Plus className="mr-1 h-3.5 w-3.5" />
1421
+ {personT('addContact')}
1422
+ </Button>
1423
+ {contactsOpen ? (
1424
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
1425
+ ) : (
1426
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
1427
+ )}
1428
+ </div>
620
1429
  </div>
1430
+ </CollapsibleTrigger>
1431
+
1432
+ <CollapsibleContent className="mt-3 space-y-2">
1433
+ {contacts.length === 0 ? (
1434
+ <div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
1435
+ {personT('noContacts')}
1436
+ </div>
1437
+ ) : (
1438
+ contacts.map((contact) => (
1439
+ <div
1440
+ key={contact.clientId}
1441
+ className={cn(
1442
+ 'space-y-2 rounded-lg border p-2',
1443
+ contact.is_primary &&
1444
+ 'border-blue-500/50 bg-blue-500/5'
1445
+ )}
1446
+ >
1447
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
1448
+ <Select
1449
+ value={
1450
+ contact.contact_type_id
1451
+ ? String(contact.contact_type_id)
1452
+ : undefined
1453
+ }
1454
+ onValueChange={(value) => {
1455
+ const nextTypeId = Number(value);
1456
+ updateContact(contact.clientId, {
1457
+ contact_type_id: nextTypeId,
1458
+ value: maskContactValueByType(
1459
+ contact.value || '',
1460
+ nextTypeId
1461
+ ),
1462
+ });
1463
+ }}
1464
+ >
1465
+ <SelectTrigger className="h-8 w-full text-xs sm:w-40">
1466
+ <SelectValue
1467
+ placeholder={personT('selectContactType')}
1468
+ />
1469
+ </SelectTrigger>
1470
+ <SelectContent>
1471
+ {contactTypes.map((contactType) => (
1472
+ <SelectItem
1473
+ key={contactType.contact_type_id}
1474
+ value={String(
1475
+ contactType.contact_type_id
1476
+ )}
1477
+ >
1478
+ {contactType.name}
1479
+ </SelectItem>
1480
+ ))}
1481
+ </SelectContent>
1482
+ </Select>
1483
+
1484
+ <div className="flex flex-1 items-center gap-2">
1485
+ <Input
1486
+ ref={(element) => {
1487
+ contactValueRefs.current[
1488
+ contact.clientId
1489
+ ] = element;
1490
+ }}
1491
+ placeholder={
1492
+ getContactTypeCode(
1493
+ contact.contact_type_id
1494
+ ) === 'EMAIL'
1495
+ ? t('form.placeholders.email')
1496
+ : t('form.placeholders.phone')
1497
+ }
1498
+ value={contact.value}
1499
+ onChange={(event) =>
1500
+ updateContact(contact.clientId, {
1501
+ value: maskContactValueByType(
1502
+ event.target.value,
1503
+ contact.contact_type_id
1504
+ ),
1505
+ })
1506
+ }
1507
+ className="h-8 flex-1 text-xs"
1508
+ />
1509
+
1510
+ <Button
1511
+ type="button"
1512
+ variant="ghost"
1513
+ size="icon"
1514
+ className={cn(
1515
+ 'h-8 w-8 shrink-0',
1516
+ contact.is_primary && 'text-amber-500'
1517
+ )}
1518
+ onClick={() =>
1519
+ setPrimaryContact(contact.clientId)
1520
+ }
1521
+ aria-label={personT('main')}
1522
+ >
1523
+ <Star
1524
+ className={cn(
1525
+ 'h-4 w-4',
1526
+ contact.is_primary && 'fill-current'
1527
+ )}
1528
+ />
1529
+ </Button>
621
1530
 
622
- <div className="flex items-center text-muted-foreground">
623
- <ChevronDown className="h-4 w-4 transition-transform group-data-[state=open]:rotate-180" />
1531
+ <Button
1532
+ type="button"
1533
+ variant="ghost"
1534
+ size="icon"
1535
+ className="h-8 w-8 shrink-0 text-red-500 hover:text-red-600"
1536
+ onClick={() =>
1537
+ removeContact(contact.clientId)
1538
+ }
1539
+ aria-label={personT('remove')}
1540
+ >
1541
+ <Trash2 className="h-4 w-4" />
1542
+ </Button>
1543
+ </div>
1544
+ </div>
1545
+ </div>
1546
+ ))
1547
+ )}
1548
+ </CollapsibleContent>
1549
+ </div>
1550
+ </Collapsible>
1551
+
1552
+ <Collapsible
1553
+ open={addressesOpen}
1554
+ onOpenChange={setAddressesOpen}
1555
+ >
1556
+ <div className="rounded-xl border bg-background p-3">
1557
+ <CollapsibleTrigger asChild>
1558
+ <div className="group flex w-full cursor-pointer flex-wrap items-center gap-2 text-left">
1559
+ <div className="flex min-w-0 items-center gap-2">
1560
+ <MapPin className="h-4 w-4 text-green-600" />
1561
+ <div>
1562
+ <p className="text-sm font-semibold text-foreground">
1563
+ {t('form.sections.addressesTitle')}
1564
+ </p>
1565
+ <p className="text-xs text-muted-foreground">
1566
+ {t('form.sections.addressesDescription')}
1567
+ </p>
1568
+ </div>
1569
+ {addresses.length > 0 ? (
1570
+ <Badge variant="secondary">
1571
+ {addresses.length}
1572
+ </Badge>
1573
+ ) : null}
1574
+ </div>
1575
+
1576
+ <div className="ml-auto flex items-center gap-1">
1577
+ <Button
1578
+ type="button"
1579
+ variant="ghost"
1580
+ size="sm"
1581
+ className="h-7 px-2 text-xs"
1582
+ onClick={(event) => {
1583
+ event.stopPropagation();
1584
+ addAddress();
1585
+ }}
1586
+ >
1587
+ <Plus className="mr-1 h-3.5 w-3.5" />
1588
+ {personT('addAddress')}
1589
+ </Button>
1590
+ {addressesOpen ? (
1591
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
1592
+ ) : (
1593
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
1594
+ )}
1595
+ </div>
624
1596
  </div>
625
- </button>
626
- </CollapsibleTrigger>
627
-
628
- <CollapsibleContent className="border-t px-3 pb-3 pt-3">
629
- <div className="space-y-3">
630
- <FormField
631
- control={form.control}
632
- name="industry"
633
- render={({ field }) => (
634
- <FormItem className="space-y-1.5">
635
- <FormLabel>{t('form.industry')}</FormLabel>
636
- <FormControl>
1597
+ </CollapsibleTrigger>
1598
+
1599
+ <CollapsibleContent className="mt-3 space-y-2">
1600
+ {addresses.length === 0 ? (
1601
+ <div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
1602
+ {personT('noAddresses')}
1603
+ </div>
1604
+ ) : (
1605
+ addresses.map((address) => (
1606
+ <div
1607
+ key={address.clientId}
1608
+ className={cn(
1609
+ 'space-y-2 rounded-lg border p-2',
1610
+ address.is_primary &&
1611
+ 'border-green-500/50 bg-green-500/5'
1612
+ )}
1613
+ >
1614
+ <div className="grid grid-cols-1 gap-2 lg:grid-cols-[180px_minmax(0,1fr)_auto_auto]">
1615
+ <Select
1616
+ value={address.address_type}
1617
+ onValueChange={(value) =>
1618
+ updateAddress(address.clientId, {
1619
+ address_type:
1620
+ value as AccountAddress['address_type'],
1621
+ })
1622
+ }
1623
+ >
1624
+ <SelectTrigger className="h-8 w-full text-xs">
1625
+ <SelectValue
1626
+ placeholder={personT('selectAddressType')}
1627
+ />
1628
+ </SelectTrigger>
1629
+ <SelectContent>
1630
+ {ADDRESS_TYPE_OPTIONS.map((option) => (
1631
+ <SelectItem
1632
+ key={option.value}
1633
+ value={option.value}
1634
+ >
1635
+ {personT(option.labelKey)}
1636
+ </SelectItem>
1637
+ ))}
1638
+ </SelectContent>
1639
+ </Select>
1640
+
1641
+ <div className="relative">
1642
+ <Input
1643
+ placeholder={personT('zipCode')}
1644
+ value={address.postal_code || ''}
1645
+ maxLength={9}
1646
+ onChange={(event) =>
1647
+ void handleCEP(event, address.clientId)
1648
+ }
1649
+ className="h-8 w-full text-xs"
1650
+ />
1651
+ {loadingCEP[address.clientId] ? (
1652
+ <Loader2 className="absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2 animate-spin text-muted-foreground" />
1653
+ ) : null}
1654
+ </div>
1655
+
1656
+ <Button
1657
+ type="button"
1658
+ variant="ghost"
1659
+ size="icon"
1660
+ className={cn(
1661
+ 'h-8 w-8 shrink-0',
1662
+ address.is_primary && 'text-amber-500'
1663
+ )}
1664
+ onClick={() =>
1665
+ setPrimaryAddress(address.clientId)
1666
+ }
1667
+ aria-label={personT('main')}
1668
+ >
1669
+ <Star
1670
+ className={cn(
1671
+ 'h-4 w-4',
1672
+ address.is_primary && 'fill-current'
1673
+ )}
1674
+ />
1675
+ </Button>
1676
+
1677
+ <Button
1678
+ type="button"
1679
+ variant="ghost"
1680
+ size="icon"
1681
+ className="h-8 w-8 shrink-0 text-red-500 hover:text-red-600"
1682
+ onClick={() =>
1683
+ removeAddress(address.clientId)
1684
+ }
1685
+ aria-label={personT('remove')}
1686
+ >
1687
+ <Trash2 className="h-4 w-4" />
1688
+ </Button>
1689
+ </div>
1690
+
1691
+ <Input
1692
+ ref={(element) => {
1693
+ addressLine1Refs.current[address.clientId] =
1694
+ element;
1695
+ }}
1696
+ placeholder={personT('addressPlaceholder')}
1697
+ value={address.line1}
1698
+ disabled={Boolean(loadingCEP[address.clientId])}
1699
+ onChange={(event) =>
1700
+ updateAddress(address.clientId, {
1701
+ line1: event.target.value,
1702
+ })
1703
+ }
1704
+ className="h-8 text-xs"
1705
+ />
1706
+
1707
+ <Input
1708
+ placeholder={personT(
1709
+ 'addressComplementPlaceholder'
1710
+ )}
1711
+ value={address.line2 || ''}
1712
+ onChange={(event) =>
1713
+ updateAddress(address.clientId, {
1714
+ line2: event.target.value,
1715
+ })
1716
+ }
1717
+ className="h-8 text-xs"
1718
+ />
1719
+
1720
+ <div className="grid grid-cols-1 gap-2 md:grid-cols-3">
637
1721
  <Input
638
- placeholder={t('form.placeholders.industry')}
639
- value={field.value ?? ''}
1722
+ placeholder={personT(
1723
+ 'addressCityPlaceholder'
1724
+ )}
1725
+ value={address.city}
1726
+ disabled={Boolean(
1727
+ loadingCEP[address.clientId]
1728
+ )}
640
1729
  onChange={(event) =>
641
- field.onChange(event.target.value || null)
1730
+ updateAddress(address.clientId, {
1731
+ city: event.target.value,
1732
+ })
642
1733
  }
643
- disabled={isLoading}
1734
+ className="h-8 text-xs"
644
1735
  />
645
- </FormControl>
646
- <FormMessage />
647
- </FormItem>
648
- )}
649
- />
1736
+ <Input
1737
+ placeholder={personT(
1738
+ 'addressStatePlaceholder'
1739
+ )}
1740
+ value={address.state}
1741
+ maxLength={2}
1742
+ disabled={Boolean(
1743
+ loadingCEP[address.clientId]
1744
+ )}
1745
+ onChange={(event) =>
1746
+ updateAddress(address.clientId, {
1747
+ state: event.target.value.toUpperCase(),
1748
+ })
1749
+ }
1750
+ className="h-8 text-xs uppercase"
1751
+ />
1752
+ <Input
1753
+ placeholder={personT('addressCountry')}
1754
+ value={address.country_code || 'BRA'}
1755
+ onChange={(event) =>
1756
+ updateAddress(address.clientId, {
1757
+ country_code:
1758
+ event.target.value.toUpperCase(),
1759
+ })
1760
+ }
1761
+ className="h-8 text-xs uppercase"
1762
+ />
1763
+ </div>
1764
+ </div>
1765
+ ))
1766
+ )}
1767
+ </CollapsibleContent>
1768
+ </div>
1769
+ </Collapsible>
1770
+
1771
+ <Collapsible
1772
+ open={documentsOpen}
1773
+ onOpenChange={setDocumentsOpen}
1774
+ >
1775
+ <div className="rounded-xl border bg-background p-3">
1776
+ <CollapsibleTrigger asChild>
1777
+ <div className="group flex w-full cursor-pointer flex-wrap items-center gap-2 text-left">
1778
+ <div className="flex min-w-0 items-center gap-2">
1779
+ <FileText className="h-4 w-4 text-amber-600" />
1780
+ <div>
1781
+ <p className="text-sm font-semibold text-foreground">
1782
+ {t('form.sections.documentsTitle')}
1783
+ </p>
1784
+ <p className="text-xs text-muted-foreground">
1785
+ {t('form.sections.documentsDescription')}
1786
+ </p>
1787
+ </div>
1788
+ {documents.length > 0 ? (
1789
+ <Badge variant="secondary">
1790
+ {documents.length}
1791
+ </Badge>
1792
+ ) : null}
1793
+ </div>
1794
+
1795
+ <div className="ml-auto flex max-w-full flex-wrap items-center justify-end gap-1 sm:flex-nowrap">
1796
+ <Button
1797
+ type="button"
1798
+ variant="ghost"
1799
+ size="sm"
1800
+ className="h-7 px-2 text-xs"
1801
+ onClick={(event) => {
1802
+ event.stopPropagation();
1803
+ addDocument('cnpj');
1804
+ }}
1805
+ >
1806
+ <Plus className="mr-1 h-3.5 w-3.5" />
1807
+ CNPJ
1808
+ </Button>
1809
+ <Button
1810
+ type="button"
1811
+ variant="ghost"
1812
+ size="sm"
1813
+ className="h-7 px-2 text-xs"
1814
+ onClick={(event) => {
1815
+ event.stopPropagation();
1816
+ addDocument('blank');
1817
+ }}
1818
+ >
1819
+ <Plus className="mr-1 h-3.5 w-3.5" />
1820
+ {personT('addDocument')}
1821
+ </Button>
1822
+ {documentsOpen ? (
1823
+ <ChevronUp className="h-4 w-4 text-muted-foreground" />
1824
+ ) : (
1825
+ <ChevronDown className="h-4 w-4 text-muted-foreground" />
1826
+ )}
1827
+ </div>
1828
+ </div>
1829
+ </CollapsibleTrigger>
1830
+
1831
+ <CollapsibleContent className="mt-3 space-y-2">
1832
+ {documents.length === 0 ? (
1833
+ <div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
1834
+ {personT('noDocuments')}
1835
+ </div>
1836
+ ) : (
1837
+ documents.map((document) => (
1838
+ <div
1839
+ key={document.clientId}
1840
+ className="rounded-lg border p-2"
1841
+ >
1842
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-center">
1843
+ <Select
1844
+ value={
1845
+ document.document_type_id
1846
+ ? String(document.document_type_id)
1847
+ : undefined
1848
+ }
1849
+ onValueChange={(value) => {
1850
+ const nextTypeId = Number(value);
1851
+ updateDocument(document.clientId, {
1852
+ document_type_id: nextTypeId,
1853
+ value: maskDocumentValueByType(
1854
+ document.value || '',
1855
+ nextTypeId
1856
+ ),
1857
+ });
1858
+ }}
1859
+ >
1860
+ <SelectTrigger className="h-8 w-full text-xs sm:w-40">
1861
+ <SelectValue
1862
+ placeholder={personT(
1863
+ 'selectDocumentType'
1864
+ )}
1865
+ />
1866
+ </SelectTrigger>
1867
+ <SelectContent>
1868
+ {documentTypes.map((documentType) => (
1869
+ <SelectItem
1870
+ key={documentType.document_type_id}
1871
+ value={String(
1872
+ documentType.document_type_id
1873
+ )}
1874
+ >
1875
+ {documentType.name}
1876
+ </SelectItem>
1877
+ ))}
1878
+ </SelectContent>
1879
+ </Select>
1880
+
1881
+ <div className="flex flex-1 items-center gap-2">
1882
+ <Input
1883
+ ref={(element) => {
1884
+ documentValueRefs.current[
1885
+ document.clientId
1886
+ ] = element;
1887
+ }}
1888
+ placeholder={personT(
1889
+ 'documentValuePlaceholder'
1890
+ )}
1891
+ value={document.value}
1892
+ onChange={(event) =>
1893
+ updateDocument(document.clientId, {
1894
+ value: maskDocumentValueByType(
1895
+ event.target.value,
1896
+ document.document_type_id
1897
+ ),
1898
+ })
1899
+ }
1900
+ className="h-8 flex-1 text-xs"
1901
+ />
1902
+
1903
+ <Button
1904
+ type="button"
1905
+ variant="ghost"
1906
+ size="icon"
1907
+ className="h-8 w-8 shrink-0 text-red-500 hover:text-red-600"
1908
+ onClick={() =>
1909
+ removeDocument(document.clientId)
1910
+ }
1911
+ aria-label={personT('remove')}
1912
+ >
1913
+ <Trash2 className="h-4 w-4" />
1914
+ </Button>
1915
+ </div>
1916
+ </div>
1917
+ </div>
1918
+ ))
1919
+ )}
1920
+ </CollapsibleContent>
1921
+ </div>
1922
+ </Collapsible>
1923
+
1924
+ <Collapsible defaultOpen={additionalDetailsDefaultOpen}>
1925
+ <div className="rounded-xl border border-dashed bg-muted/10">
1926
+ <CollapsibleTrigger asChild>
1927
+ <button
1928
+ type="button"
1929
+ className="group flex w-full items-center justify-between gap-3 px-3 py-3 text-left transition-colors hover:bg-muted/20"
1930
+ >
1931
+ <div>
1932
+ <p className="text-sm font-semibold text-foreground">
1933
+ {t('form.sections.additionalTitle')}
1934
+ </p>
1935
+ <p className="text-xs text-muted-foreground">
1936
+ {t('form.sections.additionalDescription')}
1937
+ </p>
1938
+ </div>
1939
+
1940
+ <div className="flex items-center text-muted-foreground">
1941
+ <ChevronDown className="h-4 w-4 transition-transform group-data-[state=open]:rotate-180" />
1942
+ </div>
1943
+ </button>
1944
+ </CollapsibleTrigger>
1945
+
1946
+ <CollapsibleContent className="border-t px-3 pb-3 pt-3">
1947
+ <div className="grid grid-cols-1 gap-3 md:grid-cols-2 xl:grid-cols-4">
1948
+ <FormField
1949
+ control={form.control}
1950
+ name="website"
1951
+ render={({ field }) => (
1952
+ <FormItem className="space-y-1.5 md:col-span-2 xl:col-span-2">
1953
+ <FormLabel>{t('form.website')}</FormLabel>
1954
+ <FormControl>
1955
+ <Input
1956
+ placeholder={t('form.placeholders.website')}
1957
+ autoCapitalize="none"
1958
+ autoCorrect="off"
1959
+ value={field.value ?? ''}
1960
+ onChange={(event) =>
1961
+ field.onChange(event.target.value || null)
1962
+ }
1963
+ disabled={isLoading}
1964
+ />
1965
+ </FormControl>
1966
+ <FormMessage />
1967
+ </FormItem>
1968
+ )}
1969
+ />
1970
+
1971
+ <FormField
1972
+ control={form.control}
1973
+ name="industry"
1974
+ render={({ field }) => (
1975
+ <FormItem className="space-y-1.5 md:col-span-2 xl:col-span-2">
1976
+ <FormLabel>{t('form.industry')}</FormLabel>
1977
+ <FormControl>
1978
+ <Input
1979
+ placeholder={t(
1980
+ 'form.placeholders.industry'
1981
+ )}
1982
+ value={field.value ?? ''}
1983
+ onChange={(event) =>
1984
+ field.onChange(event.target.value || null)
1985
+ }
1986
+ disabled={isLoading}
1987
+ />
1988
+ </FormControl>
1989
+ <FormMessage />
1990
+ </FormItem>
1991
+ )}
1992
+ />
650
1993
 
651
- <div className="grid grid-cols-1 gap-3 sm:grid-cols-2">
652
1994
  <FormField
653
1995
  control={form.control}
654
1996
  name="annual_revenue"
@@ -711,34 +2053,450 @@ export function AccountFormSheet({
711
2053
  )}
712
2054
  />
713
2055
  </div>
2056
+ </CollapsibleContent>
2057
+ </div>
2058
+ </Collapsible>
2059
+
2060
+ <section className="space-y-3 pb-2">
2061
+ <div className="flex flex-col gap-2 sm:flex-row sm:items-start sm:justify-between">
2062
+ <SectionHeader
2063
+ title={t('form.collaborators.title')}
2064
+ description={t('form.collaborators.description')}
2065
+ icon={<Users className="h-4 w-4" />}
2066
+ />
2067
+
2068
+ <div className="flex w-full flex-col gap-2 sm:w-auto sm:min-w-90 sm:flex-row sm:items-start sm:justify-end">
2069
+ <EntityPicker<Person>
2070
+ key={collaboratorPickerKey}
2071
+ value={null}
2072
+ placeholder="Buscar colaborador existente"
2073
+ entityLabel="colaborador"
2074
+ valueType="number"
2075
+ searchPlaceholder="Digite para buscar pelo nome"
2076
+ emptyStateDescription="Nenhum colaborador encontrado."
2077
+ loadingLabel="Carregando colaboradores..."
2078
+ showCreateButton={false}
2079
+ clearable={false}
2080
+ disabled={isLoading}
2081
+ className="w-full sm:min-w-65"
2082
+ buttonClassName="w-full"
2083
+ loadOptions={loadCollaboratorOptions}
2084
+ getOptionValue={(person) => person.id}
2085
+ getOptionLabel={(person) => person.name}
2086
+ getOptionDescription={(person) =>
2087
+ [
2088
+ person.job_title,
2089
+ person.status === 'active'
2090
+ ? t('status_active')
2091
+ : person.status === 'inactive'
2092
+ ? t('status_inactive')
2093
+ : undefined,
2094
+ ]
2095
+ .filter(Boolean)
2096
+ .join(' • ') || undefined
2097
+ }
2098
+ onChange={(value, option) => {
2099
+ void value;
2100
+ if (option) {
2101
+ upsertLocalCollaborator(option);
2102
+ setCollaboratorPickerKey(
2103
+ (current) => current + 1
2104
+ );
2105
+ }
2106
+ }}
2107
+ />
2108
+
2109
+ <div className="flex items-center gap-2 sm:self-start">
2110
+ <ToggleGroup
2111
+ type="single"
2112
+ value={collaboratorsViewMode}
2113
+ onValueChange={(v) => {
2114
+ if (v)
2115
+ setCollaboratorsViewMode(
2116
+ v as 'cards' | 'table'
2117
+ );
2118
+ }}
2119
+ variant="outline"
2120
+ size="sm"
2121
+ aria-label={t('form.collaborators.viewMode')}
2122
+ >
2123
+ <ToggleGroupItem
2124
+ value="cards"
2125
+ aria-label={t('form.collaborators.viewModeCards')}
2126
+ >
2127
+ <LayoutGrid className="h-4 w-4" />
2128
+ </ToggleGroupItem>
2129
+ <ToggleGroupItem
2130
+ value="table"
2131
+ aria-label={t('form.collaborators.viewModeTable')}
2132
+ >
2133
+ <List className="h-4 w-4" />
2134
+ </ToggleGroupItem>
2135
+ </ToggleGroup>
2136
+
2137
+ <Button
2138
+ type="button"
2139
+ size="sm"
2140
+ onClick={() => {
2141
+ setCollaboratorToEdit(null);
2142
+ setCollaboratorSheetOpen(true);
2143
+ }}
2144
+ disabled={isLoading}
2145
+ >
2146
+ <Plus className="mr-1 h-4 w-4" />
2147
+ {t('form.collaborators.addAction')}
2148
+ </Button>
2149
+ </div>
2150
+ </div>
2151
+ </div>
2152
+
2153
+ {collaborators.length === 0 ? (
2154
+ <div className="rounded-xl border border-dashed bg-muted/10 p-4 text-sm text-muted-foreground">
2155
+ {!isPersistedAccount ? (
2156
+ <>
2157
+ <p className="font-medium text-foreground">
2158
+ Busque um colaborador existente ou crie um novo
2159
+ cadastro.
2160
+ </p>
2161
+ <p className="mt-1 text-xs">
2162
+ Os vínculos serão salvos junto com a conta ao
2163
+ concluir este formulário.
2164
+ </p>
2165
+ </>
2166
+ ) : (
2167
+ t('form.collaborators.empty')
2168
+ )}
714
2169
  </div>
715
- </CollapsibleContent>
716
- </div>
717
- </Collapsible>
2170
+ ) : collaboratorsViewMode === 'cards' ? (
2171
+ <div className="grid gap-2 md:grid-cols-2">
2172
+ {collaborators.map((person) => {
2173
+ const email = getPersonPrimaryContact(person, [
2174
+ 'EMAIL',
2175
+ ]);
2176
+ const phone = getPersonPrimaryContact(person, [
2177
+ 'PHONE',
2178
+ 'MOBILE',
2179
+ 'WHATSAPP',
2180
+ ]);
2181
+ const isActive = person.status === 'active';
2182
+
2183
+ return (
2184
+ <div
2185
+ key={person.id}
2186
+ className="rounded-xl border bg-background p-2"
2187
+ >
2188
+ <div className="flex items-center justify-between gap-2">
2189
+ <div className="flex min-w-0 items-center gap-2">
2190
+ <Avatar className="h-8 w-8 shrink-0">
2191
+ <AvatarFallback className="text-xs">
2192
+ {getPersonInitials(person.name)}
2193
+ </AvatarFallback>
2194
+ </Avatar>
2195
+
2196
+ <div className="min-w-0">
2197
+ <div className="flex items-center gap-1">
2198
+ <Tooltip>
2199
+ <TooltipTrigger asChild>
2200
+ <span>
2201
+ {isActive ? (
2202
+ <CheckCircle2 className="h-3.5 w-3.5 shrink-0 text-green-500" />
2203
+ ) : (
2204
+ <XCircle className="h-3.5 w-3.5 shrink-0 text-red-400" />
2205
+ )}
2206
+ </span>
2207
+ </TooltipTrigger>
2208
+ <TooltipContent>
2209
+ <p>
2210
+ {isActive
2211
+ ? t('status_active')
2212
+ : t('status_inactive')}
2213
+ </p>
2214
+ </TooltipContent>
2215
+ </Tooltip>
2216
+ <p className="truncate text-sm font-semibold text-foreground">
2217
+ {person.name}
2218
+ </p>
2219
+ <CopyButton
2220
+ value={person.name}
2221
+ className="h-5 w-5 shrink-0"
2222
+ />
2223
+ </div>
2224
+ {person.job_title ? (
2225
+ <p className="truncate text-xs text-muted-foreground">
2226
+ {person.job_title}
2227
+ </p>
2228
+ ) : null}
2229
+ </div>
2230
+ </div>
2231
+
2232
+ <div className="flex shrink-0 items-center gap-1">
2233
+ <Tooltip>
2234
+ <TooltipTrigger asChild>
2235
+ <Button
2236
+ type="button"
2237
+ variant="ghost"
2238
+ size="icon"
2239
+ className="h-7 w-7"
2240
+ onClick={() => {
2241
+ setCollaboratorToEdit(person);
2242
+ setCollaboratorSheetOpen(true);
2243
+ }}
2244
+ aria-label={t(
2245
+ 'form.collaborators.editActionLabel'
2246
+ )}
2247
+ >
2248
+ <Pencil className="h-3.5 w-3.5" />
2249
+ </Button>
2250
+ </TooltipTrigger>
2251
+ <TooltipContent>
2252
+ <p>
2253
+ {t(
2254
+ 'form.collaborators.editActionLabel'
2255
+ )}
2256
+ </p>
2257
+ </TooltipContent>
2258
+ </Tooltip>
2259
+
2260
+ <Button
2261
+ type="button"
2262
+ variant="ghost"
2263
+ size="icon"
2264
+ className="h-7 w-7 shrink-0 text-red-500 hover:text-red-600"
2265
+ onClick={() =>
2266
+ removeCollaborator(Number(person.id))
2267
+ }
2268
+ aria-label={personT('remove')}
2269
+ >
2270
+ <Trash2 className="h-3.5 w-3.5" />
2271
+ </Button>
2272
+ </div>
2273
+ </div>
2274
+
2275
+ {(email ?? phone) ? (
2276
+ <div className="mt-2 space-y-0.5 text-xs text-muted-foreground">
2277
+ {email ? (
2278
+ <div className="flex items-center gap-1">
2279
+ <Mail className="h-3.5 w-3.5 shrink-0" />
2280
+ <span className="truncate">{email}</span>
2281
+ <CopyButton
2282
+ value={email}
2283
+ className="h-5 w-5 shrink-0"
2284
+ />
2285
+ </div>
2286
+ ) : null}
2287
+ {phone ? (
2288
+ <div className="flex items-center gap-1">
2289
+ <Phone className="h-3.5 w-3.5 shrink-0" />
2290
+ <span>{phone}</span>
2291
+ </div>
2292
+ ) : null}
2293
+ </div>
2294
+ ) : null}
2295
+ </div>
2296
+ );
2297
+ })}
2298
+ </div>
2299
+ ) : (
2300
+ <div className="overflow-x-auto rounded-xl border bg-background">
2301
+ <table className="w-full text-xs">
2302
+ <thead>
2303
+ <tr className="border-b bg-muted/40 text-muted-foreground">
2304
+ <th className="px-3 py-2 text-left font-medium">
2305
+ {t('form.collaborators.tableColName')}
2306
+ </th>
2307
+ <th className="px-3 py-2 text-left font-medium">
2308
+ {t('form.collaborators.tableColJobTitle')}
2309
+ </th>
2310
+ <th className="px-3 py-2 text-center font-medium">
2311
+ {t('form.collaborators.tableColStatus')}
2312
+ </th>
2313
+ <th className="px-3 py-2 text-left font-medium">
2314
+ {t('form.collaborators.tableColEmail')}
2315
+ </th>
2316
+ <th className="px-3 py-2 text-left font-medium">
2317
+ {t('form.collaborators.tableColPhone')}
2318
+ </th>
2319
+ <th className="px-3 py-2 text-right font-medium">
2320
+ {t('form.collaborators.tableColActions')}
2321
+ </th>
2322
+ </tr>
2323
+ </thead>
2324
+ <tbody>
2325
+ {collaborators.map((person) => {
2326
+ const email = getPersonPrimaryContact(person, [
2327
+ 'EMAIL',
2328
+ ]);
2329
+ const phone = getPersonPrimaryContact(person, [
2330
+ 'PHONE',
2331
+ 'MOBILE',
2332
+ 'WHATSAPP',
2333
+ ]);
2334
+ const isActive = person.status === 'active';
2335
+
2336
+ return (
2337
+ <tr
2338
+ key={person.id}
2339
+ className="border-b last:border-0 transition-colors hover:bg-muted/40"
2340
+ >
2341
+ <td className="px-3 py-2">
2342
+ <div className="flex items-center gap-2">
2343
+ <Avatar className="h-6 w-6 shrink-0">
2344
+ <AvatarFallback className="text-[10px]">
2345
+ {getPersonInitials(person.name)}
2346
+ </AvatarFallback>
2347
+ </Avatar>
2348
+ <span className="max-w-35 truncate font-medium text-foreground">
2349
+ {person.name}
2350
+ </span>
2351
+ <CopyButton
2352
+ value={person.name}
2353
+ className="h-5 w-5 shrink-0"
2354
+ />
2355
+ </div>
2356
+ </td>
2357
+ <td className="max-w-30 truncate px-3 py-2 text-muted-foreground">
2358
+ {person.job_title ?? '—'}
2359
+ </td>
2360
+ <td className="px-3 py-2 text-center">
2361
+ <Tooltip>
2362
+ <TooltipTrigger asChild>
2363
+ <span className="inline-flex">
2364
+ {isActive ? (
2365
+ <CheckCircle2 className="h-3.5 w-3.5 text-green-500" />
2366
+ ) : (
2367
+ <XCircle className="h-3.5 w-3.5 text-red-400" />
2368
+ )}
2369
+ </span>
2370
+ </TooltipTrigger>
2371
+ <TooltipContent>
2372
+ <p>
2373
+ {isActive
2374
+ ? t('status_active')
2375
+ : t('status_inactive')}
2376
+ </p>
2377
+ </TooltipContent>
2378
+ </Tooltip>
2379
+ </td>
2380
+ <td className="px-3 py-2">
2381
+ {email ? (
2382
+ <div className="flex items-center gap-1">
2383
+ <span className="max-w-40 truncate text-muted-foreground">
2384
+ {email}
2385
+ </span>
2386
+ <CopyButton
2387
+ value={email}
2388
+ className="h-5 w-5 shrink-0"
2389
+ />
2390
+ </div>
2391
+ ) : (
2392
+ <span className="text-muted-foreground">
2393
+
2394
+ </span>
2395
+ )}
2396
+ </td>
2397
+ <td className="px-3 py-2 text-muted-foreground">
2398
+ {phone ?? '—'}
2399
+ </td>
2400
+ <td className="px-3 py-2">
2401
+ <div className="flex items-center justify-end gap-1">
2402
+ <Tooltip>
2403
+ <TooltipTrigger asChild>
2404
+ <Button
2405
+ type="button"
2406
+ variant="ghost"
2407
+ size="icon"
2408
+ className="h-7 w-7"
2409
+ onClick={() => {
2410
+ setCollaboratorToEdit(person);
2411
+ setCollaboratorSheetOpen(true);
2412
+ }}
2413
+ aria-label={t(
2414
+ 'form.collaborators.editActionLabel'
2415
+ )}
2416
+ >
2417
+ <Pencil className="h-3.5 w-3.5" />
2418
+ </Button>
2419
+ </TooltipTrigger>
2420
+ <TooltipContent>
2421
+ <p>
2422
+ {t(
2423
+ 'form.collaborators.editActionLabel'
2424
+ )}
2425
+ </p>
2426
+ </TooltipContent>
2427
+ </Tooltip>
2428
+ <Button
2429
+ type="button"
2430
+ variant="ghost"
2431
+ size="icon"
2432
+ className="h-7 w-7 text-red-500 hover:text-red-600"
2433
+ onClick={() =>
2434
+ removeCollaborator(Number(person.id))
2435
+ }
2436
+ aria-label={personT('remove')}
2437
+ >
2438
+ <Trash2 className="h-3.5 w-3.5" />
2439
+ </Button>
2440
+ </div>
2441
+ </td>
2442
+ </tr>
2443
+ );
2444
+ })}
2445
+ </tbody>
2446
+ </table>
2447
+ </div>
2448
+ )}
2449
+ </section>
2450
+ </div>
718
2451
  </div>
719
- </div>
720
2452
 
721
- <FormActions
722
- sheet
723
- cancelLabel={t('cancel')}
724
- onCancel={() => {
725
- if (!isLoading) {
726
- onOpenChange(false);
2453
+ <FormActions
2454
+ sheet
2455
+ statusContent={draftStatusContent}
2456
+ cancelLabel={t('cancel')}
2457
+ onCancel={() => {
2458
+ if (!isLoading) {
2459
+ onOpenChange(false);
2460
+ }
2461
+ }}
2462
+ submitDisabled={isLoading}
2463
+ submitLabel={
2464
+ isLoading
2465
+ ? t('form.saving')
2466
+ : isPersistedAccount
2467
+ ? t('form.updateSubmit')
2468
+ : t('form.createSubmit')
727
2469
  }
728
- }}
729
- submitDisabled={isLoading}
730
- submitLabel={
731
- isLoading
732
- ? t('form.saving')
733
- : account
734
- ? t('form.updateSubmit')
735
- : t('form.createSubmit')
736
- }
737
- submitType="submit"
738
- />
739
- </form>
740
- </Form>
741
- </SheetContent>
742
- </Sheet>
2470
+ submitType="submit"
2471
+ />
2472
+ </form>
2473
+ </Form>
2474
+ </SheetContent>
2475
+ </Sheet>
2476
+
2477
+ <PersonFormSheet
2478
+ open={collaboratorSheetOpen}
2479
+ person={collaboratorToEdit}
2480
+ contactTypes={contactTypes}
2481
+ documentTypes={documentTypes}
2482
+ initialEmployerCompanyId={currentAccountId}
2483
+ initialEmployerCompanyLabel={companyPreviewName}
2484
+ onOpenChange={(nextOpen) => {
2485
+ setCollaboratorSheetOpen(nextOpen);
2486
+ if (!nextOpen) {
2487
+ setCollaboratorToEdit(null);
2488
+ }
2489
+ }}
2490
+ onSuccess={(savedPerson) => {
2491
+ if (savedPerson) {
2492
+ upsertLocalCollaborator(savedPerson);
2493
+ }
2494
+
2495
+ if (currentAccountId) {
2496
+ void refetchCollaborators();
2497
+ }
2498
+ }}
2499
+ />
2500
+ </>
743
2501
  );
744
2502
  }