@hed-hog/contact 0.0.304 → 0.0.305
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +225 -17
- package/dist/person/dto/account.dto.d.ts +5 -0
- package/dist/person/dto/account.dto.d.ts.map +1 -1
- package/dist/person/dto/account.dto.js +29 -0
- package/dist/person/dto/account.dto.js.map +1 -1
- package/dist/person/dto/import-preview.dto.d.ts +7 -0
- package/dist/person/dto/import-preview.dto.d.ts.map +1 -0
- package/dist/person/dto/import-preview.dto.js +7 -0
- package/dist/person/dto/import-preview.dto.js.map +1 -0
- package/dist/person/dto/import.dto.d.ts +15 -0
- package/dist/person/dto/import.dto.d.ts.map +1 -0
- package/dist/person/dto/import.dto.js +51 -0
- package/dist/person/dto/import.dto.js.map +1 -0
- package/dist/person/person.controller.d.ts +14 -0
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +53 -0
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +19 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +481 -67
- package/dist/person/person.service.js.map +1 -1
- package/dist/person-relation-type/person-relation-type.controller.d.ts +2 -2
- package/dist/person-relation-type/person-relation-type.service.d.ts +2 -2
- package/hedhog/data/route.yaml +6 -0
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +2242 -484
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +51 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +181 -16
- package/hedhog/frontend/app/contact-type/page.tsx.ejs +223 -29
- package/hedhog/frontend/app/document-type/page.tsx.ejs +248 -37
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +129 -19
- package/hedhog/frontend/app/person/_components/person-field-with-create.tsx.ejs +78 -212
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +760 -178
- package/hedhog/frontend/app/person/_components/person-import-sheet.tsx.ejs +1120 -0
- package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +171 -4
- package/hedhog/frontend/app/person/page.tsx.ejs +17 -0
- package/hedhog/frontend/app/pipeline/_components/lead-proposals-tab.tsx.ejs +160 -35
- package/hedhog/frontend/messages/en.json +104 -2
- package/hedhog/frontend/messages/pt.json +111 -9
- package/package.json +5 -5
- package/src/person/dto/account.dto.ts +31 -0
- package/src/person/dto/import-preview.dto.ts +6 -0
- package/src/person/dto/import.dto.ts +61 -0
- package/src/person/person.controller.ts +74 -12
- package/src/person/person.service.ts +615 -68
|
@@ -47,10 +47,12 @@ import {
|
|
|
47
47
|
TooltipTrigger,
|
|
48
48
|
} from '@/components/ui/tooltip';
|
|
49
49
|
import { COUNTRIES } from '@/constants/countries';
|
|
50
|
+
import { useFormDraft } from '@/hooks/use-form-draft';
|
|
51
|
+
import { formatDateTime } from '@/lib/format-date';
|
|
50
52
|
import { cn } from '@/lib/utils';
|
|
51
53
|
import { useApp, useQuery } from '@hed-hog/next-app-provider';
|
|
52
54
|
import { zodResolver } from '@hookform/resolvers/zod';
|
|
53
|
-
import { format } from 'date-fns';
|
|
55
|
+
import { format, formatDistanceToNow } from 'date-fns';
|
|
54
56
|
import { enUS, ptBR } from 'date-fns/locale';
|
|
55
57
|
import {
|
|
56
58
|
Building2,
|
|
@@ -69,8 +71,15 @@ import {
|
|
|
69
71
|
User,
|
|
70
72
|
} from 'lucide-react';
|
|
71
73
|
import { useTranslations } from 'next-intl';
|
|
72
|
-
import {
|
|
73
|
-
|
|
74
|
+
import {
|
|
75
|
+
type ChangeEvent,
|
|
76
|
+
useCallback,
|
|
77
|
+
useEffect,
|
|
78
|
+
useMemo,
|
|
79
|
+
useRef,
|
|
80
|
+
useState,
|
|
81
|
+
} from 'react';
|
|
82
|
+
import { useForm, useWatch } from 'react-hook-form';
|
|
74
83
|
import { toast } from 'sonner';
|
|
75
84
|
import { z } from 'zod';
|
|
76
85
|
|
|
@@ -94,7 +103,12 @@ type PersonFormSheetProps = {
|
|
|
94
103
|
contactTypes: ContactTypeOption[];
|
|
95
104
|
documentTypes: DocumentTypeOption[];
|
|
96
105
|
onOpenChange: (open: boolean) => void;
|
|
97
|
-
onSuccess: () => void
|
|
106
|
+
onSuccess: (person?: Person) => void | Promise<void>;
|
|
107
|
+
initialEmployerCompanyId?: number | null;
|
|
108
|
+
initialEmployerCompanyLabel?: string;
|
|
109
|
+
title?: string;
|
|
110
|
+
description?: string;
|
|
111
|
+
allowedTypes?: Array<Person['type']>;
|
|
98
112
|
};
|
|
99
113
|
|
|
100
114
|
type PersonFormValues = {
|
|
@@ -192,6 +206,34 @@ type PersonSubmitPayload = {
|
|
|
192
206
|
}>;
|
|
193
207
|
};
|
|
194
208
|
|
|
209
|
+
type PersonDraftPayload = {
|
|
210
|
+
mode: 'create' | 'edit';
|
|
211
|
+
personId: number | null;
|
|
212
|
+
values: {
|
|
213
|
+
name: string;
|
|
214
|
+
type: 'individual' | 'company';
|
|
215
|
+
status: 'active' | 'inactive';
|
|
216
|
+
birth_date: string | null;
|
|
217
|
+
gender: PersonGender | null;
|
|
218
|
+
job_title: string | null;
|
|
219
|
+
employer_company_id: number | null;
|
|
220
|
+
trade_name: string | null;
|
|
221
|
+
foundation_date: string | null;
|
|
222
|
+
legal_nature: string | null;
|
|
223
|
+
owner_user_id: number | null;
|
|
224
|
+
source: PersonSource | null;
|
|
225
|
+
lifecycle_stage: PersonLifecycleStage | null;
|
|
226
|
+
next_action_at: string | null;
|
|
227
|
+
};
|
|
228
|
+
contacts: EditablePersonContact[];
|
|
229
|
+
addresses: EditablePersonAddress[];
|
|
230
|
+
documents: EditablePersonDocument[];
|
|
231
|
+
avatarId: number | null;
|
|
232
|
+
avatarPreviewUrl: string;
|
|
233
|
+
};
|
|
234
|
+
|
|
235
|
+
const PERSON_FORM_DRAFT_STORAGE_KEY = 'contact-person-form-draft';
|
|
236
|
+
|
|
195
237
|
type PendingDuplicateSubmission = {
|
|
196
238
|
payload: PersonSubmitPayload;
|
|
197
239
|
normalizedType: 'individual' | 'company';
|
|
@@ -258,6 +300,47 @@ function toDatetimeLocalValue(value?: string | null) {
|
|
|
258
300
|
return localDate.toISOString().slice(0, 16);
|
|
259
301
|
}
|
|
260
302
|
|
|
303
|
+
function padTwoDigits(value: number) {
|
|
304
|
+
return String(value).padStart(2, '0');
|
|
305
|
+
}
|
|
306
|
+
|
|
307
|
+
function parseDatetimeLocalValue(value?: string | null) {
|
|
308
|
+
if (!value) return undefined;
|
|
309
|
+
|
|
310
|
+
const match = value.match(/^(\d{4})-(\d{2})-(\d{2})T(\d{2}):(\d{2})$/);
|
|
311
|
+
|
|
312
|
+
if (!match) return undefined;
|
|
313
|
+
|
|
314
|
+
const [, year, month, day, hours, minutes] = match;
|
|
315
|
+
const parsedDate = new Date(
|
|
316
|
+
Number(year),
|
|
317
|
+
Number(month) - 1,
|
|
318
|
+
Number(day),
|
|
319
|
+
Number(hours),
|
|
320
|
+
Number(minutes)
|
|
321
|
+
);
|
|
322
|
+
|
|
323
|
+
if (Number.isNaN(parsedDate.getTime())) {
|
|
324
|
+
return undefined;
|
|
325
|
+
}
|
|
326
|
+
|
|
327
|
+
if (
|
|
328
|
+
parsedDate.getFullYear() !== Number(year) ||
|
|
329
|
+
parsedDate.getMonth() !== Number(month) - 1 ||
|
|
330
|
+
parsedDate.getDate() !== Number(day) ||
|
|
331
|
+
parsedDate.getHours() !== Number(hours) ||
|
|
332
|
+
parsedDate.getMinutes() !== Number(minutes)
|
|
333
|
+
) {
|
|
334
|
+
return undefined;
|
|
335
|
+
}
|
|
336
|
+
|
|
337
|
+
return parsedDate;
|
|
338
|
+
}
|
|
339
|
+
|
|
340
|
+
function buildDatetimeLocalValue(date: Date) {
|
|
341
|
+
return `${format(date, 'yyyy-MM-dd')}T${padTwoDigits(date.getHours())}:${padTwoDigits(date.getMinutes())}`;
|
|
342
|
+
}
|
|
343
|
+
|
|
261
344
|
async function fetchViaCep(cep: string) {
|
|
262
345
|
try {
|
|
263
346
|
const response = await fetch(`https://viacep.com.br/ws/${cep}/json/`);
|
|
@@ -392,6 +475,217 @@ function DatePickerWithYearMonth({
|
|
|
392
475
|
);
|
|
393
476
|
}
|
|
394
477
|
|
|
478
|
+
function DateTimePickerWithTime({
|
|
479
|
+
value,
|
|
480
|
+
onChange,
|
|
481
|
+
placeholder,
|
|
482
|
+
localeCode,
|
|
483
|
+
clearLabel,
|
|
484
|
+
}: {
|
|
485
|
+
value?: string | null;
|
|
486
|
+
onChange: (value: string) => void;
|
|
487
|
+
placeholder: string;
|
|
488
|
+
localeCode: string;
|
|
489
|
+
clearLabel: string;
|
|
490
|
+
}) {
|
|
491
|
+
const [isOpen, setIsOpen] = useState(false);
|
|
492
|
+
const locale = localeCode.startsWith('pt') ? ptBR : enUS;
|
|
493
|
+
const parsedValue = useMemo(() => parseDatetimeLocalValue(value), [value]);
|
|
494
|
+
const [viewDate, setViewDate] = useState(parsedValue || new Date());
|
|
495
|
+
const currentViewDate = parsedValue || viewDate;
|
|
496
|
+
|
|
497
|
+
const currentYear = new Date().getFullYear();
|
|
498
|
+
const years = useMemo(() => {
|
|
499
|
+
const values: number[] = [];
|
|
500
|
+
for (let year = currentYear + 10; year >= currentYear - 10; year -= 1) {
|
|
501
|
+
values.push(year);
|
|
502
|
+
}
|
|
503
|
+
return values;
|
|
504
|
+
}, [currentYear]);
|
|
505
|
+
|
|
506
|
+
const months = useMemo(
|
|
507
|
+
() =>
|
|
508
|
+
Array.from({ length: 12 }, (_, index) =>
|
|
509
|
+
format(new Date(2024, index, 1), 'LLLL', { locale })
|
|
510
|
+
),
|
|
511
|
+
[locale]
|
|
512
|
+
);
|
|
513
|
+
|
|
514
|
+
const hours = useMemo(
|
|
515
|
+
() => Array.from({ length: 24 }, (_, index) => padTwoDigits(index)),
|
|
516
|
+
[]
|
|
517
|
+
);
|
|
518
|
+
const minutes = useMemo(
|
|
519
|
+
() => Array.from({ length: 60 }, (_, index) => padTwoDigits(index)),
|
|
520
|
+
[]
|
|
521
|
+
);
|
|
522
|
+
|
|
523
|
+
const handleYearChange = (year: string) => {
|
|
524
|
+
const nextDate = new Date(currentViewDate);
|
|
525
|
+
nextDate.setFullYear(Number(year));
|
|
526
|
+
setViewDate(nextDate);
|
|
527
|
+
};
|
|
528
|
+
|
|
529
|
+
const handleMonthChange = (month: string) => {
|
|
530
|
+
const nextDate = new Date(currentViewDate);
|
|
531
|
+
nextDate.setMonth(Number(month));
|
|
532
|
+
setViewDate(nextDate);
|
|
533
|
+
};
|
|
534
|
+
|
|
535
|
+
const handleDateSelect = (selectedDate: Date | undefined) => {
|
|
536
|
+
if (!selectedDate) {
|
|
537
|
+
onChange('');
|
|
538
|
+
return;
|
|
539
|
+
}
|
|
540
|
+
|
|
541
|
+
const baseDate = parsedValue || new Date();
|
|
542
|
+
const nextDate = new Date(selectedDate);
|
|
543
|
+
nextDate.setHours(baseDate.getHours(), baseDate.getMinutes(), 0, 0);
|
|
544
|
+
setViewDate(nextDate);
|
|
545
|
+
onChange(buildDatetimeLocalValue(nextDate));
|
|
546
|
+
};
|
|
547
|
+
|
|
548
|
+
const handleTimePartChange = (
|
|
549
|
+
part: 'hours' | 'minutes',
|
|
550
|
+
nextValue: string
|
|
551
|
+
) => {
|
|
552
|
+
const baseDate = parsedValue || new Date(currentViewDate);
|
|
553
|
+
const nextDate = new Date(baseDate);
|
|
554
|
+
|
|
555
|
+
if (part === 'hours') {
|
|
556
|
+
nextDate.setHours(Number(nextValue));
|
|
557
|
+
} else {
|
|
558
|
+
nextDate.setMinutes(Number(nextValue));
|
|
559
|
+
}
|
|
560
|
+
|
|
561
|
+
nextDate.setSeconds(0, 0);
|
|
562
|
+
setViewDate(nextDate);
|
|
563
|
+
onChange(buildDatetimeLocalValue(nextDate));
|
|
564
|
+
};
|
|
565
|
+
|
|
566
|
+
return (
|
|
567
|
+
<Popover open={isOpen} onOpenChange={setIsOpen}>
|
|
568
|
+
<PopoverTrigger asChild>
|
|
569
|
+
<Button
|
|
570
|
+
type="button"
|
|
571
|
+
variant="outline"
|
|
572
|
+
className={cn(
|
|
573
|
+
'h-9 w-full min-w-0 justify-start text-left text-xs font-normal',
|
|
574
|
+
!parsedValue && 'text-muted-foreground'
|
|
575
|
+
)}
|
|
576
|
+
>
|
|
577
|
+
<CalendarIcon className="mr-2 h-3.5 w-3.5" />
|
|
578
|
+
{parsedValue ? (
|
|
579
|
+
format(parsedValue, 'dd/MM/yyyy HH:mm', { locale })
|
|
580
|
+
) : (
|
|
581
|
+
<span className="block truncate text-xs">{placeholder}</span>
|
|
582
|
+
)}
|
|
583
|
+
</Button>
|
|
584
|
+
</PopoverTrigger>
|
|
585
|
+
<PopoverContent className="w-[320px] p-0" align="start">
|
|
586
|
+
<div className="flex items-center gap-1 border-b p-2">
|
|
587
|
+
<Select
|
|
588
|
+
value={String(currentViewDate.getMonth())}
|
|
589
|
+
onValueChange={handleMonthChange}
|
|
590
|
+
>
|
|
591
|
+
<SelectTrigger className="h-8 flex-1 text-xs">
|
|
592
|
+
<SelectValue />
|
|
593
|
+
</SelectTrigger>
|
|
594
|
+
<SelectContent>
|
|
595
|
+
{months.map((month, index) => (
|
|
596
|
+
<SelectItem key={month} value={String(index)}>
|
|
597
|
+
{month}
|
|
598
|
+
</SelectItem>
|
|
599
|
+
))}
|
|
600
|
+
</SelectContent>
|
|
601
|
+
</Select>
|
|
602
|
+
<Select
|
|
603
|
+
value={String(currentViewDate.getFullYear())}
|
|
604
|
+
onValueChange={handleYearChange}
|
|
605
|
+
>
|
|
606
|
+
<SelectTrigger className="h-8 w-24 text-xs">
|
|
607
|
+
<SelectValue />
|
|
608
|
+
</SelectTrigger>
|
|
609
|
+
<SelectContent className="max-h-60">
|
|
610
|
+
{years.map((year) => (
|
|
611
|
+
<SelectItem key={year} value={String(year)}>
|
|
612
|
+
{year}
|
|
613
|
+
</SelectItem>
|
|
614
|
+
))}
|
|
615
|
+
</SelectContent>
|
|
616
|
+
</Select>
|
|
617
|
+
</div>
|
|
618
|
+
|
|
619
|
+
<Calendar
|
|
620
|
+
mode="single"
|
|
621
|
+
selected={parsedValue}
|
|
622
|
+
onSelect={handleDateSelect}
|
|
623
|
+
month={currentViewDate}
|
|
624
|
+
onMonthChange={setViewDate}
|
|
625
|
+
initialFocus
|
|
626
|
+
/>
|
|
627
|
+
|
|
628
|
+
<div className="border-t p-3">
|
|
629
|
+
<div className="grid grid-cols-[1fr_1fr_auto] gap-2">
|
|
630
|
+
<Select
|
|
631
|
+
value={
|
|
632
|
+
parsedValue ? padTwoDigits(parsedValue.getHours()) : undefined
|
|
633
|
+
}
|
|
634
|
+
onValueChange={(nextValue) =>
|
|
635
|
+
handleTimePartChange('hours', nextValue)
|
|
636
|
+
}
|
|
637
|
+
disabled={!parsedValue}
|
|
638
|
+
>
|
|
639
|
+
<SelectTrigger className="h-8 text-xs">
|
|
640
|
+
<SelectValue placeholder="HH" />
|
|
641
|
+
</SelectTrigger>
|
|
642
|
+
<SelectContent className="max-h-60">
|
|
643
|
+
{hours.map((hour) => (
|
|
644
|
+
<SelectItem key={hour} value={hour}>
|
|
645
|
+
{hour}
|
|
646
|
+
</SelectItem>
|
|
647
|
+
))}
|
|
648
|
+
</SelectContent>
|
|
649
|
+
</Select>
|
|
650
|
+
|
|
651
|
+
<Select
|
|
652
|
+
value={
|
|
653
|
+
parsedValue ? padTwoDigits(parsedValue.getMinutes()) : undefined
|
|
654
|
+
}
|
|
655
|
+
onValueChange={(nextValue) =>
|
|
656
|
+
handleTimePartChange('minutes', nextValue)
|
|
657
|
+
}
|
|
658
|
+
disabled={!parsedValue}
|
|
659
|
+
>
|
|
660
|
+
<SelectTrigger className="h-8 text-xs">
|
|
661
|
+
<SelectValue placeholder="MM" />
|
|
662
|
+
</SelectTrigger>
|
|
663
|
+
<SelectContent className="max-h-60">
|
|
664
|
+
{minutes.map((minute) => (
|
|
665
|
+
<SelectItem key={minute} value={minute}>
|
|
666
|
+
{minute}
|
|
667
|
+
</SelectItem>
|
|
668
|
+
))}
|
|
669
|
+
</SelectContent>
|
|
670
|
+
</Select>
|
|
671
|
+
|
|
672
|
+
<Button
|
|
673
|
+
type="button"
|
|
674
|
+
variant="ghost"
|
|
675
|
+
size="sm"
|
|
676
|
+
className="h-8 px-2 text-xs"
|
|
677
|
+
onClick={() => onChange('')}
|
|
678
|
+
disabled={!parsedValue}
|
|
679
|
+
>
|
|
680
|
+
{clearLabel}
|
|
681
|
+
</Button>
|
|
682
|
+
</div>
|
|
683
|
+
</div>
|
|
684
|
+
</PopoverContent>
|
|
685
|
+
</Popover>
|
|
686
|
+
);
|
|
687
|
+
}
|
|
688
|
+
|
|
395
689
|
export function PersonFormSheet({
|
|
396
690
|
open,
|
|
397
691
|
person,
|
|
@@ -399,13 +693,39 @@ export function PersonFormSheet({
|
|
|
399
693
|
documentTypes,
|
|
400
694
|
onOpenChange,
|
|
401
695
|
onSuccess,
|
|
696
|
+
initialEmployerCompanyId = null,
|
|
697
|
+
initialEmployerCompanyLabel = '',
|
|
698
|
+
title,
|
|
699
|
+
description,
|
|
700
|
+
allowedTypes,
|
|
402
701
|
}: PersonFormSheetProps) {
|
|
403
702
|
const t = useTranslations('contact.ContactPage');
|
|
404
703
|
const { request, currentLocaleCode, getSettingValue, user } = useApp();
|
|
405
704
|
const isEditing = Boolean(person);
|
|
406
705
|
const allowCompanyRegistration =
|
|
407
706
|
getSettingValue('contact-allow-company-registration') !== false;
|
|
408
|
-
const
|
|
707
|
+
const effectiveAllowedTypes = useMemo<Array<Person['type']>>(() => {
|
|
708
|
+
const sanitized = Array.from(
|
|
709
|
+
new Set(
|
|
710
|
+
(allowedTypes ?? []).filter(
|
|
711
|
+
(type): type is Person['type'] =>
|
|
712
|
+
type === 'individual' ||
|
|
713
|
+
(type === 'company' && allowCompanyRegistration)
|
|
714
|
+
)
|
|
715
|
+
)
|
|
716
|
+
);
|
|
717
|
+
|
|
718
|
+
if (sanitized.length > 0) {
|
|
719
|
+
return sanitized;
|
|
720
|
+
}
|
|
721
|
+
|
|
722
|
+
return allowCompanyRegistration
|
|
723
|
+
? ['individual', 'company']
|
|
724
|
+
: ['individual'];
|
|
725
|
+
}, [allowCompanyRegistration, allowedTypes]);
|
|
726
|
+
const defaultPersonType = effectiveAllowedTypes[0] ?? 'individual';
|
|
727
|
+
const canUseCompanyType = effectiveAllowedTypes.includes('company');
|
|
728
|
+
const showTypeField = effectiveAllowedTypes.length > 1;
|
|
409
729
|
|
|
410
730
|
const personSchema = useMemo(
|
|
411
731
|
() =>
|
|
@@ -522,7 +842,112 @@ export function PersonFormSheet({
|
|
|
522
842
|
};
|
|
523
843
|
|
|
524
844
|
const watchType = watch('type');
|
|
845
|
+
const watchedFormValues = useWatch({
|
|
846
|
+
control: form.control,
|
|
847
|
+
});
|
|
525
848
|
|
|
849
|
+
const hasDraftContent = useMemo(
|
|
850
|
+
() =>
|
|
851
|
+
Boolean(
|
|
852
|
+
(watchedFormValues.name ?? '').trim() ||
|
|
853
|
+
(watchedFormValues.job_title ?? '').trim() ||
|
|
854
|
+
(watchedFormValues.trade_name ?? '').trim() ||
|
|
855
|
+
(watchedFormValues.legal_nature ?? '').trim() ||
|
|
856
|
+
(watchedFormValues.next_action_at ?? '').trim() ||
|
|
857
|
+
watchedFormValues.type === 'company' ||
|
|
858
|
+
watchedFormValues.status === 'inactive' ||
|
|
859
|
+
watchedFormValues.birth_date ||
|
|
860
|
+
watchedFormValues.foundation_date ||
|
|
861
|
+
watchedFormValues.gender ||
|
|
862
|
+
watchedFormValues.source ||
|
|
863
|
+
(watchedFormValues.lifecycle_stage ?? 'new') !== 'new' ||
|
|
864
|
+
avatarId != null ||
|
|
865
|
+
contacts.length > 0 ||
|
|
866
|
+
addresses.length > 0 ||
|
|
867
|
+
documents.length > 0
|
|
868
|
+
),
|
|
869
|
+
[watchedFormValues, avatarId, contacts, addresses, documents]
|
|
870
|
+
);
|
|
871
|
+
|
|
872
|
+
const draftValue = useMemo<PersonDraftPayload>(
|
|
873
|
+
() => ({
|
|
874
|
+
mode: person?.id ? 'edit' : 'create',
|
|
875
|
+
personId: person?.id ?? null,
|
|
876
|
+
values: {
|
|
877
|
+
name: watchedFormValues.name ?? '',
|
|
878
|
+
type: watchedFormValues.type ?? 'individual',
|
|
879
|
+
status: watchedFormValues.status ?? 'active',
|
|
880
|
+
birth_date: watchedFormValues.birth_date
|
|
881
|
+
? watchedFormValues.birth_date.toISOString()
|
|
882
|
+
: null,
|
|
883
|
+
gender: watchedFormValues.gender ?? null,
|
|
884
|
+
job_title: watchedFormValues.job_title ?? null,
|
|
885
|
+
employer_company_id: watchedFormValues.employer_company_id ?? null,
|
|
886
|
+
trade_name: watchedFormValues.trade_name ?? null,
|
|
887
|
+
foundation_date: watchedFormValues.foundation_date
|
|
888
|
+
? watchedFormValues.foundation_date.toISOString()
|
|
889
|
+
: null,
|
|
890
|
+
legal_nature: watchedFormValues.legal_nature ?? null,
|
|
891
|
+
owner_user_id: watchedFormValues.owner_user_id ?? null,
|
|
892
|
+
source: watchedFormValues.source ?? null,
|
|
893
|
+
lifecycle_stage: watchedFormValues.lifecycle_stage ?? 'new',
|
|
894
|
+
next_action_at: watchedFormValues.next_action_at ?? null,
|
|
895
|
+
},
|
|
896
|
+
contacts,
|
|
897
|
+
addresses,
|
|
898
|
+
documents,
|
|
899
|
+
avatarId,
|
|
900
|
+
avatarPreviewUrl,
|
|
901
|
+
}),
|
|
902
|
+
[
|
|
903
|
+
watchedFormValues,
|
|
904
|
+
contacts,
|
|
905
|
+
addresses,
|
|
906
|
+
documents,
|
|
907
|
+
avatarId,
|
|
908
|
+
avatarPreviewUrl,
|
|
909
|
+
person?.id,
|
|
910
|
+
]
|
|
911
|
+
);
|
|
912
|
+
|
|
913
|
+
const {
|
|
914
|
+
clearDraft,
|
|
915
|
+
loadDraft,
|
|
916
|
+
hasDraft,
|
|
917
|
+
savedAt: draftSavedAt,
|
|
918
|
+
} = useFormDraft<PersonDraftPayload>({
|
|
919
|
+
storageKey: PERSON_FORM_DRAFT_STORAGE_KEY,
|
|
920
|
+
value: draftValue,
|
|
921
|
+
hasData: hasDraftContent,
|
|
922
|
+
enabled: open,
|
|
923
|
+
});
|
|
924
|
+
|
|
925
|
+
const draftStatusContent = useMemo(() => {
|
|
926
|
+
if (!hasDraft || !draftSavedAt) {
|
|
927
|
+
return null;
|
|
928
|
+
}
|
|
929
|
+
|
|
930
|
+
const savedDate = new Date(draftSavedAt);
|
|
931
|
+
|
|
932
|
+
if (Number.isNaN(savedDate.getTime())) {
|
|
933
|
+
return null;
|
|
934
|
+
}
|
|
935
|
+
|
|
936
|
+
const locale = currentLocaleCode.startsWith('pt') ? ptBR : enUS;
|
|
937
|
+
const relativeLabel = formatDistanceToNow(savedDate, {
|
|
938
|
+
addSuffix: true,
|
|
939
|
+
locale,
|
|
940
|
+
});
|
|
941
|
+
const absoluteLabel = formatDateTime(
|
|
942
|
+
savedDate,
|
|
943
|
+
getSettingValue,
|
|
944
|
+
currentLocaleCode
|
|
945
|
+
);
|
|
946
|
+
|
|
947
|
+
return currentLocaleCode.startsWith('pt')
|
|
948
|
+
? `Rascunho salvo ${relativeLabel} • Último salvamento: ${absoluteLabel}`
|
|
949
|
+
: `Draft saved ${relativeLabel} • Last saved: ${absoluteLabel}`;
|
|
950
|
+
}, [draftSavedAt, currentLocaleCode, getSettingValue, hasDraft]);
|
|
526
951
|
const { data: ownerOptions = [] } = useQuery<UserOption[]>({
|
|
527
952
|
queryKey: ['contact-person-owner-options', currentLocaleCode],
|
|
528
953
|
queryFn: async () => {
|
|
@@ -538,77 +963,112 @@ export function PersonFormSheet({
|
|
|
538
963
|
});
|
|
539
964
|
|
|
540
965
|
useEffect(() => {
|
|
541
|
-
if (
|
|
966
|
+
if (effectiveAllowedTypes.includes(watchType)) {
|
|
542
967
|
return;
|
|
543
968
|
}
|
|
544
969
|
|
|
545
|
-
setValue('type',
|
|
546
|
-
}, [
|
|
970
|
+
setValue('type', defaultPersonType);
|
|
971
|
+
}, [defaultPersonType, effectiveAllowedTypes, setValue, watchType]);
|
|
547
972
|
|
|
548
973
|
useEffect(() => {
|
|
549
974
|
if (!open) return;
|
|
550
975
|
|
|
551
976
|
hasSavedChangesRef.current = false;
|
|
977
|
+
const storedDraft = loadDraft();
|
|
978
|
+
const shouldRestoreDraft = Boolean(
|
|
979
|
+
storedDraft &&
|
|
980
|
+
((!person && storedDraft.payload.mode === 'create') ||
|
|
981
|
+
(person &&
|
|
982
|
+
storedDraft.payload.mode === 'edit' &&
|
|
983
|
+
storedDraft.payload.personId === Number(person.id)))
|
|
984
|
+
);
|
|
985
|
+
const restoredDraft = shouldRestoreDraft ? storedDraft?.payload : null;
|
|
986
|
+
const initialTypeCandidate =
|
|
987
|
+
restoredDraft?.values.type ?? person?.type ?? defaultPersonType;
|
|
988
|
+
|
|
552
989
|
reset({
|
|
553
|
-
name: person?.name
|
|
554
|
-
type:
|
|
555
|
-
?
|
|
556
|
-
:
|
|
557
|
-
status: person?.status
|
|
558
|
-
birth_date:
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
|
|
563
|
-
|
|
564
|
-
|
|
565
|
-
|
|
566
|
-
|
|
990
|
+
name: restoredDraft?.values.name ?? person?.name ?? '',
|
|
991
|
+
type: effectiveAllowedTypes.includes(initialTypeCandidate)
|
|
992
|
+
? initialTypeCandidate
|
|
993
|
+
: defaultPersonType,
|
|
994
|
+
status: restoredDraft?.values.status ?? person?.status ?? 'active',
|
|
995
|
+
birth_date: restoredDraft?.values.birth_date
|
|
996
|
+
? new Date(restoredDraft.values.birth_date)
|
|
997
|
+
: person?.birth_date
|
|
998
|
+
? new Date(person.birth_date)
|
|
999
|
+
: null,
|
|
1000
|
+
gender: restoredDraft?.values.gender ?? person?.gender ?? null,
|
|
1001
|
+
job_title: restoredDraft?.values.job_title ?? person?.job_title ?? '',
|
|
1002
|
+
employer_company_id:
|
|
1003
|
+
restoredDraft?.values.employer_company_id ??
|
|
1004
|
+
person?.employer_company_id ??
|
|
1005
|
+
initialEmployerCompanyId ??
|
|
1006
|
+
null,
|
|
1007
|
+
trade_name: restoredDraft?.values.trade_name ?? person?.trade_name ?? '',
|
|
1008
|
+
foundation_date: restoredDraft?.values.foundation_date
|
|
1009
|
+
? new Date(restoredDraft.values.foundation_date)
|
|
1010
|
+
: person?.foundation_date
|
|
1011
|
+
? new Date(person.foundation_date)
|
|
1012
|
+
: null,
|
|
1013
|
+
legal_nature:
|
|
1014
|
+
restoredDraft?.values.legal_nature ?? person?.legal_nature ?? '',
|
|
567
1015
|
owner_user_id:
|
|
1016
|
+
restoredDraft?.values.owner_user_id ??
|
|
568
1017
|
person?.owner_user_id ??
|
|
569
1018
|
(person ? null : Number(user?.id || 0) || null),
|
|
570
|
-
source: person?.source
|
|
571
|
-
lifecycle_stage:
|
|
572
|
-
|
|
1019
|
+
source: restoredDraft?.values.source ?? person?.source ?? null,
|
|
1020
|
+
lifecycle_stage:
|
|
1021
|
+
restoredDraft?.values.lifecycle_stage ??
|
|
1022
|
+
person?.lifecycle_stage ??
|
|
1023
|
+
'new',
|
|
1024
|
+
next_action_at:
|
|
1025
|
+
restoredDraft?.values.next_action_at ??
|
|
1026
|
+
toDatetimeLocalValue(person?.next_action_at || null),
|
|
573
1027
|
});
|
|
574
|
-
const initialAvatarId =
|
|
1028
|
+
const initialAvatarId =
|
|
1029
|
+
restoredDraft?.avatarId ?? person?.avatar_id ?? null;
|
|
575
1030
|
setAvatarId(initialAvatarId);
|
|
576
1031
|
setPersistedAvatarId(initialAvatarId);
|
|
577
|
-
setAvatarPreviewUrl(
|
|
1032
|
+
setAvatarPreviewUrl(
|
|
1033
|
+
restoredDraft?.avatarPreviewUrl || getPersonAvatarUrl(initialAvatarId)
|
|
1034
|
+
);
|
|
578
1035
|
setIsUploadingAvatar(false);
|
|
579
1036
|
setAvatarUploadProgress(0);
|
|
580
1037
|
|
|
581
1038
|
setContacts(
|
|
582
|
-
|
|
583
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
|
|
589
|
-
|
|
590
|
-
|
|
1039
|
+
restoredDraft?.contacts ??
|
|
1040
|
+
(person?.contact || []).map((contact, index) => ({
|
|
1041
|
+
...contact,
|
|
1042
|
+
value: ['PHONE', 'MOBILE', 'WHATSAPP'].includes(
|
|
1043
|
+
String(contact.contact_type?.code || '').toUpperCase()
|
|
1044
|
+
)
|
|
1045
|
+
? applyPhoneMask(contact.value || '')
|
|
1046
|
+
: contact.value || '',
|
|
1047
|
+
clientId: `contact-${contact.id ?? index}`,
|
|
1048
|
+
}))
|
|
591
1049
|
);
|
|
592
1050
|
|
|
593
1051
|
setAddresses(
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
1052
|
+
restoredDraft?.addresses ??
|
|
1053
|
+
(person?.address || []).map((address, index) => ({
|
|
1054
|
+
...address,
|
|
1055
|
+
clientId: `address-${address.id ?? index}`,
|
|
1056
|
+
}))
|
|
598
1057
|
);
|
|
599
1058
|
|
|
600
1059
|
setDocuments(
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
1060
|
+
restoredDraft?.documents ??
|
|
1061
|
+
(person?.document || []).map((document, index) => ({
|
|
1062
|
+
...document,
|
|
1063
|
+
value:
|
|
1064
|
+
String(document.document_type?.code || '').toUpperCase() === 'CPF'
|
|
1065
|
+
? applyCpfMask(document.value || '')
|
|
1066
|
+
: String(document.document_type?.code || '').toUpperCase() ===
|
|
1067
|
+
'CNPJ'
|
|
1068
|
+
? applyCnpjMask(document.value || '')
|
|
1069
|
+
: document.value || '',
|
|
1070
|
+
clientId: `document-${document.id ?? index}`,
|
|
1071
|
+
}))
|
|
612
1072
|
);
|
|
613
1073
|
|
|
614
1074
|
setContactsOpen(true);
|
|
@@ -619,7 +1079,25 @@ export function PersonFormSheet({
|
|
|
619
1079
|
setPendingDuplicateSubmission(null);
|
|
620
1080
|
setDuplicateTargetPersonId(null);
|
|
621
1081
|
setIsResolvingDuplicateAction(false);
|
|
622
|
-
}, [
|
|
1082
|
+
}, [
|
|
1083
|
+
defaultPersonType,
|
|
1084
|
+
effectiveAllowedTypes,
|
|
1085
|
+
initialEmployerCompanyId,
|
|
1086
|
+
loadDraft,
|
|
1087
|
+
open,
|
|
1088
|
+
person,
|
|
1089
|
+
reset,
|
|
1090
|
+
user?.id,
|
|
1091
|
+
]);
|
|
1092
|
+
|
|
1093
|
+
const resolvePersonById = async (personId: number) => {
|
|
1094
|
+
const response = await request<Person>({
|
|
1095
|
+
url: `/person/${personId}`,
|
|
1096
|
+
method: 'GET',
|
|
1097
|
+
});
|
|
1098
|
+
|
|
1099
|
+
return response.data;
|
|
1100
|
+
};
|
|
623
1101
|
|
|
624
1102
|
const getPersonInitials = (name: string) =>
|
|
625
1103
|
name
|
|
@@ -639,25 +1117,28 @@ export function PersonFormSheet({
|
|
|
639
1117
|
? url
|
|
640
1118
|
: `${String(process.env.NEXT_PUBLIC_API_BASE_URL || '')}${url}`;
|
|
641
1119
|
|
|
642
|
-
const deleteFileById =
|
|
643
|
-
|
|
1120
|
+
const deleteFileById = useCallback(
|
|
1121
|
+
async (fileId?: number | null) => {
|
|
1122
|
+
if (!fileId || fileId <= 0) return;
|
|
644
1123
|
|
|
645
|
-
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
|
|
652
|
-
|
|
653
|
-
|
|
654
|
-
|
|
1124
|
+
try {
|
|
1125
|
+
await request({
|
|
1126
|
+
url: '/file',
|
|
1127
|
+
method: 'DELETE',
|
|
1128
|
+
data: { ids: [fileId] },
|
|
1129
|
+
});
|
|
1130
|
+
} catch {
|
|
1131
|
+
// Ignore cleanup failure to keep form interaction stable.
|
|
1132
|
+
}
|
|
1133
|
+
},
|
|
1134
|
+
[request]
|
|
1135
|
+
);
|
|
655
1136
|
|
|
656
|
-
const cleanupUnsavedAvatar = async () => {
|
|
1137
|
+
const cleanupUnsavedAvatar = useCallback(async () => {
|
|
657
1138
|
if (avatarId && avatarId !== persistedAvatarId) {
|
|
658
1139
|
await deleteFileById(avatarId);
|
|
659
1140
|
}
|
|
660
|
-
};
|
|
1141
|
+
}, [avatarId, deleteFileById, persistedAvatarId]);
|
|
661
1142
|
|
|
662
1143
|
const handleAvatarUpload = async (file: File) => {
|
|
663
1144
|
if (!file.type.startsWith('image/')) {
|
|
@@ -747,26 +1228,35 @@ export function PersonFormSheet({
|
|
|
747
1228
|
toast.success(t('avatarRemoveSuccess'));
|
|
748
1229
|
};
|
|
749
1230
|
|
|
750
|
-
const handleSheetOpenChange = (
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
1231
|
+
const handleSheetOpenChange = useCallback(
|
|
1232
|
+
(nextOpen: boolean) => {
|
|
1233
|
+
if (nextOpen) {
|
|
1234
|
+
hasSavedChangesRef.current = false;
|
|
1235
|
+
onOpenChange(true);
|
|
1236
|
+
return;
|
|
1237
|
+
}
|
|
756
1238
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
1239
|
+
if (isUploadingAvatar || isSubmitting) {
|
|
1240
|
+
return;
|
|
1241
|
+
}
|
|
760
1242
|
|
|
761
|
-
|
|
762
|
-
|
|
1243
|
+
const shouldCleanup = !hasSavedChangesRef.current && !hasDraftContent;
|
|
1244
|
+
hasSavedChangesRef.current = false;
|
|
763
1245
|
|
|
764
|
-
|
|
765
|
-
|
|
766
|
-
|
|
1246
|
+
if (shouldCleanup) {
|
|
1247
|
+
void cleanupUnsavedAvatar();
|
|
1248
|
+
}
|
|
767
1249
|
|
|
768
|
-
|
|
769
|
-
|
|
1250
|
+
onOpenChange(false);
|
|
1251
|
+
},
|
|
1252
|
+
[
|
|
1253
|
+
cleanupUnsavedAvatar,
|
|
1254
|
+
hasDraftContent,
|
|
1255
|
+
isSubmitting,
|
|
1256
|
+
isUploadingAvatar,
|
|
1257
|
+
onOpenChange,
|
|
1258
|
+
]
|
|
1259
|
+
);
|
|
770
1260
|
|
|
771
1261
|
useEffect(() => {
|
|
772
1262
|
if (!open) {
|
|
@@ -1309,13 +1799,72 @@ export function PersonFormSheet({
|
|
|
1309
1799
|
return personId;
|
|
1310
1800
|
};
|
|
1311
1801
|
|
|
1312
|
-
const
|
|
1802
|
+
const buildSuccessPerson = (
|
|
1803
|
+
personId: number,
|
|
1804
|
+
payload: PersonSubmitPayload
|
|
1805
|
+
): Person => ({
|
|
1806
|
+
id: personId,
|
|
1807
|
+
name: payload.name,
|
|
1808
|
+
type: payload.type,
|
|
1809
|
+
status: payload.status,
|
|
1810
|
+
avatar_id: payload.avatar_id,
|
|
1811
|
+
birth_date: payload.birth_date,
|
|
1812
|
+
gender: payload.gender,
|
|
1813
|
+
job_title: payload.job_title,
|
|
1814
|
+
employer_company_id: payload.employer_company_id,
|
|
1815
|
+
employer_company:
|
|
1816
|
+
payload.employer_company_id && initialEmployerCompanyLabel
|
|
1817
|
+
? {
|
|
1818
|
+
id: payload.employer_company_id,
|
|
1819
|
+
name: initialEmployerCompanyLabel,
|
|
1820
|
+
type: 'company',
|
|
1821
|
+
status: 'active',
|
|
1822
|
+
}
|
|
1823
|
+
: null,
|
|
1824
|
+
trade_name: payload.trade_name,
|
|
1825
|
+
foundation_date: payload.foundation_date,
|
|
1826
|
+
legal_nature: payload.legal_nature,
|
|
1827
|
+
owner_user_id: payload.owner_user_id,
|
|
1828
|
+
owner_user:
|
|
1829
|
+
ownerOptions.find((option) => option.id === payload.owner_user_id) ??
|
|
1830
|
+
null,
|
|
1831
|
+
source: payload.source,
|
|
1832
|
+
lifecycle_stage: payload.lifecycle_stage,
|
|
1833
|
+
next_action_at: payload.next_action_at,
|
|
1834
|
+
created_at: person?.created_at || new Date().toISOString(),
|
|
1835
|
+
contact: payload.contacts.map((contact) => ({
|
|
1836
|
+
...contact,
|
|
1837
|
+
contact_type:
|
|
1838
|
+
contactTypes.find(
|
|
1839
|
+
(item) => item.contact_type_id === contact.contact_type_id
|
|
1840
|
+
) ?? undefined,
|
|
1841
|
+
})),
|
|
1842
|
+
address: payload.addresses,
|
|
1843
|
+
document: payload.documents.map((document) => ({
|
|
1844
|
+
...document,
|
|
1845
|
+
document_type:
|
|
1846
|
+
documentTypes.find(
|
|
1847
|
+
(item) => item.document_type_id === document.document_type_id
|
|
1848
|
+
) ?? undefined,
|
|
1849
|
+
})),
|
|
1850
|
+
});
|
|
1851
|
+
|
|
1852
|
+
const finalizeSuccess = async (personId: number, fallbackPerson?: Person) => {
|
|
1853
|
+
let resolvedPerson = fallbackPerson;
|
|
1854
|
+
|
|
1855
|
+
try {
|
|
1856
|
+
resolvedPerson = await resolvePersonById(personId);
|
|
1857
|
+
} catch {
|
|
1858
|
+
resolvedPerson = fallbackPerson;
|
|
1859
|
+
}
|
|
1860
|
+
|
|
1313
1861
|
hasSavedChangesRef.current = true;
|
|
1862
|
+
clearDraft();
|
|
1314
1863
|
setPendingDuplicateSubmission(null);
|
|
1315
1864
|
setDuplicateTargetPersonId(null);
|
|
1316
1865
|
setDuplicateDialogOpen(false);
|
|
1317
1866
|
handleSheetOpenChange(false);
|
|
1318
|
-
onSuccess();
|
|
1867
|
+
await onSuccess(resolvedPerson);
|
|
1319
1868
|
};
|
|
1320
1869
|
|
|
1321
1870
|
const handleContinueWithDuplicate = async () => {
|
|
@@ -1323,11 +1872,14 @@ export function PersonFormSheet({
|
|
|
1323
1872
|
|
|
1324
1873
|
try {
|
|
1325
1874
|
setIsResolvingDuplicateAction(true);
|
|
1326
|
-
await persistPersonPayload(
|
|
1875
|
+
const savedPersonId = await persistPersonPayload(
|
|
1327
1876
|
pendingDuplicateSubmission.payload,
|
|
1328
1877
|
pendingDuplicateSubmission.normalizedType
|
|
1329
1878
|
);
|
|
1330
|
-
finalizeSuccess(
|
|
1879
|
+
await finalizeSuccess(
|
|
1880
|
+
savedPersonId,
|
|
1881
|
+
buildSuccessPerson(savedPersonId, pendingDuplicateSubmission.payload)
|
|
1882
|
+
);
|
|
1331
1883
|
} catch (error: unknown) {
|
|
1332
1884
|
const message = error instanceof Error ? error.message : null;
|
|
1333
1885
|
toast.error(message || (isEditing ? t('updateError') : t('createError')));
|
|
@@ -1371,7 +1923,13 @@ export function PersonFormSheet({
|
|
|
1371
1923
|
})
|
|
1372
1924
|
);
|
|
1373
1925
|
|
|
1374
|
-
finalizeSuccess(
|
|
1926
|
+
await finalizeSuccess(
|
|
1927
|
+
duplicateTargetPersonId,
|
|
1928
|
+
buildSuccessPerson(
|
|
1929
|
+
duplicateTargetPersonId,
|
|
1930
|
+
pendingDuplicateSubmission.payload
|
|
1931
|
+
)
|
|
1932
|
+
);
|
|
1375
1933
|
} catch (error: unknown) {
|
|
1376
1934
|
const message = error instanceof Error ? error.message : null;
|
|
1377
1935
|
toast.error(message || t('duplicateMergeError'));
|
|
@@ -1384,9 +1942,10 @@ export function PersonFormSheet({
|
|
|
1384
1942
|
try {
|
|
1385
1943
|
setIsSubmitting(true);
|
|
1386
1944
|
|
|
1387
|
-
const normalizedType =
|
|
1388
|
-
|
|
1389
|
-
|
|
1945
|
+
const normalizedType =
|
|
1946
|
+
canUseCompanyType && values.type === 'company'
|
|
1947
|
+
? 'company'
|
|
1948
|
+
: 'individual';
|
|
1390
1949
|
|
|
1391
1950
|
const payload = buildSubmitPayload(values, normalizedType);
|
|
1392
1951
|
const matches = await findDuplicates(payload);
|
|
@@ -1408,8 +1967,11 @@ export function PersonFormSheet({
|
|
|
1408
1967
|
return;
|
|
1409
1968
|
}
|
|
1410
1969
|
|
|
1411
|
-
await persistPersonPayload(payload, normalizedType);
|
|
1412
|
-
finalizeSuccess(
|
|
1970
|
+
const savedPersonId = await persistPersonPayload(payload, normalizedType);
|
|
1971
|
+
await finalizeSuccess(
|
|
1972
|
+
savedPersonId,
|
|
1973
|
+
buildSuccessPerson(savedPersonId, payload)
|
|
1974
|
+
);
|
|
1413
1975
|
} catch (error: unknown) {
|
|
1414
1976
|
const message = error instanceof Error ? error.message : null;
|
|
1415
1977
|
toast.error(message || (isEditing ? t('updateError') : t('createError')));
|
|
@@ -1421,7 +1983,10 @@ export function PersonFormSheet({
|
|
|
1421
1983
|
return (
|
|
1422
1984
|
<>
|
|
1423
1985
|
<Sheet open={open} onOpenChange={handleSheetOpenChange}>
|
|
1424
|
-
<SheetContent
|
|
1986
|
+
<SheetContent
|
|
1987
|
+
side="right"
|
|
1988
|
+
className="flex w-full flex-col sm:max-w-lg overflow-y-auto"
|
|
1989
|
+
>
|
|
1425
1990
|
<SheetHeader className="shrink-0 border-b p-4">
|
|
1426
1991
|
<div className="flex items-center gap-3">
|
|
1427
1992
|
<div
|
|
@@ -1440,14 +2005,16 @@ export function PersonFormSheet({
|
|
|
1440
2005
|
</div>
|
|
1441
2006
|
<div>
|
|
1442
2007
|
<SheetTitle>
|
|
1443
|
-
{
|
|
2008
|
+
{title ||
|
|
2009
|
+
(isEditing ? t('sheetEditTitle') : t('sheetCreateTitle'))}
|
|
1444
2010
|
</SheetTitle>
|
|
1445
2011
|
<SheetDescription>
|
|
1446
|
-
{
|
|
1447
|
-
|
|
1448
|
-
|
|
1449
|
-
|
|
1450
|
-
|
|
2012
|
+
{description ||
|
|
2013
|
+
(isEditing
|
|
2014
|
+
? t('sheetEditDescription')
|
|
2015
|
+
: canUseCompanyType
|
|
2016
|
+
? t('sheetCreateDescription')
|
|
2017
|
+
: t('sheetCreateDescriptionIndividualOnly'))}
|
|
1451
2018
|
</SheetDescription>
|
|
1452
2019
|
</div>
|
|
1453
2020
|
</div>
|
|
@@ -1565,7 +2132,7 @@ export function PersonFormSheet({
|
|
|
1565
2132
|
: 'grid-cols-1 sm:grid-cols-2'
|
|
1566
2133
|
)}
|
|
1567
2134
|
>
|
|
1568
|
-
{
|
|
2135
|
+
{showTypeField ? (
|
|
1569
2136
|
<div className="space-y-1.5">
|
|
1570
2137
|
<Label className="text-xs font-medium">{t('type')}</Label>
|
|
1571
2138
|
<Select
|
|
@@ -1590,74 +2157,78 @@ export function PersonFormSheet({
|
|
|
1590
2157
|
</Select>
|
|
1591
2158
|
</div>
|
|
1592
2159
|
) : null}
|
|
1593
|
-
<div className="
|
|
1594
|
-
<
|
|
1595
|
-
|
|
1596
|
-
|
|
1597
|
-
|
|
1598
|
-
|
|
1599
|
-
|
|
1600
|
-
|
|
1601
|
-
|
|
1602
|
-
|
|
1603
|
-
|
|
1604
|
-
|
|
1605
|
-
|
|
1606
|
-
|
|
1607
|
-
|
|
1608
|
-
|
|
1609
|
-
|
|
1610
|
-
|
|
1611
|
-
|
|
2160
|
+
<div className="flex items-center gap-3">
|
|
2161
|
+
<div className="space-y-1.5">
|
|
2162
|
+
<Label className="text-xs font-medium">
|
|
2163
|
+
{t('status')}
|
|
2164
|
+
</Label>
|
|
2165
|
+
<Select
|
|
2166
|
+
value={watch('status')}
|
|
2167
|
+
onValueChange={(value: 'active' | 'inactive') =>
|
|
2168
|
+
setValue('status', value)
|
|
2169
|
+
}
|
|
2170
|
+
>
|
|
2171
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
2172
|
+
<SelectValue />
|
|
2173
|
+
</SelectTrigger>
|
|
2174
|
+
<SelectContent>
|
|
2175
|
+
<SelectItem value="active">{t('active')}</SelectItem>
|
|
2176
|
+
<SelectItem value="inactive">
|
|
2177
|
+
{t('inactive')}
|
|
2178
|
+
</SelectItem>
|
|
2179
|
+
</SelectContent>
|
|
2180
|
+
</Select>
|
|
2181
|
+
</div>
|
|
1612
2182
|
|
|
1613
|
-
|
|
1614
|
-
|
|
1615
|
-
|
|
1616
|
-
|
|
1617
|
-
|
|
1618
|
-
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
2183
|
+
{watchType === 'individual' ? (
|
|
2184
|
+
<>
|
|
2185
|
+
<div className="space-y-1.5 mb-1.5">
|
|
2186
|
+
<Label className="text-xs font-medium">
|
|
2187
|
+
{t('birthDate')}
|
|
2188
|
+
</Label>
|
|
2189
|
+
<DatePickerWithYearMonth
|
|
2190
|
+
date={watch('birth_date') || undefined}
|
|
2191
|
+
onSelect={(date) =>
|
|
2192
|
+
setValue('birth_date', date || null)
|
|
2193
|
+
}
|
|
2194
|
+
maxDate={new Date()}
|
|
2195
|
+
placeholder={t('selectDate')}
|
|
2196
|
+
localeCode={currentLocaleCode}
|
|
2197
|
+
/>
|
|
2198
|
+
</div>
|
|
1629
2199
|
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1634
|
-
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1645
|
-
|
|
1646
|
-
|
|
1647
|
-
|
|
1648
|
-
|
|
1649
|
-
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
2200
|
+
<div className="space-y-1.5">
|
|
2201
|
+
<Label className="text-xs font-medium">
|
|
2202
|
+
{t('gender')}
|
|
2203
|
+
</Label>
|
|
2204
|
+
<Select
|
|
2205
|
+
value={watch('gender') || ''}
|
|
2206
|
+
onValueChange={(value: PersonGender) =>
|
|
2207
|
+
setValue('gender', value)
|
|
2208
|
+
}
|
|
2209
|
+
>
|
|
2210
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
2211
|
+
<SelectValue placeholder={t('selectGender')} />
|
|
2212
|
+
</SelectTrigger>
|
|
2213
|
+
<SelectContent>
|
|
2214
|
+
<SelectItem value="male">
|
|
2215
|
+
{t('genderMale')}
|
|
2216
|
+
</SelectItem>
|
|
2217
|
+
<SelectItem value="female">
|
|
2218
|
+
{t('genderFemale')}
|
|
2219
|
+
</SelectItem>
|
|
2220
|
+
<SelectItem value="other">
|
|
2221
|
+
{t('genderOther')}
|
|
2222
|
+
</SelectItem>
|
|
2223
|
+
</SelectContent>
|
|
2224
|
+
</Select>
|
|
2225
|
+
</div>
|
|
2226
|
+
</>
|
|
2227
|
+
) : null}
|
|
2228
|
+
</div>
|
|
1658
2229
|
</div>
|
|
1659
2230
|
|
|
1660
|
-
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-
|
|
2231
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-3">
|
|
1661
2232
|
<div className="space-y-1.5">
|
|
1662
2233
|
<Label className="text-xs font-medium">{t('owner')}</Label>
|
|
1663
2234
|
<Select
|
|
@@ -1764,20 +2335,19 @@ export function PersonFormSheet({
|
|
|
1764
2335
|
</SelectContent>
|
|
1765
2336
|
</Select>
|
|
1766
2337
|
</div>
|
|
2338
|
+
</div>
|
|
1767
2339
|
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
/>
|
|
1780
|
-
</div>
|
|
2340
|
+
<div className="space-y-1.5">
|
|
2341
|
+
<Label className="text-xs font-medium">
|
|
2342
|
+
{t('nextActionAt')}
|
|
2343
|
+
</Label>
|
|
2344
|
+
<DateTimePickerWithTime
|
|
2345
|
+
value={watch('next_action_at') || ''}
|
|
2346
|
+
onChange={(value) => setValue('next_action_at', value)}
|
|
2347
|
+
placeholder={t('selectDate')}
|
|
2348
|
+
localeCode={currentLocaleCode}
|
|
2349
|
+
clearLabel={t('remove')}
|
|
2350
|
+
/>
|
|
1781
2351
|
</div>
|
|
1782
2352
|
|
|
1783
2353
|
{watchType === 'individual' ? (
|
|
@@ -1807,7 +2377,9 @@ export function PersonFormSheet({
|
|
|
1807
2377
|
lockCreateType
|
|
1808
2378
|
valueType="number"
|
|
1809
2379
|
initialSelectedLabel={
|
|
1810
|
-
person?.employer_company?.name ||
|
|
2380
|
+
person?.employer_company?.name ||
|
|
2381
|
+
initialEmployerCompanyLabel ||
|
|
2382
|
+
''
|
|
1811
2383
|
}
|
|
1812
2384
|
/>
|
|
1813
2385
|
</div>
|
|
@@ -2486,6 +3058,16 @@ export function PersonFormSheet({
|
|
|
2486
3058
|
: t('createPerson')}
|
|
2487
3059
|
</Button>
|
|
2488
3060
|
</div>
|
|
3061
|
+
{draftStatusContent ? (
|
|
3062
|
+
<p className="text-xs text-muted-foreground">
|
|
3063
|
+
{draftStatusContent}
|
|
3064
|
+
</p>
|
|
3065
|
+
) : null}
|
|
3066
|
+
{draftStatusContent ? (
|
|
3067
|
+
<p className="text-xs text-muted-foreground">
|
|
3068
|
+
{draftStatusContent}
|
|
3069
|
+
</p>
|
|
3070
|
+
) : null}
|
|
2489
3071
|
<p className="text-xs text-muted-foreground">
|
|
2490
3072
|
{t('formShortcutsHint')}
|
|
2491
3073
|
</p>
|