@hed-hog/contact 0.0.270 → 0.0.274

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.
@@ -26,21 +26,21 @@ import {
26
26
  SelectValue,
27
27
  } from '@/components/ui/select';
28
28
  import { Separator } from '@/components/ui/separator';
29
- import {
30
- Sheet,
31
- SheetContent,
32
- SheetDescription,
33
- SheetHeader,
34
- SheetTitle,
35
- } from '@/components/ui/sheet';
36
- import {
37
- Tooltip,
38
- TooltipContent,
39
- TooltipTrigger,
40
- } from '@/components/ui/tooltip';
29
+ import {
30
+ Sheet,
31
+ SheetContent,
32
+ SheetDescription,
33
+ SheetHeader,
34
+ SheetTitle,
35
+ } from '@/components/ui/sheet';
36
+ import {
37
+ Tooltip,
38
+ TooltipContent,
39
+ TooltipTrigger,
40
+ } from '@/components/ui/tooltip';
41
41
  import { COUNTRIES } from '@/constants/countries';
42
42
  import { cn } from '@/lib/utils';
43
- import { useApp } from '@hed-hog/next-app-provider';
43
+ import { useApp, useQuery } from '@hed-hog/next-app-provider';
44
44
  import { zodResolver } from '@hookform/resolvers/zod';
45
45
  import { format } from 'date-fns';
46
46
  import { enUS, ptBR } from 'date-fns/locale';
@@ -75,6 +75,9 @@ import {
75
75
  type PersonContact,
76
76
  type PersonDocument,
77
77
  type PersonGender,
78
+ type PersonLifecycleStage,
79
+ type PersonSource,
80
+ type UserOption,
78
81
  } from './person-types';
79
82
 
80
83
  type PersonFormSheetProps = {
@@ -97,6 +100,10 @@ type PersonFormValues = {
97
100
  trade_name?: string | null;
98
101
  foundation_date?: Date | null;
99
102
  legal_nature?: string | null;
103
+ owner_user_id?: number | null;
104
+ source?: PersonSource | null;
105
+ lifecycle_stage?: PersonLifecycleStage | null;
106
+ next_action_at?: string | null;
100
107
  };
101
108
 
102
109
  type CreatePersonPayload = {
@@ -115,73 +122,81 @@ type OpenFilePayload = {
115
122
  url?: string;
116
123
  };
117
124
 
118
- type EditablePersonContact = Omit<PersonContact, 'contact_type_id'> & {
119
- clientId: string;
120
- contact_type_id: number | null;
121
- };
125
+ type EditablePersonContact = Omit<PersonContact, 'contact_type_id'> & {
126
+ clientId: string;
127
+ contact_type_id: number | null;
128
+ };
122
129
 
123
130
  type EditablePersonAddress = PersonAddress & {
124
131
  clientId: string;
125
132
  };
126
133
 
127
- type EditablePersonDocument = Omit<PersonDocument, 'document_type_id'> & {
128
- clientId: string;
129
- document_type_id: number | null;
130
- };
131
-
132
- function createClientId(prefix: string) {
133
- return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
134
- }
135
-
136
- function onlyDigits(value: string) {
137
- return value.replace(/\D/g, '');
138
- }
139
-
140
- function applyPhoneMask(value: string) {
141
- const digits = onlyDigits(value).slice(0, 11);
142
-
143
- if (digits.length <= 2) {
144
- return digits.length ? `(${digits}` : '';
145
- }
146
-
147
- if (digits.length <= 6) {
148
- return `(${digits.slice(0, 2)}) ${digits.slice(2)}`;
149
- }
150
-
151
- if (digits.length <= 10) {
152
- return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
153
- }
154
-
155
- return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
156
- }
157
-
158
- function applyCpfMask(value: string) {
159
- const digits = onlyDigits(value).slice(0, 11);
160
-
161
- if (digits.length <= 3) return digits;
162
- if (digits.length <= 6)
163
- return `${digits.slice(0, 3)}.${digits.slice(3)}`;
164
- if (digits.length <= 9)
165
- return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
166
-
167
- return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`;
168
- }
169
-
170
- function applyCnpjMask(value: string) {
171
- const digits = onlyDigits(value).slice(0, 14);
172
-
173
- if (digits.length <= 2) return digits;
174
- if (digits.length <= 5)
175
- return `${digits.slice(0, 2)}.${digits.slice(2)}`;
176
- if (digits.length <= 8)
177
- return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5)}`;
178
- if (digits.length <= 12)
179
- return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}/${digits.slice(8)}`;
180
-
181
- return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}/${digits.slice(8, 12)}-${digits.slice(12)}`;
182
- }
183
-
184
- async function fetchViaCep(cep: string) {
134
+ type EditablePersonDocument = Omit<PersonDocument, 'document_type_id'> & {
135
+ clientId: string;
136
+ document_type_id: number | null;
137
+ };
138
+
139
+ function createClientId(prefix: string) {
140
+ return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
141
+ }
142
+
143
+ function onlyDigits(value: string) {
144
+ return value.replace(/\D/g, '');
145
+ }
146
+
147
+ function applyPhoneMask(value: string) {
148
+ const digits = onlyDigits(value).slice(0, 11);
149
+
150
+ if (digits.length <= 2) {
151
+ return digits.length ? `(${digits}` : '';
152
+ }
153
+
154
+ if (digits.length <= 6) {
155
+ return `(${digits.slice(0, 2)}) ${digits.slice(2)}`;
156
+ }
157
+
158
+ if (digits.length <= 10) {
159
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 6)}-${digits.slice(6)}`;
160
+ }
161
+
162
+ return `(${digits.slice(0, 2)}) ${digits.slice(2, 7)}-${digits.slice(7)}`;
163
+ }
164
+
165
+ function applyCpfMask(value: string) {
166
+ const digits = onlyDigits(value).slice(0, 11);
167
+
168
+ if (digits.length <= 3) return digits;
169
+ if (digits.length <= 6) return `${digits.slice(0, 3)}.${digits.slice(3)}`;
170
+ if (digits.length <= 9)
171
+ return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6)}`;
172
+
173
+ return `${digits.slice(0, 3)}.${digits.slice(3, 6)}.${digits.slice(6, 9)}-${digits.slice(9)}`;
174
+ }
175
+
176
+ function applyCnpjMask(value: string) {
177
+ const digits = onlyDigits(value).slice(0, 14);
178
+
179
+ if (digits.length <= 2) return digits;
180
+ if (digits.length <= 5) return `${digits.slice(0, 2)}.${digits.slice(2)}`;
181
+ if (digits.length <= 8)
182
+ return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5)}`;
183
+ if (digits.length <= 12)
184
+ return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}/${digits.slice(8)}`;
185
+
186
+ return `${digits.slice(0, 2)}.${digits.slice(2, 5)}.${digits.slice(5, 8)}/${digits.slice(8, 12)}-${digits.slice(12)}`;
187
+ }
188
+
189
+ function toDatetimeLocalValue(value?: string | null) {
190
+ if (!value) return '';
191
+ const date = new Date(value);
192
+ if (Number.isNaN(date.getTime())) return '';
193
+
194
+ const offset = date.getTimezoneOffset();
195
+ const localDate = new Date(date.getTime() - offset * 60 * 1000);
196
+ return localDate.toISOString().slice(0, 16);
197
+ }
198
+
199
+ async function fetchViaCep(cep: string) {
185
200
  try {
186
201
  const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
187
202
  if (!response.ok) return null;
@@ -253,7 +268,7 @@ function DatePickerWithYearMonth({
253
268
  type="button"
254
269
  variant="outline"
255
270
  className={cn(
256
- 'h-9 w-full min-w-0 justify-start text-left font-normal',
271
+ 'h-9 w-full min-w-0 justify-start text-left text-xs font-normal',
257
272
  !date && 'text-muted-foreground'
258
273
  )}
259
274
  >
@@ -324,7 +339,7 @@ export function PersonFormSheet({
324
339
  onSuccess,
325
340
  }: PersonFormSheetProps) {
326
341
  const t = useTranslations('contact.ContactPage');
327
- const { request, currentLocaleCode, getSettingValue } = useApp();
342
+ const { request, currentLocaleCode, getSettingValue, user } = useApp();
328
343
  const isEditing = Boolean(person);
329
344
  const allowCompanyRegistration =
330
345
  getSettingValue('contact-allow-company-registration') !== false;
@@ -343,6 +358,31 @@ export function PersonFormSheet({
343
358
  trade_name: z.string().nullable().optional(),
344
359
  foundation_date: z.date().nullable().optional(),
345
360
  legal_nature: z.string().nullable().optional(),
361
+ owner_user_id: z.number().int().nullable().optional(),
362
+ source: z
363
+ .enum([
364
+ 'referral',
365
+ 'website',
366
+ 'social',
367
+ 'inbound',
368
+ 'outbound',
369
+ 'other',
370
+ ])
371
+ .nullable()
372
+ .optional(),
373
+ lifecycle_stage: z
374
+ .enum([
375
+ 'new',
376
+ 'contacted',
377
+ 'qualified',
378
+ 'proposal',
379
+ 'negotiation',
380
+ 'customer',
381
+ 'lost',
382
+ ])
383
+ .nullable()
384
+ .optional(),
385
+ next_action_at: z.string().nullable().optional(),
346
386
  }),
347
387
  [t]
348
388
  );
@@ -360,6 +400,10 @@ export function PersonFormSheet({
360
400
  trade_name: '',
361
401
  foundation_date: null,
362
402
  legal_nature: '',
403
+ owner_user_id: null,
404
+ source: null,
405
+ lifecycle_stage: 'new',
406
+ next_action_at: '',
363
407
  },
364
408
  });
365
409
  const {
@@ -383,32 +427,46 @@ export function PersonFormSheet({
383
427
  const [persistedAvatarId, setPersistedAvatarId] = useState<number | null>(
384
428
  null
385
429
  );
386
- const [avatarPreviewUrl, setAvatarPreviewUrl] =
387
- useState<string>('/placeholder.png');
388
- const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
389
- const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
390
- const fileInputRef = useRef<HTMLInputElement>(null);
391
- const hasSavedChangesRef = useRef(false);
392
- const contactValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
393
- const addressLine1Refs = useRef<Record<string, HTMLInputElement | null>>({});
394
- const documentValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
395
-
396
- const focusWhenAvailable = (getElement: () => HTMLInputElement | null) => {
397
- requestAnimationFrame(() => {
398
- const element = getElement();
399
- if (!element) {
400
- setTimeout(() => {
401
- getElement()?.focus();
402
- }, 0);
403
- return;
404
- }
405
-
406
- element.focus();
407
- });
408
- };
430
+ const [avatarPreviewUrl, setAvatarPreviewUrl] =
431
+ useState<string>('/placeholder.png');
432
+ const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
433
+ const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
434
+ const fileInputRef = useRef<HTMLInputElement>(null);
435
+ const hasSavedChangesRef = useRef(false);
436
+ const contactValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
437
+ const addressLine1Refs = useRef<Record<string, HTMLInputElement | null>>({});
438
+ const documentValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
439
+
440
+ const focusWhenAvailable = (getElement: () => HTMLInputElement | null) => {
441
+ requestAnimationFrame(() => {
442
+ const element = getElement();
443
+ if (!element) {
444
+ setTimeout(() => {
445
+ getElement()?.focus();
446
+ }, 0);
447
+ return;
448
+ }
449
+
450
+ element.focus();
451
+ });
452
+ };
409
453
 
410
454
  const watchType = watch('type');
411
455
 
456
+ const { data: ownerOptions = [] } = useQuery<UserOption[]>({
457
+ queryKey: ['contact-person-owner-options', currentLocaleCode],
458
+ queryFn: async () => {
459
+ const response = await request<UserOption[] | { data?: UserOption[] }>({
460
+ url: '/person/owner-options',
461
+ method: 'GET',
462
+ });
463
+ return Array.isArray(response.data)
464
+ ? response.data
465
+ : response.data?.data || [];
466
+ },
467
+ placeholderData: (old: UserOption[] | undefined) => old ?? [],
468
+ });
469
+
412
470
  useEffect(() => {
413
471
  if (canUseCompanyType || watchType !== 'company') {
414
472
  return;
@@ -436,6 +494,12 @@ export function PersonFormSheet({
436
494
  ? new Date(person.foundation_date)
437
495
  : null,
438
496
  legal_nature: person?.legal_nature || '',
497
+ owner_user_id:
498
+ person?.owner_user_id ??
499
+ (person ? null : Number(user?.id || 0) || null),
500
+ source: person?.source || null,
501
+ lifecycle_stage: person?.lifecycle_stage || 'new',
502
+ next_action_at: toDatetimeLocalValue(person?.next_action_at || null),
439
503
  });
440
504
  const initialAvatarId = person?.avatar_id ?? null;
441
505
  setAvatarId(initialAvatarId);
@@ -444,17 +508,17 @@ export function PersonFormSheet({
444
508
  setIsUploadingAvatar(false);
445
509
  setAvatarUploadProgress(0);
446
510
 
447
- setContacts(
448
- (person?.contact || []).map((contact, index) => ({
449
- ...contact,
450
- value: ['PHONE', 'MOBILE', 'WHATSAPP'].includes(
451
- String(contact.contact_type?.code || '').toUpperCase()
452
- )
453
- ? applyPhoneMask(contact.value || '')
454
- : contact.value || '',
455
- clientId: `contact-${contact.id ?? index}`,
456
- }))
457
- );
511
+ setContacts(
512
+ (person?.contact || []).map((contact, index) => ({
513
+ ...contact,
514
+ value: ['PHONE', 'MOBILE', 'WHATSAPP'].includes(
515
+ String(contact.contact_type?.code || '').toUpperCase()
516
+ )
517
+ ? applyPhoneMask(contact.value || '')
518
+ : contact.value || '',
519
+ clientId: `contact-${contact.id ?? index}`,
520
+ }))
521
+ );
458
522
 
459
523
  setAddresses(
460
524
  (person?.address || []).map((address, index) => ({
@@ -463,25 +527,25 @@ export function PersonFormSheet({
463
527
  }))
464
528
  );
465
529
 
466
- setDocuments(
467
- (person?.document || []).map((document, index) => ({
468
- ...document,
469
- value:
470
- String(document.document_type?.code || '').toUpperCase() === 'CPF'
471
- ? applyCpfMask(document.value || '')
472
- : String(document.document_type?.code || '').toUpperCase() ===
473
- 'CNPJ'
474
- ? applyCnpjMask(document.value || '')
475
- : document.value || '',
476
- clientId: `document-${document.id ?? index}`,
477
- }))
478
- );
530
+ setDocuments(
531
+ (person?.document || []).map((document, index) => ({
532
+ ...document,
533
+ value:
534
+ String(document.document_type?.code || '').toUpperCase() === 'CPF'
535
+ ? applyCpfMask(document.value || '')
536
+ : String(document.document_type?.code || '').toUpperCase() ===
537
+ 'CNPJ'
538
+ ? applyCnpjMask(document.value || '')
539
+ : document.value || '',
540
+ clientId: `document-${document.id ?? index}`,
541
+ }))
542
+ );
479
543
 
480
544
  setContactsOpen(true);
481
545
  setAddressesOpen(true);
482
546
  setDocumentsOpen(true);
483
547
  setLoadingCEP({});
484
- }, [allowCompanyRegistration, open, person, reset]);
548
+ }, [allowCompanyRegistration, open, person, reset, user?.id]);
485
549
 
486
550
  const getPersonInitials = (name: string) =>
487
551
  name
@@ -630,99 +694,128 @@ export function PersonFormSheet({
630
694
  onOpenChange(false);
631
695
  };
632
696
 
633
- const getContactTypeCode = (contactTypeId?: number | null) =>
634
- String(
635
- contactTypes.find(
636
- (contactType) => contactType.contact_type_id === contactTypeId
637
- )?.code || ''
638
- ).toUpperCase();
639
-
640
- const getDocumentTypeCode = (documentTypeId?: number | null) =>
641
- String(
642
- documentTypes.find(
643
- (documentType) => documentType.document_type_id === documentTypeId
644
- )?.code || ''
645
- ).toUpperCase();
646
-
647
- const maskContactValueByType = (
648
- value: string,
649
- contactTypeId?: number | null,
650
- fallbackCode?: string
651
- ) => {
652
- const code = (fallbackCode || getContactTypeCode(contactTypeId)).toUpperCase();
653
- if (['PHONE', 'MOBILE', 'WHATSAPP'].includes(code)) {
654
- return applyPhoneMask(value);
655
- }
656
-
657
- return value;
658
- };
659
-
660
- const maskDocumentValueByType = (
661
- value: string,
662
- documentTypeId?: number | null,
663
- fallbackCode?: string
664
- ) => {
665
- const code = (fallbackCode || getDocumentTypeCode(documentTypeId)).toUpperCase();
666
-
667
- if (code === 'CPF') {
668
- return applyCpfMask(value);
669
- }
670
-
671
- if (code === 'CNPJ') {
672
- return applyCnpjMask(value);
673
- }
674
-
675
- return value;
676
- };
677
-
678
- const resolveContactTypeIdByCodes = (codes: string[]) => {
679
- const normalizedCodes = codes.map((code) => code.toUpperCase());
680
- return (
681
- contactTypes.find((contactType) =>
682
- normalizedCodes.includes(String(contactType.code || '').toUpperCase())
683
- )?.contact_type_id || null
684
- );
685
- };
686
-
687
- const resolveDocumentTypeIdByCodes = (codes: string[]) => {
688
- const normalizedCodes = codes.map((code) => code.toUpperCase());
689
- return (
690
- documentTypes.find((documentType) =>
691
- normalizedCodes.includes(String(documentType.code || '').toUpperCase())
692
- )?.document_type_id || null
693
- );
694
- };
695
-
696
- const addContact = (preset: 'email' | 'phone' | 'blank' = 'blank') => {
697
- const presetTypeId =
698
- preset === 'email'
699
- ? resolveContactTypeIdByCodes(['EMAIL'])
700
- : preset === 'phone'
701
- ? resolveContactTypeIdByCodes(['PHONE', 'MOBILE', 'WHATSAPP'])
702
- : null;
703
-
704
- const selectedTypeId = presetTypeId;
705
- const hasPrimaryForType = contacts.some(
706
- (contact) =>
707
- selectedTypeId !== null &&
708
- contact.contact_type_id === selectedTypeId &&
709
- contact.is_primary
710
- );
711
-
712
- const clientId = createClientId('contact');
713
-
714
- setContactsOpen(true);
715
- setContacts((previous) => [
716
- ...previous,
717
- {
718
- clientId,
719
- value: '',
720
- is_primary: selectedTypeId !== null ? !hasPrimaryForType : false,
721
- contact_type_id: selectedTypeId,
722
- },
723
- ]);
724
- focusWhenAvailable(() => contactValueRefs.current[clientId] || null);
725
- };
697
+ useEffect(() => {
698
+ if (!open) {
699
+ return;
700
+ }
701
+
702
+ const onKeyDown = (event: KeyboardEvent) => {
703
+ if ((event.ctrlKey || event.metaKey) && event.key.toLowerCase() === 's') {
704
+ event.preventDefault();
705
+ const formElement = document.getElementById(
706
+ 'person-form'
707
+ ) as HTMLFormElement | null;
708
+ formElement?.requestSubmit();
709
+ return;
710
+ }
711
+
712
+ if (event.key === 'Escape' && !isSubmitting && !isUploadingAvatar) {
713
+ event.preventDefault();
714
+ handleSheetOpenChange(false);
715
+ }
716
+ };
717
+
718
+ window.addEventListener('keydown', onKeyDown);
719
+ return () => window.removeEventListener('keydown', onKeyDown);
720
+ }, [open, isSubmitting, isUploadingAvatar, handleSheetOpenChange]);
721
+
722
+ const getContactTypeCode = (contactTypeId?: number | null) =>
723
+ String(
724
+ contactTypes.find(
725
+ (contactType) => contactType.contact_type_id === contactTypeId
726
+ )?.code || ''
727
+ ).toUpperCase();
728
+
729
+ const getDocumentTypeCode = (documentTypeId?: number | null) =>
730
+ String(
731
+ documentTypes.find(
732
+ (documentType) => documentType.document_type_id === documentTypeId
733
+ )?.code || ''
734
+ ).toUpperCase();
735
+
736
+ const maskContactValueByType = (
737
+ value: string,
738
+ contactTypeId?: number | null,
739
+ fallbackCode?: string
740
+ ) => {
741
+ const code = (
742
+ fallbackCode || getContactTypeCode(contactTypeId)
743
+ ).toUpperCase();
744
+ if (['PHONE', 'MOBILE', 'WHATSAPP'].includes(code)) {
745
+ return applyPhoneMask(value);
746
+ }
747
+
748
+ return value;
749
+ };
750
+
751
+ const maskDocumentValueByType = (
752
+ value: string,
753
+ documentTypeId?: number | null,
754
+ fallbackCode?: string
755
+ ) => {
756
+ const code = (
757
+ fallbackCode || getDocumentTypeCode(documentTypeId)
758
+ ).toUpperCase();
759
+
760
+ if (code === 'CPF') {
761
+ return applyCpfMask(value);
762
+ }
763
+
764
+ if (code === 'CNPJ') {
765
+ return applyCnpjMask(value);
766
+ }
767
+
768
+ return value;
769
+ };
770
+
771
+ const resolveContactTypeIdByCodes = (codes: string[]) => {
772
+ const normalizedCodes = codes.map((code) => code.toUpperCase());
773
+ return (
774
+ contactTypes.find((contactType) =>
775
+ normalizedCodes.includes(String(contactType.code || '').toUpperCase())
776
+ )?.contact_type_id || null
777
+ );
778
+ };
779
+
780
+ const resolveDocumentTypeIdByCodes = (codes: string[]) => {
781
+ const normalizedCodes = codes.map((code) => code.toUpperCase());
782
+ return (
783
+ documentTypes.find((documentType) =>
784
+ normalizedCodes.includes(String(documentType.code || '').toUpperCase())
785
+ )?.document_type_id || null
786
+ );
787
+ };
788
+
789
+ const addContact = (preset: 'email' | 'phone' | 'blank' = 'blank') => {
790
+ const presetTypeId =
791
+ preset === 'email'
792
+ ? resolveContactTypeIdByCodes(['EMAIL'])
793
+ : preset === 'phone'
794
+ ? resolveContactTypeIdByCodes(['PHONE', 'MOBILE', 'WHATSAPP'])
795
+ : null;
796
+
797
+ const selectedTypeId = presetTypeId;
798
+ const hasPrimaryForType = contacts.some(
799
+ (contact) =>
800
+ selectedTypeId !== null &&
801
+ contact.contact_type_id === selectedTypeId &&
802
+ contact.is_primary
803
+ );
804
+
805
+ const clientId = createClientId('contact');
806
+
807
+ setContactsOpen(true);
808
+ setContacts((previous) => [
809
+ ...previous,
810
+ {
811
+ clientId,
812
+ value: '',
813
+ is_primary: selectedTypeId !== null ? !hasPrimaryForType : false,
814
+ contact_type_id: selectedTypeId,
815
+ },
816
+ ]);
817
+ focusWhenAvailable(() => contactValueRefs.current[clientId] || null);
818
+ };
726
819
 
727
820
  const updateContact = (
728
821
  clientId: string,
@@ -747,9 +840,9 @@ export function PersonFormSheet({
747
840
  });
748
841
  };
749
842
 
750
- const removeContact = (clientId: string) => {
751
- delete contactValueRefs.current[clientId];
752
- setContacts((previous) => {
843
+ const removeContact = (clientId: string) => {
844
+ delete contactValueRefs.current[clientId];
845
+ setContacts((previous) => {
753
846
  const removed = previous.find((contact) => contact.clientId === clientId);
754
847
  const next = previous.filter((contact) => contact.clientId !== clientId);
755
848
 
@@ -793,31 +886,31 @@ export function PersonFormSheet({
793
886
  });
794
887
  };
795
888
 
796
- const addAddress = () => {
889
+ const addAddress = () => {
797
890
  const defaultType = ADDRESS_TYPE_OPTIONS[0]?.value || 'residential';
798
891
  const hasPrimaryForType = addresses.some(
799
892
  (address) => address.address_type === defaultType && address.is_primary
800
893
  );
801
894
 
802
- const clientId = createClientId('address');
803
-
804
- setAddressesOpen(true);
805
- setAddresses((previous) => [
806
- ...previous,
807
- {
808
- clientId,
809
- line1: '',
810
- line2: '',
811
- city: '',
895
+ const clientId = createClientId('address');
896
+
897
+ setAddressesOpen(true);
898
+ setAddresses((previous) => [
899
+ ...previous,
900
+ {
901
+ clientId,
902
+ line1: '',
903
+ line2: '',
904
+ city: '',
812
905
  state: '',
813
906
  is_primary: !hasPrimaryForType,
814
907
  address_type: defaultType,
815
908
  postal_code: '',
816
- country_code: 'BRA',
817
- },
818
- ]);
819
- focusWhenAvailable(() => addressLine1Refs.current[clientId] || null);
820
- };
909
+ country_code: 'BRA',
910
+ },
911
+ ]);
912
+ focusWhenAvailable(() => addressLine1Refs.current[clientId] || null);
913
+ };
821
914
 
822
915
  const updateAddress = (
823
916
  clientId: string,
@@ -842,9 +935,9 @@ export function PersonFormSheet({
842
935
  });
843
936
  };
844
937
 
845
- const removeAddress = (clientId: string) => {
846
- delete addressLine1Refs.current[clientId];
847
- setAddresses((previous) => {
938
+ const removeAddress = (clientId: string) => {
939
+ delete addressLine1Refs.current[clientId];
940
+ setAddresses((previous) => {
848
941
  const removed = previous.find((address) => address.clientId === clientId);
849
942
  const next = previous.filter((address) => address.clientId !== clientId);
850
943
 
@@ -888,29 +981,29 @@ export function PersonFormSheet({
888
981
  });
889
982
  };
890
983
 
891
- const addDocument = (preset: 'cpf' | 'cnpj' | 'rg' | 'blank' = 'blank') => {
892
- const presetTypeId =
893
- preset === 'cpf'
894
- ? resolveDocumentTypeIdByCodes(['CPF'])
895
- : preset === 'cnpj'
896
- ? resolveDocumentTypeIdByCodes(['CNPJ'])
897
- : preset === 'rg'
898
- ? resolveDocumentTypeIdByCodes(['RG'])
899
- : null;
900
-
901
- const clientId = createClientId('document');
902
-
903
- setDocumentsOpen(true);
904
- setDocuments((previous) => [
905
- ...previous,
906
- {
907
- clientId,
908
- value: '',
909
- document_type_id: presetTypeId,
910
- },
911
- ]);
912
- focusWhenAvailable(() => documentValueRefs.current[clientId] || null);
913
- };
984
+ const addDocument = (preset: 'cpf' | 'cnpj' | 'rg' | 'blank' = 'blank') => {
985
+ const presetTypeId =
986
+ preset === 'cpf'
987
+ ? resolveDocumentTypeIdByCodes(['CPF'])
988
+ : preset === 'cnpj'
989
+ ? resolveDocumentTypeIdByCodes(['CNPJ'])
990
+ : preset === 'rg'
991
+ ? resolveDocumentTypeIdByCodes(['RG'])
992
+ : null;
993
+
994
+ const clientId = createClientId('document');
995
+
996
+ setDocumentsOpen(true);
997
+ setDocuments((previous) => [
998
+ ...previous,
999
+ {
1000
+ clientId,
1001
+ value: '',
1002
+ document_type_id: presetTypeId,
1003
+ },
1004
+ ]);
1005
+ focusWhenAvailable(() => documentValueRefs.current[clientId] || null);
1006
+ };
914
1007
 
915
1008
  const updateDocument = (
916
1009
  clientId: string,
@@ -923,12 +1016,12 @@ export function PersonFormSheet({
923
1016
  );
924
1017
  };
925
1018
 
926
- const removeDocument = (clientId: string) => {
927
- delete documentValueRefs.current[clientId];
928
- setDocuments((previous) =>
929
- previous.filter((document) => document.clientId !== clientId)
930
- );
931
- };
1019
+ const removeDocument = (clientId: string) => {
1020
+ delete documentValueRefs.current[clientId];
1021
+ setDocuments((previous) =>
1022
+ previous.filter((document) => document.clientId !== clientId)
1023
+ );
1024
+ };
932
1025
 
933
1026
  const handleCEP = async (
934
1027
  event: ChangeEvent<HTMLInputElement>,
@@ -985,17 +1078,17 @@ export function PersonFormSheet({
985
1078
  ? values.foundation_date.toISOString()
986
1079
  : null,
987
1080
  legal_nature: values.legal_nature?.trim() || null,
988
- contacts: contacts
989
- .filter(
990
- (contact) =>
991
- contact.value.trim().length > 0 && !!contact.contact_type_id
992
- )
993
- .map((contact) => ({
994
- id: contact.id,
995
- value: contact.value.trim(),
996
- is_primary: contact.is_primary,
997
- contact_type_id: contact.contact_type_id as number,
998
- })),
1081
+ contacts: contacts
1082
+ .filter(
1083
+ (contact) =>
1084
+ contact.value.trim().length > 0 && !!contact.contact_type_id
1085
+ )
1086
+ .map((contact) => ({
1087
+ id: contact.id,
1088
+ value: contact.value.trim(),
1089
+ is_primary: contact.is_primary,
1090
+ contact_type_id: contact.contact_type_id as number,
1091
+ })),
999
1092
  addresses: addresses
1000
1093
  .filter(
1001
1094
  (address) =>
@@ -1015,18 +1108,64 @@ export function PersonFormSheet({
1015
1108
  is_primary: address.is_primary,
1016
1109
  address_type: address.address_type,
1017
1110
  })),
1018
- documents: documents
1019
- .filter(
1020
- (document) =>
1021
- document.value.trim().length > 0 && !!document.document_type_id
1022
- )
1023
- .map((document) => ({
1024
- id: document.id,
1025
- value: document.value.trim(),
1026
- document_type_id: document.document_type_id as number,
1027
- })),
1111
+ documents: documents
1112
+ .filter(
1113
+ (document) =>
1114
+ document.value.trim().length > 0 && !!document.document_type_id
1115
+ )
1116
+ .map((document) => ({
1117
+ id: document.id,
1118
+ value: document.value.trim(),
1119
+ document_type_id: document.document_type_id as number,
1120
+ })),
1028
1121
  };
1029
1122
 
1123
+ const primaryEmail = payload.contacts.find((contact) =>
1124
+ ['EMAIL'].includes(getContactTypeCode(contact.contact_type_id))
1125
+ );
1126
+ const primaryPhone = payload.contacts.find((contact) =>
1127
+ ['PHONE', 'MOBILE', 'WHATSAPP'].includes(
1128
+ getContactTypeCode(contact.contact_type_id)
1129
+ )
1130
+ );
1131
+ const firstDocument = payload.documents[0];
1132
+
1133
+ if (primaryEmail || primaryPhone || firstDocument) {
1134
+ const duplicateParams = new URLSearchParams();
1135
+ if (person?.id) duplicateParams.set('person_id', String(person.id));
1136
+ if (primaryEmail?.value)
1137
+ duplicateParams.set('email', primaryEmail.value);
1138
+ if (primaryPhone?.value)
1139
+ duplicateParams.set('phone', primaryPhone.value);
1140
+ if (firstDocument?.value && firstDocument?.document_type_id) {
1141
+ duplicateParams.set('document_value', firstDocument.value);
1142
+ duplicateParams.set(
1143
+ 'document_type_id',
1144
+ String(firstDocument.document_type_id)
1145
+ );
1146
+ }
1147
+
1148
+ if (duplicateParams.size > 0) {
1149
+ const duplicateResponse = await request<{
1150
+ hasDuplicates: boolean;
1151
+ matches: Array<{ id: number; name: string; reasons: string[] }>;
1152
+ }>({
1153
+ url: `/person/duplicates?${duplicateParams.toString()}`,
1154
+ method: 'GET',
1155
+ });
1156
+
1157
+ if (duplicateResponse.data.hasDuplicates) {
1158
+ toast.warning(
1159
+ t('duplicateWarning', {
1160
+ names: duplicateResponse.data.matches
1161
+ .map((item) => item.name)
1162
+ .join(', '),
1163
+ })
1164
+ );
1165
+ }
1166
+ }
1167
+ }
1168
+
1030
1169
  if (person) {
1031
1170
  await request({
1032
1171
  url: `/person/${person.id}`,
@@ -1084,7 +1223,7 @@ export function PersonFormSheet({
1084
1223
 
1085
1224
  return (
1086
1225
  <Sheet open={open} onOpenChange={handleSheetOpenChange}>
1087
- <SheetContent className="flex h-full w-full flex-col overflow-hidden p-0 sm:max-w-xl">
1226
+ <SheetContent className="flex h-full w-full max-w-full flex-col overflow-hidden p-0 lg:max-w-4xl xl:max-w-5xl">
1088
1227
  <SheetHeader className="shrink-0 border-b p-4">
1089
1228
  <div className="flex items-center gap-3">
1090
1229
  <div
@@ -1120,7 +1259,7 @@ export function PersonFormSheet({
1120
1259
  <form
1121
1260
  id="person-form"
1122
1261
  onSubmit={handleSubmit(handleFormSubmit)}
1123
- className="space-y-4 px-4"
1262
+ className="space-y-4 px-4 [&_button[role='combobox']]:text-xs [&_button[role='combobox']_span]:truncate [&_input]:text-xs"
1124
1263
  >
1125
1264
  <div className="space-y-3">
1126
1265
  <h3 className="text-sm font-semibold tracking-wider text-muted-foreground uppercase">
@@ -1220,7 +1359,7 @@ export function PersonFormSheet({
1220
1359
  className={cn(
1221
1360
  'grid gap-3',
1222
1361
  watchType === 'individual'
1223
- ? 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-[minmax(0,1fr)_minmax(0,0.85fr)_minmax(0,1.25fr)_minmax(0,1.25fr)]'
1362
+ ? 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4'
1224
1363
  : 'grid-cols-1 sm:grid-cols-2'
1225
1364
  )}
1226
1365
  >
@@ -1314,6 +1453,122 @@ export function PersonFormSheet({
1314
1453
  ) : null}
1315
1454
  </div>
1316
1455
 
1456
+ <div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
1457
+ <div className="space-y-1.5">
1458
+ <Label className="text-xs font-medium">{t('owner')}</Label>
1459
+ <Select
1460
+ value={
1461
+ watch('owner_user_id')
1462
+ ? String(watch('owner_user_id'))
1463
+ : 'none'
1464
+ }
1465
+ onValueChange={(value) =>
1466
+ setValue(
1467
+ 'owner_user_id',
1468
+ value === 'none' ? null : Number(value)
1469
+ )
1470
+ }
1471
+ >
1472
+ <SelectTrigger className="h-9 w-full min-w-0">
1473
+ <SelectValue placeholder={t('unassigned')} />
1474
+ </SelectTrigger>
1475
+ <SelectContent>
1476
+ <SelectItem value="none">{t('unassigned')}</SelectItem>
1477
+ {ownerOptions.map((owner) => (
1478
+ <SelectItem key={owner.id} value={String(owner.id)}>
1479
+ {owner.name}
1480
+ </SelectItem>
1481
+ ))}
1482
+ </SelectContent>
1483
+ </Select>
1484
+ </div>
1485
+
1486
+ <div className="space-y-1.5">
1487
+ <Label className="text-xs font-medium">{t('source')}</Label>
1488
+ <Select
1489
+ value={watch('source') || 'none'}
1490
+ onValueChange={(value) =>
1491
+ setValue(
1492
+ 'source',
1493
+ value === 'none' ? null : (value as PersonSource)
1494
+ )
1495
+ }
1496
+ >
1497
+ <SelectTrigger className="h-9 w-full min-w-0">
1498
+ <SelectValue placeholder={t('source')} />
1499
+ </SelectTrigger>
1500
+ <SelectContent>
1501
+ <SelectItem value="none">{t('all')}</SelectItem>
1502
+ <SelectItem value="referral">
1503
+ {t('sourceReferral')}
1504
+ </SelectItem>
1505
+ <SelectItem value="website">
1506
+ {t('sourceWebsite')}
1507
+ </SelectItem>
1508
+ <SelectItem value="social">
1509
+ {t('sourceSocial')}
1510
+ </SelectItem>
1511
+ <SelectItem value="inbound">
1512
+ {t('sourceInbound')}
1513
+ </SelectItem>
1514
+ <SelectItem value="outbound">
1515
+ {t('sourceOutbound')}
1516
+ </SelectItem>
1517
+ <SelectItem value="other">{t('sourceOther')}</SelectItem>
1518
+ </SelectContent>
1519
+ </Select>
1520
+ </div>
1521
+
1522
+ <div className="space-y-1.5">
1523
+ <Label className="text-xs font-medium">
1524
+ {t('lifecycleStage')}
1525
+ </Label>
1526
+ <Select
1527
+ value={watch('lifecycle_stage') || 'new'}
1528
+ onValueChange={(value) =>
1529
+ setValue('lifecycle_stage', value as PersonLifecycleStage)
1530
+ }
1531
+ >
1532
+ <SelectTrigger className="h-9 w-full min-w-0">
1533
+ <SelectValue />
1534
+ </SelectTrigger>
1535
+ <SelectContent>
1536
+ <SelectItem value="new">{t('lifecycleNew')}</SelectItem>
1537
+ <SelectItem value="contacted">
1538
+ {t('lifecycleContacted')}
1539
+ </SelectItem>
1540
+ <SelectItem value="qualified">
1541
+ {t('lifecycleQualified')}
1542
+ </SelectItem>
1543
+ <SelectItem value="proposal">
1544
+ {t('lifecycleProposal')}
1545
+ </SelectItem>
1546
+ <SelectItem value="negotiation">
1547
+ {t('lifecycleNegotiation')}
1548
+ </SelectItem>
1549
+ <SelectItem value="customer">
1550
+ {t('lifecycleCustomer')}
1551
+ </SelectItem>
1552
+ <SelectItem value="lost">{t('lifecycleLost')}</SelectItem>
1553
+ </SelectContent>
1554
+ </Select>
1555
+ </div>
1556
+
1557
+ <div className="space-y-1.5">
1558
+ <Label className="text-xs font-medium">
1559
+ {t('nextActionAt')}
1560
+ </Label>
1561
+ <Input
1562
+ type="datetime-local"
1563
+ value={watch('next_action_at') || ''}
1564
+ onChange={(event) =>
1565
+ setValue('next_action_at', event.target.value)
1566
+ }
1567
+ className="h-9"
1568
+ />
1569
+ </div>
1570
+ </div>
1571
+
1317
1572
  {watchType === 'individual' ? (
1318
1573
  <>
1319
1574
  <div className="grid grid-cols-2 gap-3">
@@ -1412,44 +1667,44 @@ export function PersonFormSheet({
1412
1667
  ) : null}
1413
1668
  </div>
1414
1669
 
1415
- <div className="flex items-center gap-1">
1416
- <Button
1417
- type="button"
1418
- variant="ghost"
1419
- size="sm"
1420
- className="h-7 px-2 text-xs"
1421
- onClick={(event) => {
1422
- event.stopPropagation();
1423
- addContact('email');
1424
- }}
1425
- >
1426
- <Plus className="mr-1 h-3.5 w-3.5" />
1427
- {t('addEmail')}
1428
- </Button>
1429
- <Button
1430
- type="button"
1431
- variant="ghost"
1432
- size="sm"
1433
- className="h-7 px-2 text-xs"
1434
- onClick={(event) => {
1435
- event.stopPropagation();
1436
- addContact('phone');
1437
- }}
1438
- >
1439
- <Plus className="mr-1 h-3.5 w-3.5" />
1440
- {t('addPhone')}
1441
- </Button>
1442
- <Button
1443
- type="button"
1444
- variant="ghost"
1445
- size="sm"
1446
- className="h-7 px-2 text-xs"
1447
- onClick={(event) => {
1448
- event.stopPropagation();
1449
- addContact('blank');
1450
- }}
1451
- >
1452
- <Plus className="mr-1 h-3.5 w-3.5" />
1670
+ <div className="flex items-center gap-1">
1671
+ <Button
1672
+ type="button"
1673
+ variant="ghost"
1674
+ size="sm"
1675
+ className="h-7 px-2 text-xs"
1676
+ onClick={(event) => {
1677
+ event.stopPropagation();
1678
+ addContact('email');
1679
+ }}
1680
+ >
1681
+ <Plus className="mr-1 h-3.5 w-3.5" />
1682
+ {t('addEmail')}
1683
+ </Button>
1684
+ <Button
1685
+ type="button"
1686
+ variant="ghost"
1687
+ size="sm"
1688
+ className="h-7 px-2 text-xs"
1689
+ onClick={(event) => {
1690
+ event.stopPropagation();
1691
+ addContact('phone');
1692
+ }}
1693
+ >
1694
+ <Plus className="mr-1 h-3.5 w-3.5" />
1695
+ {t('addPhone')}
1696
+ </Button>
1697
+ <Button
1698
+ type="button"
1699
+ variant="ghost"
1700
+ size="sm"
1701
+ className="h-7 px-2 text-xs"
1702
+ onClick={(event) => {
1703
+ event.stopPropagation();
1704
+ addContact('blank');
1705
+ }}
1706
+ >
1707
+ <Plus className="mr-1 h-3.5 w-3.5" />
1453
1708
  {t('addContact')}
1454
1709
  </Button>
1455
1710
  {contactsOpen ? (
@@ -1476,26 +1731,26 @@ export function PersonFormSheet({
1476
1731
  )}
1477
1732
  >
1478
1733
  <div className="flex items-center gap-2">
1479
- <Select
1480
- value={
1481
- contact.contact_type_id
1482
- ? String(contact.contact_type_id)
1483
- : undefined
1484
- }
1485
- onValueChange={(value) => {
1486
- const nextTypeId = Number(value);
1487
- updateContact(contact.clientId, {
1488
- contact_type_id: nextTypeId,
1489
- value: maskContactValueByType(
1490
- contact.value || '',
1491
- nextTypeId
1492
- ),
1493
- });
1494
- }}
1495
- >
1496
- <SelectTrigger className="h-8 w-36 text-xs">
1497
- <SelectValue placeholder={t('selectContactType')} />
1498
- </SelectTrigger>
1734
+ <Select
1735
+ value={
1736
+ contact.contact_type_id
1737
+ ? String(contact.contact_type_id)
1738
+ : undefined
1739
+ }
1740
+ onValueChange={(value) => {
1741
+ const nextTypeId = Number(value);
1742
+ updateContact(contact.clientId, {
1743
+ contact_type_id: nextTypeId,
1744
+ value: maskContactValueByType(
1745
+ contact.value || '',
1746
+ nextTypeId
1747
+ ),
1748
+ });
1749
+ }}
1750
+ >
1751
+ <SelectTrigger className="h-8 w-36 text-xs">
1752
+ <SelectValue placeholder={t('selectContactType')} />
1753
+ </SelectTrigger>
1499
1754
  <SelectContent>
1500
1755
  {contactTypes.map((contactType) => (
1501
1756
  <SelectItem
@@ -1508,75 +1763,76 @@ export function PersonFormSheet({
1508
1763
  </SelectContent>
1509
1764
  </Select>
1510
1765
 
1511
- <Input
1512
- ref={(element) => {
1513
- contactValueRefs.current[contact.clientId] = element;
1514
- }}
1515
- placeholder={(() => {
1516
- const contactTypeCode = getContactTypeCode(
1517
- contact.contact_type_id
1518
- );
1519
- if (contactTypeCode === 'EMAIL') {
1520
- return 'email@exemplo.com';
1521
- }
1522
- if (contactTypeCode.length === 0) {
1523
- return t('contactValue');
1524
- }
1525
- return '(00) 00000-0000';
1526
- })()}
1527
- value={contact.value}
1528
- onChange={(event) =>
1529
- updateContact(contact.clientId, {
1530
- value: maskContactValueByType(
1531
- event.target.value,
1532
- contact.contact_type_id
1533
- ),
1534
- })
1535
- }
1536
- className="h-8 flex-1 text-xs"
1766
+ <Input
1767
+ ref={(element) => {
1768
+ contactValueRefs.current[contact.clientId] =
1769
+ element;
1770
+ }}
1771
+ placeholder={(() => {
1772
+ const contactTypeCode = getContactTypeCode(
1773
+ contact.contact_type_id
1774
+ );
1775
+ if (contactTypeCode === 'EMAIL') {
1776
+ return 'email@exemplo.com';
1777
+ }
1778
+ if (contactTypeCode.length === 0) {
1779
+ return t('contactValue');
1780
+ }
1781
+ return '(00) 00000-0000';
1782
+ })()}
1783
+ value={contact.value}
1784
+ onChange={(event) =>
1785
+ updateContact(contact.clientId, {
1786
+ value: maskContactValueByType(
1787
+ event.target.value,
1788
+ contact.contact_type_id
1789
+ ),
1790
+ })
1791
+ }
1792
+ className="h-8 flex-1 text-xs"
1537
1793
  />
1538
1794
 
1539
- <Tooltip>
1540
- <TooltipTrigger asChild>
1541
- <Button
1542
- type="button"
1543
- variant="ghost"
1544
- size="icon"
1545
- className={cn(
1546
- 'h-8 w-8',
1547
- contact.is_primary && 'text-amber-500'
1548
- )}
1549
- onClick={() =>
1550
- setPrimaryContact(contact.clientId)
1551
- }
1552
- aria-label={t('main')}
1553
- >
1554
- <Star
1555
- className={cn(
1556
- 'h-4 w-4',
1557
- contact.is_primary && 'fill-current'
1558
- )}
1559
- />
1560
- </Button>
1561
- </TooltipTrigger>
1562
- <TooltipContent>{t('main')}</TooltipContent>
1563
- </Tooltip>
1564
-
1565
- <Tooltip>
1566
- <TooltipTrigger asChild>
1567
- <Button
1568
- type="button"
1569
- variant="ghost"
1570
- size="icon"
1571
- className="h-8 w-8 text-red-500 hover:text-red-600"
1572
- onClick={() => removeContact(contact.clientId)}
1573
- aria-label={t('remove')}
1574
- >
1575
- <Trash2 className="h-4 w-4" />
1576
- </Button>
1577
- </TooltipTrigger>
1578
- <TooltipContent>{t('remove')}</TooltipContent>
1579
- </Tooltip>
1795
+ <Tooltip>
1796
+ <TooltipTrigger asChild>
1797
+ <Button
1798
+ type="button"
1799
+ variant="ghost"
1800
+ size="icon"
1801
+ className={cn(
1802
+ 'h-8 w-8',
1803
+ contact.is_primary && 'text-amber-500'
1804
+ )}
1805
+ onClick={() =>
1806
+ setPrimaryContact(contact.clientId)
1807
+ }
1808
+ aria-label={t('main')}
1809
+ >
1810
+ <Star
1811
+ className={cn(
1812
+ 'h-4 w-4',
1813
+ contact.is_primary && 'fill-current'
1814
+ )}
1815
+ />
1816
+ </Button>
1817
+ </TooltipTrigger>
1818
+ <TooltipContent>{t('main')}</TooltipContent>
1819
+ </Tooltip>
1820
+
1821
+ <Tooltip>
1822
+ <TooltipTrigger asChild>
1823
+ <Button
1824
+ type="button"
1825
+ variant="ghost"
1826
+ size="icon"
1827
+ className="h-8 w-8 text-red-500 hover:text-red-600"
1828
+ onClick={() => removeContact(contact.clientId)}
1829
+ aria-label={t('remove')}
1830
+ >
1831
+ <Trash2 className="h-4 w-4" />
1832
+ </Button>
1833
+ </TooltipTrigger>
1834
+ <TooltipContent>{t('remove')}</TooltipContent>
1835
+ </Tooltip>
1580
1836
  </div>
1581
1837
  </div>
1582
1838
  ))
@@ -1677,57 +1933,58 @@ export function PersonFormSheet({
1677
1933
  className="h-8 w-28 text-xs"
1678
1934
  />
1679
1935
 
1680
- <Tooltip>
1681
- <TooltipTrigger asChild>
1682
- <Button
1683
- type="button"
1684
- variant="ghost"
1685
- size="icon"
1686
- className={cn(
1687
- 'h-8 w-8',
1688
- address.is_primary && 'text-amber-500'
1689
- )}
1690
- onClick={() =>
1691
- setPrimaryAddress(address.clientId)
1692
- }
1693
- aria-label={t('main')}
1694
- >
1695
- <Star
1696
- className={cn(
1697
- 'h-4 w-4',
1698
- address.is_primary && 'fill-current'
1699
- )}
1700
- />
1701
- </Button>
1702
- </TooltipTrigger>
1703
- <TooltipContent>{t('main')}</TooltipContent>
1704
- </Tooltip>
1705
-
1706
- <Tooltip>
1707
- <TooltipTrigger asChild>
1708
- <Button
1709
- type="button"
1710
- variant="ghost"
1711
- size="icon"
1712
- className="h-8 w-8 text-red-500 hover:text-red-600"
1713
- onClick={() => removeAddress(address.clientId)}
1714
- aria-label={t('remove')}
1715
- >
1716
- <Trash2 className="h-4 w-4" />
1717
- </Button>
1718
- </TooltipTrigger>
1719
- <TooltipContent>{t('remove')}</TooltipContent>
1720
- </Tooltip>
1936
+ <Tooltip>
1937
+ <TooltipTrigger asChild>
1938
+ <Button
1939
+ type="button"
1940
+ variant="ghost"
1941
+ size="icon"
1942
+ className={cn(
1943
+ 'h-8 w-8',
1944
+ address.is_primary && 'text-amber-500'
1945
+ )}
1946
+ onClick={() =>
1947
+ setPrimaryAddress(address.clientId)
1948
+ }
1949
+ aria-label={t('main')}
1950
+ >
1951
+ <Star
1952
+ className={cn(
1953
+ 'h-4 w-4',
1954
+ address.is_primary && 'fill-current'
1955
+ )}
1956
+ />
1957
+ </Button>
1958
+ </TooltipTrigger>
1959
+ <TooltipContent>{t('main')}</TooltipContent>
1960
+ </Tooltip>
1961
+
1962
+ <Tooltip>
1963
+ <TooltipTrigger asChild>
1964
+ <Button
1965
+ type="button"
1966
+ variant="ghost"
1967
+ size="icon"
1968
+ className="h-8 w-8 text-red-500 hover:text-red-600"
1969
+ onClick={() => removeAddress(address.clientId)}
1970
+ aria-label={t('remove')}
1971
+ >
1972
+ <Trash2 className="h-4 w-4" />
1973
+ </Button>
1974
+ </TooltipTrigger>
1975
+ <TooltipContent>{t('remove')}</TooltipContent>
1976
+ </Tooltip>
1721
1977
  </div>
1722
1978
 
1723
1979
  <div className="relative">
1724
- <Input
1725
- ref={(element) => {
1726
- addressLine1Refs.current[address.clientId] = element;
1727
- }}
1728
- placeholder={t('addressPlaceholder')}
1729
- value={address.line1}
1730
- disabled={Boolean(loadingCEP[address.clientId])}
1980
+ <Input
1981
+ ref={(element) => {
1982
+ addressLine1Refs.current[address.clientId] =
1983
+ element;
1984
+ }}
1985
+ placeholder={t('addressPlaceholder')}
1986
+ value={address.line1}
1987
+ disabled={Boolean(loadingCEP[address.clientId])}
1731
1988
  onChange={(event) =>
1732
1989
  updateAddress(address.clientId, {
1733
1990
  line1: event.target.value,
@@ -1807,8 +2064,8 @@ export function PersonFormSheet({
1807
2064
 
1808
2065
  <Collapsible open={documentsOpen} onOpenChange={setDocumentsOpen}>
1809
2066
  <CollapsibleTrigger asChild>
1810
- <div className="group flex cursor-pointer items-center justify-between">
1811
- <div className="flex items-center gap-2">
2067
+ <div className="group flex cursor-pointer flex-wrap items-center gap-2">
2068
+ <div className="flex min-w-0 items-center gap-2">
1812
2069
  <FileText className="h-4 w-4 text-amber-500" />
1813
2070
  <h3 className="text-sm font-semibold">
1814
2071
  {t('tabDocuments')}
@@ -1823,59 +2080,61 @@ export function PersonFormSheet({
1823
2080
  ) : null}
1824
2081
  </div>
1825
2082
 
1826
- <div className="flex items-center gap-1">
1827
- <Button
1828
- type="button"
1829
- variant="ghost"
1830
- size="sm"
1831
- className="h-7 px-2 text-xs"
1832
- onClick={(event) => {
1833
- event.stopPropagation();
1834
- addDocument('cpf');
1835
- }}
1836
- >
1837
- <Plus className="mr-1 h-3.5 w-3.5" />
1838
- CPF
1839
- </Button>
1840
- <Button
1841
- type="button"
1842
- variant="ghost"
1843
- size="sm"
1844
- className="h-7 px-2 text-xs"
1845
- onClick={(event) => {
1846
- event.stopPropagation();
1847
- addDocument('cnpj');
1848
- }}
1849
- >
1850
- <Plus className="mr-1 h-3.5 w-3.5" />
1851
- CNPJ
1852
- </Button>
1853
- <Button
1854
- type="button"
1855
- variant="ghost"
1856
- size="sm"
1857
- className="h-7 px-2 text-xs"
1858
- onClick={(event) => {
1859
- event.stopPropagation();
1860
- addDocument('rg');
1861
- }}
1862
- >
1863
- <Plus className="mr-1 h-3.5 w-3.5" />
1864
- RG
1865
- </Button>
1866
- <Button
1867
- type="button"
1868
- variant="ghost"
1869
- size="sm"
1870
- className="h-7 px-2 text-xs"
1871
- onClick={(event) => {
1872
- event.stopPropagation();
1873
- addDocument('blank');
1874
- }}
1875
- >
1876
- <Plus className="mr-1 h-3.5 w-3.5" />
1877
- {t('addDocument')}
1878
- </Button>
2083
+ <div className="ml-auto flex max-w-full flex-wrap items-center justify-end gap-1 sm:flex-nowrap">
2084
+ <Button
2085
+ type="button"
2086
+ variant="ghost"
2087
+ size="sm"
2088
+ className="h-7 shrink-0 px-2 text-xs"
2089
+ onClick={(event) => {
2090
+ event.stopPropagation();
2091
+ addDocument('cpf');
2092
+ }}
2093
+ >
2094
+ <Plus className="mr-1 h-3.5 w-3.5" />
2095
+ CPF
2096
+ </Button>
2097
+ <Button
2098
+ type="button"
2099
+ variant="ghost"
2100
+ size="sm"
2101
+ className="h-7 shrink-0 px-2 text-xs"
2102
+ onClick={(event) => {
2103
+ event.stopPropagation();
2104
+ addDocument('cnpj');
2105
+ }}
2106
+ >
2107
+ <Plus className="mr-1 h-3.5 w-3.5" />
2108
+ CNPJ
2109
+ </Button>
2110
+ <Button
2111
+ type="button"
2112
+ variant="ghost"
2113
+ size="sm"
2114
+ className="h-7 shrink-0 px-2 text-xs"
2115
+ onClick={(event) => {
2116
+ event.stopPropagation();
2117
+ addDocument('rg');
2118
+ }}
2119
+ >
2120
+ <Plus className="mr-1 h-3.5 w-3.5" />
2121
+ RG
2122
+ </Button>
2123
+ <Button
2124
+ type="button"
2125
+ variant="ghost"
2126
+ size="sm"
2127
+ className="h-7 shrink-0 px-2 text-xs"
2128
+ onClick={(event) => {
2129
+ event.stopPropagation();
2130
+ addDocument('blank');
2131
+ }}
2132
+ >
2133
+ <Plus className="mr-1 h-3.5 w-3.5" />
2134
+ <span className="max-w-20 truncate sm:max-w-none">
2135
+ {t('addDocument')}
2136
+ </span>
2137
+ </Button>
1879
2138
  {documentsOpen ? (
1880
2139
  <ChevronUp className="h-4 w-4" />
1881
2140
  ) : (
@@ -1897,23 +2156,23 @@ export function PersonFormSheet({
1897
2156
  className="rounded-lg border p-2"
1898
2157
  >
1899
2158
  <div className="flex items-center gap-2">
1900
- <Select
1901
- value={
1902
- document.document_type_id
1903
- ? String(document.document_type_id)
1904
- : undefined
1905
- }
1906
- onValueChange={(value) => {
1907
- const nextTypeId = Number(value);
1908
- updateDocument(document.clientId, {
1909
- document_type_id: nextTypeId,
1910
- value: maskDocumentValueByType(
1911
- document.value || '',
1912
- nextTypeId
1913
- ),
1914
- });
1915
- }}
1916
- >
2159
+ <Select
2160
+ value={
2161
+ document.document_type_id
2162
+ ? String(document.document_type_id)
2163
+ : undefined
2164
+ }
2165
+ onValueChange={(value) => {
2166
+ const nextTypeId = Number(value);
2167
+ updateDocument(document.clientId, {
2168
+ document_type_id: nextTypeId,
2169
+ value: maskDocumentValueByType(
2170
+ document.value || '',
2171
+ nextTypeId
2172
+ ),
2173
+ });
2174
+ }}
2175
+ >
1917
2176
  <SelectTrigger className="h-8 w-36 text-xs">
1918
2177
  <SelectValue
1919
2178
  placeholder={t('selectDocumentType')}
@@ -1931,38 +2190,39 @@ export function PersonFormSheet({
1931
2190
  </SelectContent>
1932
2191
  </Select>
1933
2192
 
1934
- <Input
1935
- ref={(element) => {
1936
- documentValueRefs.current[document.clientId] = element;
1937
- }}
1938
- placeholder={t('documentValuePlaceholder')}
1939
- value={document.value}
1940
- onChange={(event) =>
1941
- updateDocument(document.clientId, {
1942
- value: maskDocumentValueByType(
1943
- event.target.value,
1944
- document.document_type_id
1945
- ),
1946
- })
1947
- }
1948
- className="h-8 flex-1 text-xs"
2193
+ <Input
2194
+ ref={(element) => {
2195
+ documentValueRefs.current[document.clientId] =
2196
+ element;
2197
+ }}
2198
+ placeholder={t('documentValuePlaceholder')}
2199
+ value={document.value}
2200
+ onChange={(event) =>
2201
+ updateDocument(document.clientId, {
2202
+ value: maskDocumentValueByType(
2203
+ event.target.value,
2204
+ document.document_type_id
2205
+ ),
2206
+ })
2207
+ }
2208
+ className="h-8 flex-1 text-xs"
1949
2209
  />
1950
2210
 
1951
- <Tooltip>
1952
- <TooltipTrigger asChild>
1953
- <Button
1954
- type="button"
1955
- variant="ghost"
1956
- size="icon"
1957
- className="h-8 w-8 text-red-500 hover:text-red-600"
1958
- onClick={() => removeDocument(document.clientId)}
1959
- aria-label={t('remove')}
1960
- >
1961
- <Trash2 className="h-4 w-4" />
1962
- </Button>
1963
- </TooltipTrigger>
1964
- <TooltipContent>{t('remove')}</TooltipContent>
1965
- </Tooltip>
2211
+ <Tooltip>
2212
+ <TooltipTrigger asChild>
2213
+ <Button
2214
+ type="button"
2215
+ variant="ghost"
2216
+ size="icon"
2217
+ className="h-8 w-8 text-red-500 hover:text-red-600"
2218
+ onClick={() => removeDocument(document.clientId)}
2219
+ aria-label={t('remove')}
2220
+ >
2221
+ <Trash2 className="h-4 w-4" />
2222
+ </Button>
2223
+ </TooltipTrigger>
2224
+ <TooltipContent>{t('remove')}</TooltipContent>
2225
+ </Tooltip>
1966
2226
  </div>
1967
2227
  </div>
1968
2228
  ))
@@ -1973,23 +2233,43 @@ export function PersonFormSheet({
1973
2233
  </div>
1974
2234
 
1975
2235
  <div className="shrink-0 space-y-2 border-t p-4">
1976
- <Button
1977
- type="submit"
1978
- form="person-form"
1979
- className="w-full"
1980
- disabled={isSubmitting || isUploadingAvatar}
1981
- >
1982
- {isSubmitting ? (
1983
- <Loader2 className="mr-2 h-4 w-4 animate-spin" />
1984
- ) : (
1985
- <Save className="mr-2 h-4 w-4" />
1986
- )}
1987
- {isSubmitting
1988
- ? t('saving')
1989
- : isEditing
1990
- ? t('saveChanges')
1991
- : t('createPerson')}
1992
- </Button>
2236
+ <div className="grid gap-2 sm:grid-cols-2">
2237
+ {!isEditing ? (
2238
+ <Button
2239
+ type="button"
2240
+ variant="outline"
2241
+ onClick={() => {
2242
+ const formElement = document.getElementById(
2243
+ 'person-form'
2244
+ ) as HTMLFormElement | null;
2245
+ formElement?.requestSubmit();
2246
+ }}
2247
+ disabled={isSubmitting || isUploadingAvatar}
2248
+ >
2249
+ {t('saveAndNew')}
2250
+ </Button>
2251
+ ) : null}
2252
+ <Button
2253
+ type="submit"
2254
+ form="person-form"
2255
+ className="w-full"
2256
+ disabled={isSubmitting || isUploadingAvatar}
2257
+ >
2258
+ {isSubmitting ? (
2259
+ <Loader2 className="mr-2 h-4 w-4 animate-spin" />
2260
+ ) : (
2261
+ <Save className="mr-2 h-4 w-4" />
2262
+ )}
2263
+ {isSubmitting
2264
+ ? t('saving')
2265
+ : isEditing
2266
+ ? t('saveChanges')
2267
+ : t('createPerson')}
2268
+ </Button>
2269
+ </div>
2270
+ <p className="text-xs text-muted-foreground">
2271
+ {t('formShortcutsHint')}
2272
+ </p>
1993
2273
  </div>
1994
2274
  </SheetContent>
1995
2275
  </Sheet>