@hed-hog/contact 0.0.279 → 0.0.285
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 +2 -0
- package/dist/person/dto/create-followup.dto.d.ts +5 -0
- package/dist/person/dto/create-followup.dto.d.ts.map +1 -0
- package/dist/person/dto/create-followup.dto.js +31 -0
- package/dist/person/dto/create-followup.dto.js.map +1 -0
- package/dist/person/dto/create-interaction.dto.d.ts +12 -0
- package/dist/person/dto/create-interaction.dto.d.ts.map +1 -0
- package/dist/person/dto/create-interaction.dto.js +39 -0
- package/dist/person/dto/create-interaction.dto.js.map +1 -0
- package/dist/person/dto/create.dto.d.ts +24 -0
- package/dist/person/dto/create.dto.d.ts.map +1 -1
- package/dist/person/dto/create.dto.js +56 -1
- package/dist/person/dto/create.dto.js.map +1 -1
- package/dist/person/dto/duplicates-query.dto.d.ts +8 -0
- package/dist/person/dto/duplicates-query.dto.d.ts.map +1 -0
- package/dist/person/dto/duplicates-query.dto.js +45 -0
- package/dist/person/dto/duplicates-query.dto.js.map +1 -0
- package/dist/person/dto/merge.dto.d.ts +6 -0
- package/dist/person/dto/merge.dto.d.ts.map +1 -0
- package/dist/person/dto/merge.dto.js +35 -0
- package/dist/person/dto/merge.dto.js.map +1 -0
- package/dist/person/dto/update-lifecycle-stage.dto.d.ts +13 -0
- package/dist/person/dto/update-lifecycle-stage.dto.d.ts.map +1 -0
- package/dist/person/dto/update-lifecycle-stage.dto.js +34 -0
- package/dist/person/dto/update-lifecycle-stage.dto.js.map +1 -0
- package/dist/person/dto/update.dto.d.ts +8 -1
- package/dist/person/dto/update.dto.d.ts.map +1 -1
- package/dist/person/dto/update.dto.js +36 -0
- package/dist/person/dto/update.dto.js.map +1 -1
- package/dist/person/person.controller.d.ts +57 -1
- package/dist/person/person.controller.d.ts.map +1 -1
- package/dist/person/person.controller.js +85 -3
- package/dist/person/person.controller.js.map +1 -1
- package/dist/person/person.service.d.ts +79 -0
- package/dist/person/person.service.d.ts.map +1 -1
- package/dist/person/person.service.js +730 -9
- package/dist/person/person.service.js.map +1 -1
- package/hedhog/data/route.yaml +18 -0
- package/hedhog/frontend/app/_components/crm-coming-soon.tsx.ejs +110 -110
- package/hedhog/frontend/app/_components/crm-nav.tsx.ejs +73 -73
- package/hedhog/frontend/app/_lib/crm-mocks.ts.ejs +498 -256
- package/hedhog/frontend/app/_lib/crm-sections.tsx.ejs +81 -81
- package/hedhog/frontend/app/accounts/_components/account-form-sheet.tsx.ejs +477 -0
- package/hedhog/frontend/app/accounts/_components/account-types.ts.ejs +62 -0
- package/hedhog/frontend/app/accounts/page.tsx.ejs +886 -15
- package/hedhog/frontend/app/activities/page.tsx.ejs +15 -15
- package/hedhog/frontend/app/contact-type/page.tsx.ejs +105 -91
- package/hedhog/frontend/app/dashboard/page.tsx.ejs +506 -573
- package/hedhog/frontend/app/document-type/page.tsx.ejs +105 -91
- package/hedhog/frontend/app/follow-ups/page.tsx.ejs +15 -15
- package/hedhog/frontend/app/page.tsx.ejs +5 -5
- package/hedhog/frontend/app/person/_components/person-form-sheet.tsx.ejs +1440 -1103
- package/hedhog/frontend/app/person/_components/person-interaction-dialog.tsx.ejs +4 -3
- package/hedhog/frontend/app/person/_components/person-types.ts.ejs +14 -0
- package/hedhog/frontend/app/person/page.tsx.ejs +108 -190
- package/hedhog/frontend/app/pipeline/_components/lead-detail-sheet.tsx.ejs +599 -0
- package/hedhog/frontend/app/pipeline/page.tsx.ejs +1074 -299
- package/hedhog/frontend/app/reports/page.tsx.ejs +15 -15
- package/hedhog/frontend/messages/en.json +107 -0
- package/hedhog/frontend/messages/pt.json +106 -0
- package/package.json +6 -6
- package/src/person/dto/create-followup.dto.ts +15 -0
- package/src/person/dto/create-interaction.dto.ts +23 -0
- package/src/person/dto/create.dto.ts +50 -0
- package/src/person/dto/duplicates-query.dto.ts +34 -0
- package/src/person/dto/merge.dto.ts +15 -0
- package/src/person/dto/update-lifecycle-stage.dto.ts +19 -0
- package/src/person/dto/update.dto.ts +31 -1
- package/src/person/person.controller.ts +63 -2
- package/src/person/person.service.ts +1096 -7
|
@@ -1,6 +1,14 @@
|
|
|
1
1
|
'use client';
|
|
2
2
|
|
|
3
3
|
import { PersonFieldWithCreate } from '@/app/(app)/(libraries)/contact/person/_components/person-field-with-create';
|
|
4
|
+
import {
|
|
5
|
+
AlertDialog,
|
|
6
|
+
AlertDialogContent,
|
|
7
|
+
AlertDialogDescription,
|
|
8
|
+
AlertDialogFooter,
|
|
9
|
+
AlertDialogHeader,
|
|
10
|
+
AlertDialogTitle,
|
|
11
|
+
} from '@/components/ui/alert-dialog';
|
|
4
12
|
import { Avatar, AvatarFallback, AvatarImage } from '@/components/ui/avatar';
|
|
5
13
|
import { Badge } from '@/components/ui/badge';
|
|
6
14
|
import { Button } from '@/components/ui/button';
|
|
@@ -136,6 +144,60 @@ type EditablePersonDocument = Omit<PersonDocument, 'document_type_id'> & {
|
|
|
136
144
|
document_type_id: number | null;
|
|
137
145
|
};
|
|
138
146
|
|
|
147
|
+
type DuplicateReason = 'email' | 'phone' | 'document';
|
|
148
|
+
|
|
149
|
+
type DuplicateMatch = {
|
|
150
|
+
id: number;
|
|
151
|
+
name: string;
|
|
152
|
+
reasons: DuplicateReason[];
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
type PersonSubmitPayload = {
|
|
156
|
+
name: string;
|
|
157
|
+
type: 'individual' | 'company';
|
|
158
|
+
status: 'active' | 'inactive';
|
|
159
|
+
avatar_id: number | null;
|
|
160
|
+
birth_date: string | null;
|
|
161
|
+
gender: PersonGender | null;
|
|
162
|
+
job_title: string | null;
|
|
163
|
+
employer_company_id: number | null;
|
|
164
|
+
owner_user_id: number | null;
|
|
165
|
+
source: PersonSource | null;
|
|
166
|
+
lifecycle_stage: PersonLifecycleStage | null;
|
|
167
|
+
next_action_at: string | null;
|
|
168
|
+
trade_name: string | null;
|
|
169
|
+
foundation_date: string | null;
|
|
170
|
+
legal_nature: string | null;
|
|
171
|
+
contacts: Array<{
|
|
172
|
+
id?: number;
|
|
173
|
+
value: string;
|
|
174
|
+
is_primary: boolean;
|
|
175
|
+
contact_type_id: number;
|
|
176
|
+
}>;
|
|
177
|
+
addresses: Array<{
|
|
178
|
+
id?: number;
|
|
179
|
+
line1: string;
|
|
180
|
+
line2: string;
|
|
181
|
+
city: string;
|
|
182
|
+
state: string;
|
|
183
|
+
country_code: string;
|
|
184
|
+
postal_code: string;
|
|
185
|
+
is_primary: boolean;
|
|
186
|
+
address_type: PersonAddress['address_type'];
|
|
187
|
+
}>;
|
|
188
|
+
documents: Array<{
|
|
189
|
+
id?: number;
|
|
190
|
+
value: string;
|
|
191
|
+
document_type_id: number;
|
|
192
|
+
}>;
|
|
193
|
+
};
|
|
194
|
+
|
|
195
|
+
type PendingDuplicateSubmission = {
|
|
196
|
+
payload: PersonSubmitPayload;
|
|
197
|
+
normalizedType: 'individual' | 'company';
|
|
198
|
+
matches: DuplicateMatch[];
|
|
199
|
+
};
|
|
200
|
+
|
|
139
201
|
function createClientId(prefix: string) {
|
|
140
202
|
return `${prefix}-${Date.now()}-${Math.random().toString(36).slice(2, 8)}`;
|
|
141
203
|
}
|
|
@@ -431,6 +493,14 @@ export function PersonFormSheet({
|
|
|
431
493
|
useState<string>('/placeholder.png');
|
|
432
494
|
const [isUploadingAvatar, setIsUploadingAvatar] = useState(false);
|
|
433
495
|
const [avatarUploadProgress, setAvatarUploadProgress] = useState(0);
|
|
496
|
+
const [duplicateDialogOpen, setDuplicateDialogOpen] = useState(false);
|
|
497
|
+
const [pendingDuplicateSubmission, setPendingDuplicateSubmission] =
|
|
498
|
+
useState<PendingDuplicateSubmission | null>(null);
|
|
499
|
+
const [duplicateTargetPersonId, setDuplicateTargetPersonId] = useState<
|
|
500
|
+
number | null
|
|
501
|
+
>(null);
|
|
502
|
+
const [isResolvingDuplicateAction, setIsResolvingDuplicateAction] =
|
|
503
|
+
useState(false);
|
|
434
504
|
const fileInputRef = useRef<HTMLInputElement>(null);
|
|
435
505
|
const hasSavedChangesRef = useRef(false);
|
|
436
506
|
const contactValueRefs = useRef<Record<string, HTMLInputElement | null>>({});
|
|
@@ -545,6 +615,10 @@ export function PersonFormSheet({
|
|
|
545
615
|
setAddressesOpen(true);
|
|
546
616
|
setDocumentsOpen(true);
|
|
547
617
|
setLoadingCEP({});
|
|
618
|
+
setDuplicateDialogOpen(false);
|
|
619
|
+
setPendingDuplicateSubmission(null);
|
|
620
|
+
setDuplicateTargetPersonId(null);
|
|
621
|
+
setIsResolvingDuplicateAction(false);
|
|
548
622
|
}, [allowCompanyRegistration, open, person, reset, user?.id]);
|
|
549
623
|
|
|
550
624
|
const getPersonInitials = (name: string) =>
|
|
@@ -1051,168 +1125,291 @@ export function PersonFormSheet({
|
|
|
1051
1125
|
setLoadingCEP((previous) => ({ ...previous, [clientId]: false }));
|
|
1052
1126
|
};
|
|
1053
1127
|
|
|
1128
|
+
const getDuplicateReasonLabel = (reason: DuplicateReason) => {
|
|
1129
|
+
if (reason === 'email') return t('columnEmail');
|
|
1130
|
+
if (reason === 'phone') return t('columnPhone');
|
|
1131
|
+
return t('documentValue');
|
|
1132
|
+
};
|
|
1133
|
+
|
|
1134
|
+
const buildSubmitPayload = (
|
|
1135
|
+
values: PersonFormValues,
|
|
1136
|
+
normalizedType: 'individual' | 'company'
|
|
1137
|
+
): PersonSubmitPayload => ({
|
|
1138
|
+
name: values.name.trim(),
|
|
1139
|
+
type: normalizedType,
|
|
1140
|
+
status: values.status,
|
|
1141
|
+
avatar_id: avatarId,
|
|
1142
|
+
birth_date: values.birth_date ? values.birth_date.toISOString() : null,
|
|
1143
|
+
gender: values.gender || null,
|
|
1144
|
+
job_title:
|
|
1145
|
+
normalizedType === 'individual' ? values.job_title?.trim() || null : null,
|
|
1146
|
+
employer_company_id:
|
|
1147
|
+
normalizedType === 'individual' && allowCompanyRegistration
|
|
1148
|
+
? (values.employer_company_id ?? null)
|
|
1149
|
+
: null,
|
|
1150
|
+
owner_user_id: values.owner_user_id ?? null,
|
|
1151
|
+
source: values.source || null,
|
|
1152
|
+
lifecycle_stage: values.lifecycle_stage || 'new',
|
|
1153
|
+
next_action_at: values.next_action_at
|
|
1154
|
+
? new Date(values.next_action_at).toISOString()
|
|
1155
|
+
: null,
|
|
1156
|
+
trade_name: values.trade_name?.trim() || null,
|
|
1157
|
+
foundation_date: values.foundation_date
|
|
1158
|
+
? values.foundation_date.toISOString()
|
|
1159
|
+
: null,
|
|
1160
|
+
legal_nature: values.legal_nature?.trim() || null,
|
|
1161
|
+
contacts: contacts
|
|
1162
|
+
.filter(
|
|
1163
|
+
(contact) =>
|
|
1164
|
+
contact.value.trim().length > 0 && !!contact.contact_type_id
|
|
1165
|
+
)
|
|
1166
|
+
.map((contact) => ({
|
|
1167
|
+
id: contact.id,
|
|
1168
|
+
value: contact.value.trim(),
|
|
1169
|
+
is_primary: contact.is_primary,
|
|
1170
|
+
contact_type_id: contact.contact_type_id as number,
|
|
1171
|
+
})),
|
|
1172
|
+
addresses: addresses
|
|
1173
|
+
.filter(
|
|
1174
|
+
(address) =>
|
|
1175
|
+
address.line1.trim().length > 0 ||
|
|
1176
|
+
address.city.trim().length > 0 ||
|
|
1177
|
+
address.state.trim().length > 0 ||
|
|
1178
|
+
(address.postal_code || '').trim().length > 0
|
|
1179
|
+
)
|
|
1180
|
+
.map((address) => ({
|
|
1181
|
+
id: address.id,
|
|
1182
|
+
line1: address.line1.trim(),
|
|
1183
|
+
line2: address.line2?.trim() || '',
|
|
1184
|
+
city: address.city.trim(),
|
|
1185
|
+
state: address.state.trim(),
|
|
1186
|
+
country_code: address.country_code || 'BRA',
|
|
1187
|
+
postal_code: address.postal_code?.trim() || '',
|
|
1188
|
+
is_primary: address.is_primary,
|
|
1189
|
+
address_type: address.address_type,
|
|
1190
|
+
})),
|
|
1191
|
+
documents: documents
|
|
1192
|
+
.filter(
|
|
1193
|
+
(document) =>
|
|
1194
|
+
document.value.trim().length > 0 && !!document.document_type_id
|
|
1195
|
+
)
|
|
1196
|
+
.map((document) => ({
|
|
1197
|
+
id: document.id,
|
|
1198
|
+
value: document.value.trim(),
|
|
1199
|
+
document_type_id: document.document_type_id as number,
|
|
1200
|
+
})),
|
|
1201
|
+
});
|
|
1202
|
+
|
|
1203
|
+
const findDuplicates = async (payload: PersonSubmitPayload) => {
|
|
1204
|
+
const primaryEmail = payload.contacts.find((contact) =>
|
|
1205
|
+
['EMAIL'].includes(getContactTypeCode(contact.contact_type_id))
|
|
1206
|
+
);
|
|
1207
|
+
const primaryPhone = payload.contacts.find((contact) =>
|
|
1208
|
+
['PHONE', 'MOBILE', 'WHATSAPP'].includes(
|
|
1209
|
+
getContactTypeCode(contact.contact_type_id)
|
|
1210
|
+
)
|
|
1211
|
+
);
|
|
1212
|
+
const firstDocument = payload.documents[0];
|
|
1213
|
+
|
|
1214
|
+
if (!primaryEmail && !primaryPhone && !firstDocument) {
|
|
1215
|
+
return [] as DuplicateMatch[];
|
|
1216
|
+
}
|
|
1217
|
+
|
|
1218
|
+
const duplicateParams = new URLSearchParams();
|
|
1219
|
+
if (person?.id) duplicateParams.set('person_id', String(person.id));
|
|
1220
|
+
if (primaryEmail?.value) duplicateParams.set('email', primaryEmail.value);
|
|
1221
|
+
if (primaryPhone?.value) duplicateParams.set('phone', primaryPhone.value);
|
|
1222
|
+
if (firstDocument?.value && firstDocument?.document_type_id) {
|
|
1223
|
+
duplicateParams.set('document_value', firstDocument.value);
|
|
1224
|
+
duplicateParams.set(
|
|
1225
|
+
'document_type_id',
|
|
1226
|
+
String(firstDocument.document_type_id)
|
|
1227
|
+
);
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
if (duplicateParams.size === 0) {
|
|
1231
|
+
return [] as DuplicateMatch[];
|
|
1232
|
+
}
|
|
1233
|
+
|
|
1234
|
+
const duplicateResponse = await request<{
|
|
1235
|
+
hasDuplicates: boolean;
|
|
1236
|
+
matches: DuplicateMatch[];
|
|
1237
|
+
}>({
|
|
1238
|
+
url: `/person/duplicates?${duplicateParams.toString()}`,
|
|
1239
|
+
method: 'GET',
|
|
1240
|
+
});
|
|
1241
|
+
|
|
1242
|
+
if (!duplicateResponse.data.hasDuplicates) {
|
|
1243
|
+
return [] as DuplicateMatch[];
|
|
1244
|
+
}
|
|
1245
|
+
|
|
1246
|
+
return duplicateResponse.data.matches || [];
|
|
1247
|
+
};
|
|
1248
|
+
|
|
1249
|
+
const persistPersonPayload = async (
|
|
1250
|
+
payload: PersonSubmitPayload,
|
|
1251
|
+
normalizedType: 'individual' | 'company',
|
|
1252
|
+
options?: { showSuccessToast?: boolean }
|
|
1253
|
+
) => {
|
|
1254
|
+
const showSuccessToast = options?.showSuccessToast !== false;
|
|
1255
|
+
|
|
1256
|
+
if (person) {
|
|
1257
|
+
await request({
|
|
1258
|
+
url: `/person/${person.id}`,
|
|
1259
|
+
method: 'PATCH',
|
|
1260
|
+
data: payload,
|
|
1261
|
+
});
|
|
1262
|
+
|
|
1263
|
+
setPersistedAvatarId(avatarId);
|
|
1264
|
+
|
|
1265
|
+
if (showSuccessToast) {
|
|
1266
|
+
toast.success(t('updateSuccess'));
|
|
1267
|
+
}
|
|
1268
|
+
|
|
1269
|
+
return person.id;
|
|
1270
|
+
}
|
|
1271
|
+
|
|
1272
|
+
const createResponse = await request<CreatePersonPayload>({
|
|
1273
|
+
url: '/person',
|
|
1274
|
+
method: 'POST',
|
|
1275
|
+
data: {
|
|
1276
|
+
name: payload.name,
|
|
1277
|
+
type: normalizedType,
|
|
1278
|
+
status: payload.status,
|
|
1279
|
+
avatar_id: payload.avatar_id,
|
|
1280
|
+
birth_date: payload.birth_date,
|
|
1281
|
+
gender: payload.gender,
|
|
1282
|
+
job_title: payload.job_title,
|
|
1283
|
+
employer_company_id: payload.employer_company_id,
|
|
1284
|
+
trade_name: payload.trade_name,
|
|
1285
|
+
foundation_date: payload.foundation_date,
|
|
1286
|
+
legal_nature: payload.legal_nature,
|
|
1287
|
+
},
|
|
1288
|
+
});
|
|
1289
|
+
|
|
1290
|
+
const personId = Number(
|
|
1291
|
+
createResponse?.data?.id ?? createResponse?.data?.data?.id
|
|
1292
|
+
);
|
|
1293
|
+
|
|
1294
|
+
if (!personId) {
|
|
1295
|
+
throw new Error('Could not resolve created person id');
|
|
1296
|
+
}
|
|
1297
|
+
|
|
1298
|
+
setPersistedAvatarId(avatarId);
|
|
1299
|
+
await request({
|
|
1300
|
+
url: `/person/${personId}`,
|
|
1301
|
+
method: 'PATCH',
|
|
1302
|
+
data: payload,
|
|
1303
|
+
});
|
|
1304
|
+
|
|
1305
|
+
if (showSuccessToast) {
|
|
1306
|
+
toast.success(t('createSuccess'));
|
|
1307
|
+
}
|
|
1308
|
+
|
|
1309
|
+
return personId;
|
|
1310
|
+
};
|
|
1311
|
+
|
|
1312
|
+
const finalizeSuccess = () => {
|
|
1313
|
+
hasSavedChangesRef.current = true;
|
|
1314
|
+
setPendingDuplicateSubmission(null);
|
|
1315
|
+
setDuplicateTargetPersonId(null);
|
|
1316
|
+
setDuplicateDialogOpen(false);
|
|
1317
|
+
handleSheetOpenChange(false);
|
|
1318
|
+
onSuccess();
|
|
1319
|
+
};
|
|
1320
|
+
|
|
1321
|
+
const handleContinueWithDuplicate = async () => {
|
|
1322
|
+
if (!pendingDuplicateSubmission) return;
|
|
1323
|
+
|
|
1324
|
+
try {
|
|
1325
|
+
setIsResolvingDuplicateAction(true);
|
|
1326
|
+
await persistPersonPayload(
|
|
1327
|
+
pendingDuplicateSubmission.payload,
|
|
1328
|
+
pendingDuplicateSubmission.normalizedType
|
|
1329
|
+
);
|
|
1330
|
+
finalizeSuccess();
|
|
1331
|
+
} catch (error: unknown) {
|
|
1332
|
+
const message = error instanceof Error ? error.message : null;
|
|
1333
|
+
toast.error(message || (isEditing ? t('updateError') : t('createError')));
|
|
1334
|
+
} finally {
|
|
1335
|
+
setIsResolvingDuplicateAction(false);
|
|
1336
|
+
}
|
|
1337
|
+
};
|
|
1338
|
+
|
|
1339
|
+
const handleMergeWithDuplicate = async () => {
|
|
1340
|
+
if (!pendingDuplicateSubmission || !duplicateTargetPersonId) {
|
|
1341
|
+
toast.error(t('duplicateSelectTarget'));
|
|
1342
|
+
return;
|
|
1343
|
+
}
|
|
1344
|
+
|
|
1345
|
+
try {
|
|
1346
|
+
setIsResolvingDuplicateAction(true);
|
|
1347
|
+
|
|
1348
|
+
const sourcePersonId = await persistPersonPayload(
|
|
1349
|
+
pendingDuplicateSubmission.payload,
|
|
1350
|
+
pendingDuplicateSubmission.normalizedType,
|
|
1351
|
+
{ showSuccessToast: false }
|
|
1352
|
+
);
|
|
1353
|
+
|
|
1354
|
+
await request({
|
|
1355
|
+
url: '/person/merge',
|
|
1356
|
+
method: 'POST',
|
|
1357
|
+
data: {
|
|
1358
|
+
source_person_id: sourcePersonId,
|
|
1359
|
+
target_person_id: duplicateTargetPersonId,
|
|
1360
|
+
strategy: 'contact_only',
|
|
1361
|
+
},
|
|
1362
|
+
});
|
|
1363
|
+
|
|
1364
|
+
const target = pendingDuplicateSubmission.matches.find(
|
|
1365
|
+
(item) => item.id === duplicateTargetPersonId
|
|
1366
|
+
);
|
|
1367
|
+
|
|
1368
|
+
toast.success(
|
|
1369
|
+
t('duplicateMergeSuccess', {
|
|
1370
|
+
name: target?.name || '#',
|
|
1371
|
+
})
|
|
1372
|
+
);
|
|
1373
|
+
|
|
1374
|
+
finalizeSuccess();
|
|
1375
|
+
} catch (error: unknown) {
|
|
1376
|
+
const message = error instanceof Error ? error.message : null;
|
|
1377
|
+
toast.error(message || t('duplicateMergeError'));
|
|
1378
|
+
} finally {
|
|
1379
|
+
setIsResolvingDuplicateAction(false);
|
|
1380
|
+
}
|
|
1381
|
+
};
|
|
1382
|
+
|
|
1054
1383
|
const handleFormSubmit = async (values: PersonFormValues) => {
|
|
1055
1384
|
try {
|
|
1056
1385
|
setIsSubmitting(true);
|
|
1386
|
+
|
|
1057
1387
|
const normalizedType = allowCompanyRegistration
|
|
1058
1388
|
? values.type
|
|
1059
1389
|
: 'individual';
|
|
1060
1390
|
|
|
1061
|
-
const payload =
|
|
1062
|
-
|
|
1063
|
-
type: normalizedType,
|
|
1064
|
-
status: values.status,
|
|
1065
|
-
avatar_id: avatarId,
|
|
1066
|
-
birth_date: values.birth_date ? values.birth_date.toISOString() : null,
|
|
1067
|
-
gender: values.gender || null,
|
|
1068
|
-
job_title:
|
|
1069
|
-
normalizedType === 'individual'
|
|
1070
|
-
? values.job_title?.trim() || null
|
|
1071
|
-
: null,
|
|
1072
|
-
employer_company_id:
|
|
1073
|
-
normalizedType === 'individual' && allowCompanyRegistration
|
|
1074
|
-
? (values.employer_company_id ?? null)
|
|
1075
|
-
: null,
|
|
1076
|
-
trade_name: values.trade_name?.trim() || null,
|
|
1077
|
-
foundation_date: values.foundation_date
|
|
1078
|
-
? values.foundation_date.toISOString()
|
|
1079
|
-
: null,
|
|
1080
|
-
legal_nature: values.legal_nature?.trim() || null,
|
|
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
|
-
})),
|
|
1092
|
-
addresses: addresses
|
|
1093
|
-
.filter(
|
|
1094
|
-
(address) =>
|
|
1095
|
-
address.line1.trim().length > 0 ||
|
|
1096
|
-
address.city.trim().length > 0 ||
|
|
1097
|
-
address.state.trim().length > 0 ||
|
|
1098
|
-
(address.postal_code || '').trim().length > 0
|
|
1099
|
-
)
|
|
1100
|
-
.map((address) => ({
|
|
1101
|
-
id: address.id,
|
|
1102
|
-
line1: address.line1.trim(),
|
|
1103
|
-
line2: address.line2?.trim() || '',
|
|
1104
|
-
city: address.city.trim(),
|
|
1105
|
-
state: address.state.trim(),
|
|
1106
|
-
country_code: address.country_code || 'BRA',
|
|
1107
|
-
postal_code: address.postal_code?.trim() || '',
|
|
1108
|
-
is_primary: address.is_primary,
|
|
1109
|
-
address_type: address.address_type,
|
|
1110
|
-
})),
|
|
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
|
-
})),
|
|
1121
|
-
};
|
|
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
|
-
}
|
|
1391
|
+
const payload = buildSubmitPayload(values, normalizedType);
|
|
1392
|
+
const matches = await findDuplicates(payload);
|
|
1168
1393
|
|
|
1169
|
-
if (
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
});
|
|
1175
|
-
setPersistedAvatarId(avatarId);
|
|
1176
|
-
toast.success(t('updateSuccess'));
|
|
1177
|
-
} else {
|
|
1178
|
-
const createResponse = await request<CreatePersonPayload>({
|
|
1179
|
-
url: '/person',
|
|
1180
|
-
method: 'POST',
|
|
1181
|
-
data: {
|
|
1182
|
-
name: payload.name,
|
|
1183
|
-
type: normalizedType,
|
|
1184
|
-
status: payload.status,
|
|
1185
|
-
avatar_id: payload.avatar_id,
|
|
1186
|
-
birth_date: payload.birth_date,
|
|
1187
|
-
gender: payload.gender,
|
|
1188
|
-
job_title: payload.job_title,
|
|
1189
|
-
employer_company_id: payload.employer_company_id,
|
|
1190
|
-
trade_name: payload.trade_name,
|
|
1191
|
-
foundation_date: payload.foundation_date,
|
|
1192
|
-
legal_nature: payload.legal_nature,
|
|
1193
|
-
},
|
|
1394
|
+
if (matches.length > 0) {
|
|
1395
|
+
setPendingDuplicateSubmission({
|
|
1396
|
+
payload,
|
|
1397
|
+
normalizedType,
|
|
1398
|
+
matches,
|
|
1194
1399
|
});
|
|
1400
|
+
setDuplicateTargetPersonId(matches[0]?.id || null);
|
|
1401
|
+
setDuplicateDialogOpen(true);
|
|
1195
1402
|
|
|
1196
|
-
|
|
1197
|
-
|
|
1403
|
+
toast.warning(
|
|
1404
|
+
t('duplicateWarning', {
|
|
1405
|
+
names: matches.map((item) => item.name).join(', '),
|
|
1406
|
+
})
|
|
1198
1407
|
);
|
|
1199
|
-
|
|
1200
|
-
if (!personId) {
|
|
1201
|
-
throw new Error('Could not resolve created person id');
|
|
1202
|
-
}
|
|
1203
|
-
|
|
1204
|
-
setPersistedAvatarId(avatarId);
|
|
1205
|
-
await request({
|
|
1206
|
-
url: `/person/${personId}`,
|
|
1207
|
-
method: 'PATCH',
|
|
1208
|
-
data: payload,
|
|
1209
|
-
});
|
|
1210
|
-
toast.success(t('createSuccess'));
|
|
1408
|
+
return;
|
|
1211
1409
|
}
|
|
1212
1410
|
|
|
1213
|
-
|
|
1214
|
-
|
|
1215
|
-
onSuccess();
|
|
1411
|
+
await persistPersonPayload(payload, normalizedType);
|
|
1412
|
+
finalizeSuccess();
|
|
1216
1413
|
} catch (error: unknown) {
|
|
1217
1414
|
const message = error instanceof Error ? error.message : null;
|
|
1218
1415
|
toast.error(message || (isEditing ? t('updateError') : t('createError')));
|
|
@@ -1222,1056 +1419,1196 @@ export function PersonFormSheet({
|
|
|
1222
1419
|
};
|
|
1223
1420
|
|
|
1224
1421
|
return (
|
|
1225
|
-
|
|
1226
|
-
<
|
|
1227
|
-
<
|
|
1228
|
-
<
|
|
1229
|
-
<div
|
|
1230
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
1239
|
-
|
|
1240
|
-
|
|
1241
|
-
|
|
1242
|
-
|
|
1243
|
-
|
|
1244
|
-
<
|
|
1245
|
-
|
|
1246
|
-
|
|
1247
|
-
|
|
1248
|
-
|
|
1249
|
-
|
|
1250
|
-
|
|
1251
|
-
|
|
1252
|
-
|
|
1253
|
-
|
|
1422
|
+
<>
|
|
1423
|
+
<Sheet open={open} onOpenChange={handleSheetOpenChange}>
|
|
1424
|
+
<SheetContent className="flex h-full w-full max-w-full flex-col overflow-hidden p-0 lg:max-w-4xl xl:max-w-5xl">
|
|
1425
|
+
<SheetHeader className="shrink-0 border-b p-4">
|
|
1426
|
+
<div className="flex items-center gap-3">
|
|
1427
|
+
<div
|
|
1428
|
+
className={cn(
|
|
1429
|
+
'flex h-10 w-10 items-center justify-center rounded-full',
|
|
1430
|
+
watchType === 'individual'
|
|
1431
|
+
? 'bg-blue-500/10'
|
|
1432
|
+
: 'bg-amber-500/10'
|
|
1433
|
+
)}
|
|
1434
|
+
>
|
|
1435
|
+
{watchType === 'individual' ? (
|
|
1436
|
+
<User className="h-5 w-5 text-blue-600" />
|
|
1437
|
+
) : (
|
|
1438
|
+
<Building2 className="h-5 w-5 text-amber-600" />
|
|
1439
|
+
)}
|
|
1440
|
+
</div>
|
|
1441
|
+
<div>
|
|
1442
|
+
<SheetTitle>
|
|
1443
|
+
{isEditing ? t('sheetEditTitle') : t('sheetCreateTitle')}
|
|
1444
|
+
</SheetTitle>
|
|
1445
|
+
<SheetDescription>
|
|
1446
|
+
{isEditing
|
|
1447
|
+
? t('sheetEditDescription')
|
|
1448
|
+
: allowCompanyRegistration
|
|
1449
|
+
? t('sheetCreateDescription')
|
|
1450
|
+
: t('sheetCreateDescriptionIndividualOnly')}
|
|
1451
|
+
</SheetDescription>
|
|
1452
|
+
</div>
|
|
1254
1453
|
</div>
|
|
1255
|
-
</
|
|
1256
|
-
</SheetHeader>
|
|
1454
|
+
</SheetHeader>
|
|
1257
1455
|
|
|
1258
|
-
|
|
1259
|
-
|
|
1260
|
-
|
|
1261
|
-
|
|
1262
|
-
|
|
1263
|
-
|
|
1264
|
-
|
|
1265
|
-
|
|
1266
|
-
|
|
1267
|
-
|
|
1268
|
-
|
|
1269
|
-
|
|
1270
|
-
|
|
1271
|
-
|
|
1272
|
-
|
|
1273
|
-
|
|
1274
|
-
|
|
1275
|
-
|
|
1276
|
-
|
|
1277
|
-
|
|
1278
|
-
|
|
1279
|
-
|
|
1280
|
-
|
|
1281
|
-
|
|
1282
|
-
|
|
1283
|
-
<
|
|
1284
|
-
|
|
1285
|
-
|
|
1286
|
-
|
|
1287
|
-
|
|
1288
|
-
|
|
1289
|
-
|
|
1290
|
-
|
|
1291
|
-
|
|
1292
|
-
|
|
1293
|
-
|
|
1456
|
+
<div className="flex-1 overflow-y-auto">
|
|
1457
|
+
<form
|
|
1458
|
+
id="person-form"
|
|
1459
|
+
onSubmit={handleSubmit(handleFormSubmit)}
|
|
1460
|
+
className="space-y-4 px-4 [&_button[role='combobox']]:text-xs [&_button[role='combobox']_span]:truncate [&_input]:text-xs"
|
|
1461
|
+
>
|
|
1462
|
+
<div className="space-y-3">
|
|
1463
|
+
<h3 className="text-sm font-semibold tracking-wider text-muted-foreground uppercase">
|
|
1464
|
+
{t('dialogBasicInformationTitle')}
|
|
1465
|
+
</h3>
|
|
1466
|
+
|
|
1467
|
+
<div className="flex items-start gap-3">
|
|
1468
|
+
<Avatar className="h-16 w-16 border rounded-md">
|
|
1469
|
+
<AvatarImage
|
|
1470
|
+
src={avatarPreviewUrl}
|
|
1471
|
+
alt={watch('name') || t('name')}
|
|
1472
|
+
className="rounded-md object-cover"
|
|
1473
|
+
/>
|
|
1474
|
+
<AvatarFallback className="text-sm font-semibold uppercase">
|
|
1475
|
+
{getPersonInitials(
|
|
1476
|
+
watch('name') || person?.name || 'NA'
|
|
1477
|
+
) || 'NA'}
|
|
1478
|
+
</AvatarFallback>
|
|
1479
|
+
</Avatar>
|
|
1480
|
+
|
|
1481
|
+
<div className="min-w-0 flex-1 space-y-2">
|
|
1482
|
+
<input
|
|
1483
|
+
ref={fileInputRef}
|
|
1484
|
+
type="file"
|
|
1485
|
+
accept="image/*"
|
|
1486
|
+
className="hidden"
|
|
1487
|
+
onChange={(event) => {
|
|
1488
|
+
const file = event.target.files?.[0];
|
|
1489
|
+
if (!file) return;
|
|
1490
|
+
void handleAvatarUpload(file);
|
|
1491
|
+
}}
|
|
1492
|
+
/>
|
|
1294
1493
|
|
|
1295
|
-
|
|
1296
|
-
<Button
|
|
1297
|
-
type="button"
|
|
1298
|
-
variant="outline"
|
|
1299
|
-
size="sm"
|
|
1300
|
-
onClick={handleSelectAvatar}
|
|
1301
|
-
disabled={isUploadingAvatar}
|
|
1302
|
-
>
|
|
1303
|
-
{isUploadingAvatar ? (
|
|
1304
|
-
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1305
|
-
) : (
|
|
1306
|
-
<Upload className="mr-2 h-4 w-4" />
|
|
1307
|
-
)}
|
|
1308
|
-
{avatarId ? t('avatarReplace') : t('avatarUpload')}
|
|
1309
|
-
</Button>
|
|
1310
|
-
{avatarId || isUploadingAvatar ? (
|
|
1494
|
+
<div className="flex flex-wrap gap-2">
|
|
1311
1495
|
<Button
|
|
1312
1496
|
type="button"
|
|
1313
|
-
variant="
|
|
1497
|
+
variant="outline"
|
|
1314
1498
|
size="sm"
|
|
1315
|
-
|
|
1316
|
-
|
|
1317
|
-
disabled={!avatarId || isUploadingAvatar}
|
|
1499
|
+
onClick={handleSelectAvatar}
|
|
1500
|
+
disabled={isUploadingAvatar}
|
|
1318
1501
|
>
|
|
1319
|
-
|
|
1320
|
-
|
|
1502
|
+
{isUploadingAvatar ? (
|
|
1503
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
1504
|
+
) : (
|
|
1505
|
+
<Upload className="mr-2 h-4 w-4" />
|
|
1506
|
+
)}
|
|
1507
|
+
{avatarId ? t('avatarReplace') : t('avatarUpload')}
|
|
1321
1508
|
</Button>
|
|
1322
|
-
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
1333
|
-
|
|
1334
|
-
|
|
1335
|
-
})}
|
|
1336
|
-
</p>
|
|
1509
|
+
{avatarId || isUploadingAvatar ? (
|
|
1510
|
+
<Button
|
|
1511
|
+
type="button"
|
|
1512
|
+
variant="ghost"
|
|
1513
|
+
size="sm"
|
|
1514
|
+
className="text-red-600 hover:text-red-700"
|
|
1515
|
+
onClick={() => void handleRemoveAvatar()}
|
|
1516
|
+
disabled={!avatarId || isUploadingAvatar}
|
|
1517
|
+
>
|
|
1518
|
+
<Trash2 className="mr-2 h-4 w-4" />
|
|
1519
|
+
{t('avatarRemove')}
|
|
1520
|
+
</Button>
|
|
1521
|
+
) : null}
|
|
1337
1522
|
</div>
|
|
1338
|
-
) : null}
|
|
1339
|
-
</div>
|
|
1340
|
-
</div>
|
|
1341
1523
|
|
|
1342
|
-
|
|
1343
|
-
|
|
1344
|
-
|
|
1345
|
-
</Label>
|
|
1346
|
-
<Input
|
|
1347
|
-
placeholder={t('namePlaceholder')}
|
|
1348
|
-
{...register('name')}
|
|
1349
|
-
className={cn('h-9', errors.name && 'border-destructive')}
|
|
1350
|
-
/>
|
|
1351
|
-
{errors.name ? (
|
|
1352
|
-
<p className="text-xs text-destructive">
|
|
1353
|
-
{errors.name.message}
|
|
1354
|
-
</p>
|
|
1355
|
-
) : null}
|
|
1356
|
-
</div>
|
|
1524
|
+
<p className="text-xs text-muted-foreground">
|
|
1525
|
+
{t('avatarGuidelines')}
|
|
1526
|
+
</p>
|
|
1357
1527
|
|
|
1358
|
-
|
|
1359
|
-
|
|
1360
|
-
|
|
1361
|
-
|
|
1362
|
-
|
|
1363
|
-
|
|
1364
|
-
|
|
1365
|
-
|
|
1366
|
-
|
|
1367
|
-
|
|
1368
|
-
|
|
1369
|
-
|
|
1370
|
-
|
|
1371
|
-
onValueChange={(value: 'individual' | 'company') =>
|
|
1372
|
-
setValue('type', value)
|
|
1373
|
-
}
|
|
1374
|
-
>
|
|
1375
|
-
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1376
|
-
<SelectValue />
|
|
1377
|
-
</SelectTrigger>
|
|
1378
|
-
<SelectContent>
|
|
1379
|
-
<SelectItem value="individual">
|
|
1380
|
-
{t('individual')}
|
|
1381
|
-
</SelectItem>
|
|
1382
|
-
{canUseCompanyType ? (
|
|
1383
|
-
<SelectItem value="company">
|
|
1384
|
-
{t('company')}
|
|
1385
|
-
</SelectItem>
|
|
1386
|
-
) : null}
|
|
1387
|
-
</SelectContent>
|
|
1388
|
-
</Select>
|
|
1528
|
+
{isUploadingAvatar ? (
|
|
1529
|
+
<div className="space-y-1">
|
|
1530
|
+
<Progress
|
|
1531
|
+
value={avatarUploadProgress}
|
|
1532
|
+
className="h-2"
|
|
1533
|
+
/>
|
|
1534
|
+
<p className="text-xs text-muted-foreground">
|
|
1535
|
+
{t('avatarUploadingProgress', {
|
|
1536
|
+
progress: avatarUploadProgress,
|
|
1537
|
+
})}
|
|
1538
|
+
</p>
|
|
1539
|
+
</div>
|
|
1540
|
+
) : null}
|
|
1389
1541
|
</div>
|
|
1390
|
-
) : null}
|
|
1391
|
-
<div className="space-y-1.5">
|
|
1392
|
-
<Label className="text-xs font-medium">{t('status')}</Label>
|
|
1393
|
-
<Select
|
|
1394
|
-
value={watch('status')}
|
|
1395
|
-
onValueChange={(value: 'active' | 'inactive') =>
|
|
1396
|
-
setValue('status', value)
|
|
1397
|
-
}
|
|
1398
|
-
>
|
|
1399
|
-
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1400
|
-
<SelectValue />
|
|
1401
|
-
</SelectTrigger>
|
|
1402
|
-
<SelectContent>
|
|
1403
|
-
<SelectItem value="active">{t('active')}</SelectItem>
|
|
1404
|
-
<SelectItem value="inactive">{t('inactive')}</SelectItem>
|
|
1405
|
-
</SelectContent>
|
|
1406
|
-
</Select>
|
|
1407
1542
|
</div>
|
|
1408
1543
|
|
|
1409
|
-
|
|
1410
|
-
|
|
1411
|
-
<
|
|
1412
|
-
|
|
1413
|
-
|
|
1414
|
-
|
|
1415
|
-
|
|
1416
|
-
|
|
1417
|
-
|
|
1418
|
-
|
|
1419
|
-
|
|
1420
|
-
|
|
1421
|
-
|
|
1422
|
-
|
|
1423
|
-
|
|
1424
|
-
</div>
|
|
1544
|
+
<div className="space-y-1.5">
|
|
1545
|
+
<Label className="text-xs font-medium">
|
|
1546
|
+
{t('name')} <span className="text-destructive">*</span>
|
|
1547
|
+
</Label>
|
|
1548
|
+
<Input
|
|
1549
|
+
placeholder={t('namePlaceholder')}
|
|
1550
|
+
{...register('name')}
|
|
1551
|
+
className={cn('h-9', errors.name && 'border-destructive')}
|
|
1552
|
+
/>
|
|
1553
|
+
{errors.name ? (
|
|
1554
|
+
<p className="text-xs text-destructive">
|
|
1555
|
+
{errors.name.message}
|
|
1556
|
+
</p>
|
|
1557
|
+
) : null}
|
|
1558
|
+
</div>
|
|
1425
1559
|
|
|
1560
|
+
<div
|
|
1561
|
+
className={cn(
|
|
1562
|
+
'grid gap-3',
|
|
1563
|
+
watchType === 'individual'
|
|
1564
|
+
? 'grid-cols-1 sm:grid-cols-2 xl:grid-cols-4'
|
|
1565
|
+
: 'grid-cols-1 sm:grid-cols-2'
|
|
1566
|
+
)}
|
|
1567
|
+
>
|
|
1568
|
+
{allowCompanyRegistration ? (
|
|
1426
1569
|
<div className="space-y-1.5">
|
|
1427
|
-
<Label className="text-xs font-medium">
|
|
1428
|
-
{t('gender')}
|
|
1429
|
-
</Label>
|
|
1570
|
+
<Label className="text-xs font-medium">{t('type')}</Label>
|
|
1430
1571
|
<Select
|
|
1431
|
-
value={watch('
|
|
1432
|
-
onValueChange={(value:
|
|
1433
|
-
setValue('
|
|
1572
|
+
value={watch('type')}
|
|
1573
|
+
onValueChange={(value: 'individual' | 'company') =>
|
|
1574
|
+
setValue('type', value)
|
|
1434
1575
|
}
|
|
1435
1576
|
>
|
|
1436
1577
|
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1437
|
-
<SelectValue
|
|
1578
|
+
<SelectValue />
|
|
1438
1579
|
</SelectTrigger>
|
|
1439
1580
|
<SelectContent>
|
|
1440
|
-
<SelectItem value="
|
|
1441
|
-
{t('
|
|
1442
|
-
</SelectItem>
|
|
1443
|
-
<SelectItem value="female">
|
|
1444
|
-
{t('genderFemale')}
|
|
1445
|
-
</SelectItem>
|
|
1446
|
-
<SelectItem value="other">
|
|
1447
|
-
{t('genderOther')}
|
|
1581
|
+
<SelectItem value="individual">
|
|
1582
|
+
{t('individual')}
|
|
1448
1583
|
</SelectItem>
|
|
1584
|
+
{canUseCompanyType ? (
|
|
1585
|
+
<SelectItem value="company">
|
|
1586
|
+
{t('company')}
|
|
1587
|
+
</SelectItem>
|
|
1588
|
+
) : null}
|
|
1449
1589
|
</SelectContent>
|
|
1450
1590
|
</Select>
|
|
1451
1591
|
</div>
|
|
1452
|
-
|
|
1453
|
-
|
|
1454
|
-
|
|
1455
|
-
|
|
1456
|
-
|
|
1457
|
-
|
|
1458
|
-
|
|
1459
|
-
|
|
1460
|
-
|
|
1461
|
-
|
|
1462
|
-
|
|
1463
|
-
|
|
1464
|
-
|
|
1465
|
-
|
|
1466
|
-
|
|
1467
|
-
|
|
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}
|
|
1592
|
+
) : null}
|
|
1593
|
+
<div className="space-y-1.5">
|
|
1594
|
+
<Label className="text-xs font-medium">{t('status')}</Label>
|
|
1595
|
+
<Select
|
|
1596
|
+
value={watch('status')}
|
|
1597
|
+
onValueChange={(value: 'active' | 'inactive') =>
|
|
1598
|
+
setValue('status', value)
|
|
1599
|
+
}
|
|
1600
|
+
>
|
|
1601
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1602
|
+
<SelectValue />
|
|
1603
|
+
</SelectTrigger>
|
|
1604
|
+
<SelectContent>
|
|
1605
|
+
<SelectItem value="active">{t('active')}</SelectItem>
|
|
1606
|
+
<SelectItem value="inactive">
|
|
1607
|
+
{t('inactive')}
|
|
1480
1608
|
</SelectItem>
|
|
1481
|
-
|
|
1482
|
-
</
|
|
1483
|
-
</
|
|
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
|
-
|
|
1572
|
-
{watchType === 'individual' ? (
|
|
1573
|
-
<>
|
|
1574
|
-
<div className="grid grid-cols-2 gap-3">
|
|
1575
|
-
<div className="space-y-1.5">
|
|
1576
|
-
<Label className="text-xs font-medium">
|
|
1577
|
-
{t('jobTitle')}
|
|
1578
|
-
</Label>
|
|
1579
|
-
<Input
|
|
1580
|
-
placeholder={t('jobTitlePlaceholder')}
|
|
1581
|
-
{...register('job_title')}
|
|
1582
|
-
className="h-9"
|
|
1583
|
-
/>
|
|
1584
|
-
</div>
|
|
1609
|
+
</SelectContent>
|
|
1610
|
+
</Select>
|
|
1611
|
+
</div>
|
|
1585
1612
|
|
|
1586
|
-
|
|
1613
|
+
{watchType === 'individual' ? (
|
|
1614
|
+
<>
|
|
1587
1615
|
<div className="space-y-1.5">
|
|
1588
|
-
<
|
|
1589
|
-
|
|
1590
|
-
|
|
1591
|
-
|
|
1592
|
-
|
|
1593
|
-
|
|
1594
|
-
|
|
1595
|
-
createType="company"
|
|
1596
|
-
lockCreateType
|
|
1597
|
-
valueType="number"
|
|
1598
|
-
initialSelectedLabel={
|
|
1599
|
-
person?.employer_company?.name || ''
|
|
1616
|
+
<Label className="text-xs font-medium">
|
|
1617
|
+
{t('birthDate')}
|
|
1618
|
+
</Label>
|
|
1619
|
+
<DatePickerWithYearMonth
|
|
1620
|
+
date={watch('birth_date') || undefined}
|
|
1621
|
+
onSelect={(date) =>
|
|
1622
|
+
setValue('birth_date', date || null)
|
|
1600
1623
|
}
|
|
1624
|
+
maxDate={new Date()}
|
|
1625
|
+
placeholder={t('selectDate')}
|
|
1626
|
+
localeCode={currentLocaleCode}
|
|
1601
1627
|
/>
|
|
1602
1628
|
</div>
|
|
1603
|
-
) : null}
|
|
1604
|
-
</div>
|
|
1605
|
-
</>
|
|
1606
|
-
) : (
|
|
1607
|
-
<>
|
|
1608
|
-
<div className="space-y-1.5">
|
|
1609
|
-
<Label className="text-xs font-medium">
|
|
1610
|
-
{t('tradeName')}
|
|
1611
|
-
</Label>
|
|
1612
|
-
<Input
|
|
1613
|
-
placeholder={t('tradeNamePlaceholder')}
|
|
1614
|
-
{...register('trade_name')}
|
|
1615
|
-
className="h-9"
|
|
1616
|
-
/>
|
|
1617
|
-
</div>
|
|
1618
1629
|
|
|
1619
|
-
|
|
1620
|
-
|
|
1621
|
-
|
|
1622
|
-
|
|
1623
|
-
|
|
1624
|
-
|
|
1625
|
-
|
|
1626
|
-
|
|
1627
|
-
|
|
1628
|
-
|
|
1629
|
-
|
|
1630
|
-
|
|
1631
|
-
|
|
1632
|
-
|
|
1633
|
-
|
|
1630
|
+
<div className="space-y-1.5">
|
|
1631
|
+
<Label className="text-xs font-medium">
|
|
1632
|
+
{t('gender')}
|
|
1633
|
+
</Label>
|
|
1634
|
+
<Select
|
|
1635
|
+
value={watch('gender') || ''}
|
|
1636
|
+
onValueChange={(value: PersonGender) =>
|
|
1637
|
+
setValue('gender', value)
|
|
1638
|
+
}
|
|
1639
|
+
>
|
|
1640
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1641
|
+
<SelectValue placeholder={t('selectGender')} />
|
|
1642
|
+
</SelectTrigger>
|
|
1643
|
+
<SelectContent>
|
|
1644
|
+
<SelectItem value="male">
|
|
1645
|
+
{t('genderMale')}
|
|
1646
|
+
</SelectItem>
|
|
1647
|
+
<SelectItem value="female">
|
|
1648
|
+
{t('genderFemale')}
|
|
1649
|
+
</SelectItem>
|
|
1650
|
+
<SelectItem value="other">
|
|
1651
|
+
{t('genderOther')}
|
|
1652
|
+
</SelectItem>
|
|
1653
|
+
</SelectContent>
|
|
1654
|
+
</Select>
|
|
1655
|
+
</div>
|
|
1656
|
+
</>
|
|
1657
|
+
) : null}
|
|
1658
|
+
</div>
|
|
1634
1659
|
|
|
1635
|
-
|
|
1636
|
-
|
|
1637
|
-
|
|
1638
|
-
|
|
1639
|
-
|
|
1640
|
-
|
|
1641
|
-
|
|
1642
|
-
|
|
1643
|
-
|
|
1644
|
-
|
|
1660
|
+
<div className="grid gap-3 sm:grid-cols-2 xl:grid-cols-4">
|
|
1661
|
+
<div className="space-y-1.5">
|
|
1662
|
+
<Label className="text-xs font-medium">{t('owner')}</Label>
|
|
1663
|
+
<Select
|
|
1664
|
+
value={
|
|
1665
|
+
watch('owner_user_id')
|
|
1666
|
+
? String(watch('owner_user_id'))
|
|
1667
|
+
: 'none'
|
|
1668
|
+
}
|
|
1669
|
+
onValueChange={(value) =>
|
|
1670
|
+
setValue(
|
|
1671
|
+
'owner_user_id',
|
|
1672
|
+
value === 'none' ? null : Number(value)
|
|
1673
|
+
)
|
|
1674
|
+
}
|
|
1675
|
+
>
|
|
1676
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1677
|
+
<SelectValue placeholder={t('unassigned')} />
|
|
1678
|
+
</SelectTrigger>
|
|
1679
|
+
<SelectContent>
|
|
1680
|
+
<SelectItem value="none">{t('unassigned')}</SelectItem>
|
|
1681
|
+
{ownerOptions.map((owner) => (
|
|
1682
|
+
<SelectItem key={owner.id} value={String(owner.id)}>
|
|
1683
|
+
{owner.name}
|
|
1684
|
+
</SelectItem>
|
|
1685
|
+
))}
|
|
1686
|
+
</SelectContent>
|
|
1687
|
+
</Select>
|
|
1645
1688
|
</div>
|
|
1646
|
-
</>
|
|
1647
|
-
)}
|
|
1648
|
-
</div>
|
|
1649
1689
|
|
|
1650
|
-
|
|
1651
|
-
|
|
1652
|
-
|
|
1653
|
-
|
|
1654
|
-
|
|
1655
|
-
|
|
1656
|
-
|
|
1657
|
-
|
|
1658
|
-
|
|
1659
|
-
|
|
1660
|
-
|
|
1661
|
-
<
|
|
1662
|
-
|
|
1663
|
-
|
|
1664
|
-
>
|
|
1665
|
-
{
|
|
1666
|
-
|
|
1667
|
-
|
|
1690
|
+
<div className="space-y-1.5">
|
|
1691
|
+
<Label className="text-xs font-medium">{t('source')}</Label>
|
|
1692
|
+
<Select
|
|
1693
|
+
value={watch('source') || 'none'}
|
|
1694
|
+
onValueChange={(value) =>
|
|
1695
|
+
setValue(
|
|
1696
|
+
'source',
|
|
1697
|
+
value === 'none' ? null : (value as PersonSource)
|
|
1698
|
+
)
|
|
1699
|
+
}
|
|
1700
|
+
>
|
|
1701
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1702
|
+
<SelectValue placeholder={t('source')} />
|
|
1703
|
+
</SelectTrigger>
|
|
1704
|
+
<SelectContent>
|
|
1705
|
+
<SelectItem value="none">{t('all')}</SelectItem>
|
|
1706
|
+
<SelectItem value="referral">
|
|
1707
|
+
{t('sourceReferral')}
|
|
1708
|
+
</SelectItem>
|
|
1709
|
+
<SelectItem value="website">
|
|
1710
|
+
{t('sourceWebsite')}
|
|
1711
|
+
</SelectItem>
|
|
1712
|
+
<SelectItem value="social">
|
|
1713
|
+
{t('sourceSocial')}
|
|
1714
|
+
</SelectItem>
|
|
1715
|
+
<SelectItem value="inbound">
|
|
1716
|
+
{t('sourceInbound')}
|
|
1717
|
+
</SelectItem>
|
|
1718
|
+
<SelectItem value="outbound">
|
|
1719
|
+
{t('sourceOutbound')}
|
|
1720
|
+
</SelectItem>
|
|
1721
|
+
<SelectItem value="other">
|
|
1722
|
+
{t('sourceOther')}
|
|
1723
|
+
</SelectItem>
|
|
1724
|
+
</SelectContent>
|
|
1725
|
+
</Select>
|
|
1668
1726
|
</div>
|
|
1669
1727
|
|
|
1670
|
-
<div className="
|
|
1671
|
-
<
|
|
1672
|
-
|
|
1673
|
-
|
|
1674
|
-
|
|
1675
|
-
|
|
1676
|
-
|
|
1677
|
-
|
|
1678
|
-
|
|
1679
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
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
|
-
}}
|
|
1728
|
+
<div className="space-y-1.5">
|
|
1729
|
+
<Label className="text-xs font-medium">
|
|
1730
|
+
{t('lifecycleStage')}
|
|
1731
|
+
</Label>
|
|
1732
|
+
<Select
|
|
1733
|
+
value={watch('lifecycle_stage') || 'new'}
|
|
1734
|
+
onValueChange={(value) =>
|
|
1735
|
+
setValue(
|
|
1736
|
+
'lifecycle_stage',
|
|
1737
|
+
value as PersonLifecycleStage
|
|
1738
|
+
)
|
|
1739
|
+
}
|
|
1706
1740
|
>
|
|
1707
|
-
<
|
|
1708
|
-
|
|
1709
|
-
|
|
1710
|
-
|
|
1711
|
-
|
|
1712
|
-
|
|
1713
|
-
|
|
1714
|
-
|
|
1741
|
+
<SelectTrigger className="h-9 w-full min-w-0">
|
|
1742
|
+
<SelectValue />
|
|
1743
|
+
</SelectTrigger>
|
|
1744
|
+
<SelectContent>
|
|
1745
|
+
<SelectItem value="new">{t('lifecycleNew')}</SelectItem>
|
|
1746
|
+
<SelectItem value="contacted">
|
|
1747
|
+
{t('lifecycleContacted')}
|
|
1748
|
+
</SelectItem>
|
|
1749
|
+
<SelectItem value="qualified">
|
|
1750
|
+
{t('lifecycleQualified')}
|
|
1751
|
+
</SelectItem>
|
|
1752
|
+
<SelectItem value="proposal">
|
|
1753
|
+
{t('lifecycleProposal')}
|
|
1754
|
+
</SelectItem>
|
|
1755
|
+
<SelectItem value="negotiation">
|
|
1756
|
+
{t('lifecycleNegotiation')}
|
|
1757
|
+
</SelectItem>
|
|
1758
|
+
<SelectItem value="customer">
|
|
1759
|
+
{t('lifecycleCustomer')}
|
|
1760
|
+
</SelectItem>
|
|
1761
|
+
<SelectItem value="lost">
|
|
1762
|
+
{t('lifecycleLost')}
|
|
1763
|
+
</SelectItem>
|
|
1764
|
+
</SelectContent>
|
|
1765
|
+
</Select>
|
|
1715
1766
|
</div>
|
|
1716
|
-
</div>
|
|
1717
|
-
</CollapsibleTrigger>
|
|
1718
1767
|
|
|
1719
|
-
|
|
1720
|
-
|
|
1721
|
-
|
|
1722
|
-
|
|
1768
|
+
<div className="space-y-1.5">
|
|
1769
|
+
<Label className="text-xs font-medium">
|
|
1770
|
+
{t('nextActionAt')}
|
|
1771
|
+
</Label>
|
|
1772
|
+
<Input
|
|
1773
|
+
type="datetime-local"
|
|
1774
|
+
value={watch('next_action_at') || ''}
|
|
1775
|
+
onChange={(event) =>
|
|
1776
|
+
setValue('next_action_at', event.target.value)
|
|
1777
|
+
}
|
|
1778
|
+
className="h-9"
|
|
1779
|
+
/>
|
|
1723
1780
|
</div>
|
|
1724
|
-
|
|
1725
|
-
contacts.map((contact) => (
|
|
1726
|
-
<div
|
|
1727
|
-
key={contact.clientId}
|
|
1728
|
-
className={cn(
|
|
1729
|
-
'space-y-2 rounded-lg border p-2',
|
|
1730
|
-
contact.is_primary && 'border-blue-500/50 bg-blue-500/5'
|
|
1731
|
-
)}
|
|
1732
|
-
>
|
|
1733
|
-
<div className="flex items-center gap-2">
|
|
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>
|
|
1754
|
-
<SelectContent>
|
|
1755
|
-
{contactTypes.map((contactType) => (
|
|
1756
|
-
<SelectItem
|
|
1757
|
-
key={contactType.contact_type_id}
|
|
1758
|
-
value={String(contactType.contact_type_id)}
|
|
1759
|
-
>
|
|
1760
|
-
{contactType.name}
|
|
1761
|
-
</SelectItem>
|
|
1762
|
-
))}
|
|
1763
|
-
</SelectContent>
|
|
1764
|
-
</Select>
|
|
1781
|
+
</div>
|
|
1765
1782
|
|
|
1783
|
+
{watchType === 'individual' ? (
|
|
1784
|
+
<>
|
|
1785
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1786
|
+
<div className="space-y-1.5">
|
|
1787
|
+
<Label className="text-xs font-medium">
|
|
1788
|
+
{t('jobTitle')}
|
|
1789
|
+
</Label>
|
|
1766
1790
|
<Input
|
|
1767
|
-
|
|
1768
|
-
|
|
1769
|
-
|
|
1770
|
-
|
|
1771
|
-
|
|
1772
|
-
|
|
1773
|
-
|
|
1774
|
-
|
|
1775
|
-
|
|
1776
|
-
|
|
1777
|
-
|
|
1778
|
-
|
|
1779
|
-
|
|
1791
|
+
placeholder={t('jobTitlePlaceholder')}
|
|
1792
|
+
{...register('job_title')}
|
|
1793
|
+
className="h-9"
|
|
1794
|
+
/>
|
|
1795
|
+
</div>
|
|
1796
|
+
|
|
1797
|
+
{allowCompanyRegistration ? (
|
|
1798
|
+
<div className="space-y-1.5">
|
|
1799
|
+
<PersonFieldWithCreate<PersonFormValues>
|
|
1800
|
+
form={form}
|
|
1801
|
+
name="employer_company_id"
|
|
1802
|
+
label={t('employerCompany')}
|
|
1803
|
+
entityLabel={t('company')}
|
|
1804
|
+
selectPlaceholder={t('employerCompanyPlaceholder')}
|
|
1805
|
+
personTypeFilter="company"
|
|
1806
|
+
createType="company"
|
|
1807
|
+
lockCreateType
|
|
1808
|
+
valueType="number"
|
|
1809
|
+
initialSelectedLabel={
|
|
1810
|
+
person?.employer_company?.name || ''
|
|
1780
1811
|
}
|
|
1781
|
-
|
|
1782
|
-
|
|
1783
|
-
|
|
1784
|
-
|
|
1785
|
-
|
|
1786
|
-
|
|
1787
|
-
|
|
1788
|
-
|
|
1789
|
-
|
|
1790
|
-
|
|
1812
|
+
/>
|
|
1813
|
+
</div>
|
|
1814
|
+
) : null}
|
|
1815
|
+
</div>
|
|
1816
|
+
</>
|
|
1817
|
+
) : (
|
|
1818
|
+
<>
|
|
1819
|
+
<div className="space-y-1.5">
|
|
1820
|
+
<Label className="text-xs font-medium">
|
|
1821
|
+
{t('tradeName')}
|
|
1822
|
+
</Label>
|
|
1823
|
+
<Input
|
|
1824
|
+
placeholder={t('tradeNamePlaceholder')}
|
|
1825
|
+
{...register('trade_name')}
|
|
1826
|
+
className="h-9"
|
|
1827
|
+
/>
|
|
1828
|
+
</div>
|
|
1829
|
+
|
|
1830
|
+
<div className="grid grid-cols-2 gap-3">
|
|
1831
|
+
<div className="space-y-1.5">
|
|
1832
|
+
<Label className="text-xs font-medium">
|
|
1833
|
+
{t('foundationDate')}
|
|
1834
|
+
</Label>
|
|
1835
|
+
<DatePickerWithYearMonth
|
|
1836
|
+
date={watch('foundation_date') || undefined}
|
|
1837
|
+
onSelect={(date) =>
|
|
1838
|
+
setValue('foundation_date', date || null)
|
|
1791
1839
|
}
|
|
1792
|
-
|
|
1840
|
+
maxDate={new Date()}
|
|
1841
|
+
placeholder={t('selectDate')}
|
|
1842
|
+
localeCode={currentLocaleCode}
|
|
1793
1843
|
/>
|
|
1844
|
+
</div>
|
|
1794
1845
|
|
|
1795
|
-
|
|
1796
|
-
|
|
1797
|
-
|
|
1798
|
-
|
|
1799
|
-
|
|
1800
|
-
|
|
1801
|
-
|
|
1802
|
-
|
|
1803
|
-
|
|
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>
|
|
1846
|
+
<div className="space-y-1.5">
|
|
1847
|
+
<Label className="text-xs font-medium">
|
|
1848
|
+
{t('legalNature')}
|
|
1849
|
+
</Label>
|
|
1850
|
+
<Input
|
|
1851
|
+
placeholder={t('legalNaturePlaceholder')}
|
|
1852
|
+
{...register('legal_nature')}
|
|
1853
|
+
className="h-9"
|
|
1854
|
+
/>
|
|
1836
1855
|
</div>
|
|
1837
1856
|
</div>
|
|
1838
|
-
|
|
1857
|
+
</>
|
|
1839
1858
|
)}
|
|
1840
|
-
</
|
|
1841
|
-
</Collapsible>
|
|
1842
|
-
|
|
1843
|
-
<Separator />
|
|
1844
|
-
|
|
1845
|
-
<Collapsible open={addressesOpen} onOpenChange={setAddressesOpen}>
|
|
1846
|
-
<CollapsibleTrigger asChild>
|
|
1847
|
-
<div className="group flex cursor-pointer items-center justify-between">
|
|
1848
|
-
<div className="flex items-center gap-2">
|
|
1849
|
-
<MapPin className="h-4 w-4 text-green-500" />
|
|
1850
|
-
<h3 className="text-sm font-semibold">
|
|
1851
|
-
{t('tabAddresses')}
|
|
1852
|
-
</h3>
|
|
1853
|
-
{addresses.length > 0 ? (
|
|
1854
|
-
<Badge
|
|
1855
|
-
variant="secondary"
|
|
1856
|
-
className="bg-green-500/10 text-green-600"
|
|
1857
|
-
>
|
|
1858
|
-
{addresses.length}
|
|
1859
|
-
</Badge>
|
|
1860
|
-
) : null}
|
|
1861
|
-
</div>
|
|
1862
|
-
|
|
1863
|
-
<div className="flex items-center gap-2">
|
|
1864
|
-
<Button
|
|
1865
|
-
type="button"
|
|
1866
|
-
variant="ghost"
|
|
1867
|
-
size="sm"
|
|
1868
|
-
className="h-7 px-2 text-xs"
|
|
1869
|
-
onClick={(event) => {
|
|
1870
|
-
event.stopPropagation();
|
|
1871
|
-
addAddress();
|
|
1872
|
-
}}
|
|
1873
|
-
>
|
|
1874
|
-
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
1875
|
-
{t('addAddress')}
|
|
1876
|
-
</Button>
|
|
1877
|
-
{addressesOpen ? (
|
|
1878
|
-
<ChevronUp className="h-4 w-4" />
|
|
1879
|
-
) : (
|
|
1880
|
-
<ChevronDown className="h-4 w-4" />
|
|
1881
|
-
)}
|
|
1882
|
-
</div>
|
|
1883
|
-
</div>
|
|
1884
|
-
</CollapsibleTrigger>
|
|
1859
|
+
</div>
|
|
1885
1860
|
|
|
1886
|
-
<
|
|
1887
|
-
|
|
1888
|
-
|
|
1889
|
-
|
|
1890
|
-
|
|
1891
|
-
|
|
1892
|
-
|
|
1893
|
-
|
|
1894
|
-
|
|
1895
|
-
|
|
1896
|
-
|
|
1897
|
-
|
|
1898
|
-
|
|
1899
|
-
|
|
1900
|
-
>
|
|
1901
|
-
<div className="flex items-center gap-2">
|
|
1902
|
-
<Select
|
|
1903
|
-
value={address.address_type}
|
|
1904
|
-
onValueChange={(value) =>
|
|
1905
|
-
updateAddress(address.clientId, {
|
|
1906
|
-
address_type:
|
|
1907
|
-
value as PersonAddress['address_type'],
|
|
1908
|
-
})
|
|
1909
|
-
}
|
|
1861
|
+
<Separator />
|
|
1862
|
+
|
|
1863
|
+
<Collapsible open={contactsOpen} onOpenChange={setContactsOpen}>
|
|
1864
|
+
<CollapsibleTrigger asChild>
|
|
1865
|
+
<div className="group flex cursor-pointer items-center justify-between">
|
|
1866
|
+
<div className="flex items-center gap-2">
|
|
1867
|
+
<Mail className="h-4 w-4 text-blue-500" />
|
|
1868
|
+
<h3 className="text-sm font-semibold">
|
|
1869
|
+
{t('tabContacts')}
|
|
1870
|
+
</h3>
|
|
1871
|
+
{contacts.length > 0 ? (
|
|
1872
|
+
<Badge
|
|
1873
|
+
variant="secondary"
|
|
1874
|
+
className="bg-blue-500/10 text-blue-600"
|
|
1910
1875
|
>
|
|
1911
|
-
|
|
1912
|
-
|
|
1913
|
-
|
|
1914
|
-
|
|
1915
|
-
{ADDRESS_TYPE_OPTIONS.map((option) => (
|
|
1916
|
-
<SelectItem
|
|
1917
|
-
key={option.value}
|
|
1918
|
-
value={option.value}
|
|
1919
|
-
>
|
|
1920
|
-
{t(option.labelKey)}
|
|
1921
|
-
</SelectItem>
|
|
1922
|
-
))}
|
|
1923
|
-
</SelectContent>
|
|
1924
|
-
</Select>
|
|
1876
|
+
{contacts.length}
|
|
1877
|
+
</Badge>
|
|
1878
|
+
) : null}
|
|
1879
|
+
</div>
|
|
1925
1880
|
|
|
1926
|
-
|
|
1927
|
-
|
|
1928
|
-
|
|
1929
|
-
|
|
1930
|
-
|
|
1931
|
-
|
|
1932
|
-
|
|
1933
|
-
|
|
1934
|
-
|
|
1881
|
+
<div className="flex items-center gap-1">
|
|
1882
|
+
<Button
|
|
1883
|
+
type="button"
|
|
1884
|
+
variant="ghost"
|
|
1885
|
+
size="sm"
|
|
1886
|
+
className="h-7 px-2 text-xs"
|
|
1887
|
+
onClick={(event) => {
|
|
1888
|
+
event.stopPropagation();
|
|
1889
|
+
addContact('email');
|
|
1890
|
+
}}
|
|
1891
|
+
>
|
|
1892
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
1893
|
+
{t('addEmail')}
|
|
1894
|
+
</Button>
|
|
1895
|
+
<Button
|
|
1896
|
+
type="button"
|
|
1897
|
+
variant="ghost"
|
|
1898
|
+
size="sm"
|
|
1899
|
+
className="h-7 px-2 text-xs"
|
|
1900
|
+
onClick={(event) => {
|
|
1901
|
+
event.stopPropagation();
|
|
1902
|
+
addContact('phone');
|
|
1903
|
+
}}
|
|
1904
|
+
>
|
|
1905
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
1906
|
+
{t('addPhone')}
|
|
1907
|
+
</Button>
|
|
1908
|
+
<Button
|
|
1909
|
+
type="button"
|
|
1910
|
+
variant="ghost"
|
|
1911
|
+
size="sm"
|
|
1912
|
+
className="h-7 px-2 text-xs"
|
|
1913
|
+
onClick={(event) => {
|
|
1914
|
+
event.stopPropagation();
|
|
1915
|
+
addContact('blank');
|
|
1916
|
+
}}
|
|
1917
|
+
>
|
|
1918
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
1919
|
+
{t('addContact')}
|
|
1920
|
+
</Button>
|
|
1921
|
+
{contactsOpen ? (
|
|
1922
|
+
<ChevronUp className="h-4 w-4" />
|
|
1923
|
+
) : (
|
|
1924
|
+
<ChevronDown className="h-4 w-4" />
|
|
1925
|
+
)}
|
|
1926
|
+
</div>
|
|
1927
|
+
</div>
|
|
1928
|
+
</CollapsibleTrigger>
|
|
1935
1929
|
|
|
1936
|
-
|
|
1937
|
-
|
|
1938
|
-
|
|
1939
|
-
|
|
1940
|
-
|
|
1941
|
-
|
|
1942
|
-
|
|
1943
|
-
|
|
1944
|
-
|
|
1945
|
-
|
|
1946
|
-
|
|
1947
|
-
|
|
1930
|
+
<CollapsibleContent className="mt-2 space-y-2">
|
|
1931
|
+
{contacts.length === 0 ? (
|
|
1932
|
+
<div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
|
|
1933
|
+
{t('noContacts')}
|
|
1934
|
+
</div>
|
|
1935
|
+
) : (
|
|
1936
|
+
contacts.map((contact) => (
|
|
1937
|
+
<div
|
|
1938
|
+
key={contact.clientId}
|
|
1939
|
+
className={cn(
|
|
1940
|
+
'space-y-2 rounded-lg border p-2',
|
|
1941
|
+
contact.is_primary &&
|
|
1942
|
+
'border-blue-500/50 bg-blue-500/5'
|
|
1943
|
+
)}
|
|
1944
|
+
>
|
|
1945
|
+
<div className="flex items-center gap-2">
|
|
1946
|
+
<Select
|
|
1947
|
+
value={
|
|
1948
|
+
contact.contact_type_id
|
|
1949
|
+
? String(contact.contact_type_id)
|
|
1950
|
+
: undefined
|
|
1951
|
+
}
|
|
1952
|
+
onValueChange={(value) => {
|
|
1953
|
+
const nextTypeId = Number(value);
|
|
1954
|
+
updateContact(contact.clientId, {
|
|
1955
|
+
contact_type_id: nextTypeId,
|
|
1956
|
+
value: maskContactValueByType(
|
|
1957
|
+
contact.value || '',
|
|
1958
|
+
nextTypeId
|
|
1959
|
+
),
|
|
1960
|
+
});
|
|
1961
|
+
}}
|
|
1962
|
+
>
|
|
1963
|
+
<SelectTrigger className="h-8 w-36 text-xs">
|
|
1964
|
+
<SelectValue
|
|
1965
|
+
placeholder={t('selectContactType')}
|
|
1966
|
+
/>
|
|
1967
|
+
</SelectTrigger>
|
|
1968
|
+
<SelectContent>
|
|
1969
|
+
{contactTypes.map((contactType) => (
|
|
1970
|
+
<SelectItem
|
|
1971
|
+
key={contactType.contact_type_id}
|
|
1972
|
+
value={String(contactType.contact_type_id)}
|
|
1973
|
+
>
|
|
1974
|
+
{contactType.name}
|
|
1975
|
+
</SelectItem>
|
|
1976
|
+
))}
|
|
1977
|
+
</SelectContent>
|
|
1978
|
+
</Select>
|
|
1979
|
+
|
|
1980
|
+
<Input
|
|
1981
|
+
ref={(element) => {
|
|
1982
|
+
contactValueRefs.current[contact.clientId] =
|
|
1983
|
+
element;
|
|
1984
|
+
}}
|
|
1985
|
+
placeholder={(() => {
|
|
1986
|
+
const contactTypeCode = getContactTypeCode(
|
|
1987
|
+
contact.contact_type_id
|
|
1988
|
+
);
|
|
1989
|
+
if (contactTypeCode === 'EMAIL') {
|
|
1990
|
+
return 'email@exemplo.com';
|
|
1948
1991
|
}
|
|
1949
|
-
|
|
1950
|
-
|
|
1951
|
-
|
|
1992
|
+
if (contactTypeCode.length === 0) {
|
|
1993
|
+
return t('contactValue');
|
|
1994
|
+
}
|
|
1995
|
+
return '(00) 00000-0000';
|
|
1996
|
+
})()}
|
|
1997
|
+
value={contact.value}
|
|
1998
|
+
onChange={(event) =>
|
|
1999
|
+
updateContact(contact.clientId, {
|
|
2000
|
+
value: maskContactValueByType(
|
|
2001
|
+
event.target.value,
|
|
2002
|
+
contact.contact_type_id
|
|
2003
|
+
),
|
|
2004
|
+
})
|
|
2005
|
+
}
|
|
2006
|
+
className="h-8 flex-1 text-xs"
|
|
2007
|
+
/>
|
|
2008
|
+
|
|
2009
|
+
<Tooltip>
|
|
2010
|
+
<TooltipTrigger asChild>
|
|
2011
|
+
<Button
|
|
2012
|
+
type="button"
|
|
2013
|
+
variant="ghost"
|
|
2014
|
+
size="icon"
|
|
1952
2015
|
className={cn(
|
|
1953
|
-
'h-
|
|
1954
|
-
|
|
2016
|
+
'h-8 w-8',
|
|
2017
|
+
contact.is_primary && 'text-amber-500'
|
|
1955
2018
|
)}
|
|
1956
|
-
|
|
1957
|
-
|
|
1958
|
-
|
|
1959
|
-
|
|
1960
|
-
|
|
1961
|
-
|
|
1962
|
-
|
|
1963
|
-
|
|
1964
|
-
|
|
1965
|
-
|
|
1966
|
-
|
|
1967
|
-
|
|
1968
|
-
|
|
1969
|
-
|
|
1970
|
-
|
|
1971
|
-
|
|
1972
|
-
|
|
1973
|
-
|
|
1974
|
-
|
|
1975
|
-
|
|
1976
|
-
|
|
2019
|
+
onClick={() =>
|
|
2020
|
+
setPrimaryContact(contact.clientId)
|
|
2021
|
+
}
|
|
2022
|
+
aria-label={t('main')}
|
|
2023
|
+
>
|
|
2024
|
+
<Star
|
|
2025
|
+
className={cn(
|
|
2026
|
+
'h-4 w-4',
|
|
2027
|
+
contact.is_primary && 'fill-current'
|
|
2028
|
+
)}
|
|
2029
|
+
/>
|
|
2030
|
+
</Button>
|
|
2031
|
+
</TooltipTrigger>
|
|
2032
|
+
<TooltipContent>{t('main')}</TooltipContent>
|
|
2033
|
+
</Tooltip>
|
|
2034
|
+
|
|
2035
|
+
<Tooltip>
|
|
2036
|
+
<TooltipTrigger asChild>
|
|
2037
|
+
<Button
|
|
2038
|
+
type="button"
|
|
2039
|
+
variant="ghost"
|
|
2040
|
+
size="icon"
|
|
2041
|
+
className="h-8 w-8 text-red-500 hover:text-red-600"
|
|
2042
|
+
onClick={() => removeContact(contact.clientId)}
|
|
2043
|
+
aria-label={t('remove')}
|
|
2044
|
+
>
|
|
2045
|
+
<Trash2 className="h-4 w-4" />
|
|
2046
|
+
</Button>
|
|
2047
|
+
</TooltipTrigger>
|
|
2048
|
+
<TooltipContent>{t('remove')}</TooltipContent>
|
|
2049
|
+
</Tooltip>
|
|
2050
|
+
</div>
|
|
1977
2051
|
</div>
|
|
2052
|
+
))
|
|
2053
|
+
)}
|
|
2054
|
+
</CollapsibleContent>
|
|
2055
|
+
</Collapsible>
|
|
2056
|
+
|
|
2057
|
+
<Separator />
|
|
2058
|
+
|
|
2059
|
+
<Collapsible open={addressesOpen} onOpenChange={setAddressesOpen}>
|
|
2060
|
+
<CollapsibleTrigger asChild>
|
|
2061
|
+
<div className="group flex cursor-pointer items-center justify-between">
|
|
2062
|
+
<div className="flex items-center gap-2">
|
|
2063
|
+
<MapPin className="h-4 w-4 text-green-500" />
|
|
2064
|
+
<h3 className="text-sm font-semibold">
|
|
2065
|
+
{t('tabAddresses')}
|
|
2066
|
+
</h3>
|
|
2067
|
+
{addresses.length > 0 ? (
|
|
2068
|
+
<Badge
|
|
2069
|
+
variant="secondary"
|
|
2070
|
+
className="bg-green-500/10 text-green-600"
|
|
2071
|
+
>
|
|
2072
|
+
{addresses.length}
|
|
2073
|
+
</Badge>
|
|
2074
|
+
) : null}
|
|
2075
|
+
</div>
|
|
1978
2076
|
|
|
1979
|
-
|
|
1980
|
-
|
|
1981
|
-
|
|
1982
|
-
|
|
1983
|
-
|
|
1984
|
-
|
|
1985
|
-
|
|
1986
|
-
|
|
1987
|
-
|
|
1988
|
-
|
|
1989
|
-
|
|
1990
|
-
|
|
1991
|
-
|
|
1992
|
-
|
|
1993
|
-
|
|
1994
|
-
/>
|
|
1995
|
-
|
|
1996
|
-
|
|
1997
|
-
|
|
1998
|
-
|
|
2077
|
+
<div className="flex items-center gap-2">
|
|
2078
|
+
<Button
|
|
2079
|
+
type="button"
|
|
2080
|
+
variant="ghost"
|
|
2081
|
+
size="sm"
|
|
2082
|
+
className="h-7 px-2 text-xs"
|
|
2083
|
+
onClick={(event) => {
|
|
2084
|
+
event.stopPropagation();
|
|
2085
|
+
addAddress();
|
|
2086
|
+
}}
|
|
2087
|
+
>
|
|
2088
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
2089
|
+
{t('addAddress')}
|
|
2090
|
+
</Button>
|
|
2091
|
+
{addressesOpen ? (
|
|
2092
|
+
<ChevronUp className="h-4 w-4" />
|
|
2093
|
+
) : (
|
|
2094
|
+
<ChevronDown className="h-4 w-4" />
|
|
2095
|
+
)}
|
|
2096
|
+
</div>
|
|
2097
|
+
</div>
|
|
2098
|
+
</CollapsibleTrigger>
|
|
1999
2099
|
|
|
2000
|
-
|
|
2001
|
-
|
|
2002
|
-
|
|
2003
|
-
|
|
2004
|
-
|
|
2005
|
-
|
|
2006
|
-
|
|
2007
|
-
|
|
2008
|
-
|
|
2009
|
-
|
|
2100
|
+
<CollapsibleContent className="mt-2 space-y-2">
|
|
2101
|
+
{addresses.length === 0 ? (
|
|
2102
|
+
<div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
|
|
2103
|
+
{t('noAddresses')}
|
|
2104
|
+
</div>
|
|
2105
|
+
) : (
|
|
2106
|
+
addresses.map((address) => (
|
|
2107
|
+
<div
|
|
2108
|
+
key={address.clientId}
|
|
2109
|
+
className={cn(
|
|
2110
|
+
'space-y-2 rounded-lg border p-2',
|
|
2111
|
+
address.is_primary &&
|
|
2112
|
+
'border-green-500/50 bg-green-500/5'
|
|
2113
|
+
)}
|
|
2114
|
+
>
|
|
2115
|
+
<div className="flex items-center gap-2">
|
|
2116
|
+
<Select
|
|
2117
|
+
value={address.address_type}
|
|
2118
|
+
onValueChange={(value) =>
|
|
2119
|
+
updateAddress(address.clientId, {
|
|
2120
|
+
address_type:
|
|
2121
|
+
value as PersonAddress['address_type'],
|
|
2122
|
+
})
|
|
2123
|
+
}
|
|
2124
|
+
>
|
|
2125
|
+
<SelectTrigger className="h-8 w-36 text-xs">
|
|
2126
|
+
<SelectValue />
|
|
2127
|
+
</SelectTrigger>
|
|
2128
|
+
<SelectContent>
|
|
2129
|
+
{ADDRESS_TYPE_OPTIONS.map((option) => (
|
|
2130
|
+
<SelectItem
|
|
2131
|
+
key={option.value}
|
|
2132
|
+
value={option.value}
|
|
2133
|
+
>
|
|
2134
|
+
{t(option.labelKey)}
|
|
2135
|
+
</SelectItem>
|
|
2136
|
+
))}
|
|
2137
|
+
</SelectContent>
|
|
2138
|
+
</Select>
|
|
2139
|
+
|
|
2140
|
+
<Input
|
|
2141
|
+
placeholder={t('zipCode')}
|
|
2142
|
+
value={address.postal_code || ''}
|
|
2143
|
+
maxLength={9}
|
|
2144
|
+
onChange={(event) =>
|
|
2145
|
+
void handleCEP(event, address.clientId)
|
|
2146
|
+
}
|
|
2147
|
+
className="h-8 w-28 text-xs"
|
|
2148
|
+
/>
|
|
2149
|
+
|
|
2150
|
+
<Tooltip>
|
|
2151
|
+
<TooltipTrigger asChild>
|
|
2152
|
+
<Button
|
|
2153
|
+
type="button"
|
|
2154
|
+
variant="ghost"
|
|
2155
|
+
size="icon"
|
|
2156
|
+
className={cn(
|
|
2157
|
+
'h-8 w-8',
|
|
2158
|
+
address.is_primary && 'text-amber-500'
|
|
2159
|
+
)}
|
|
2160
|
+
onClick={() =>
|
|
2161
|
+
setPrimaryAddress(address.clientId)
|
|
2162
|
+
}
|
|
2163
|
+
aria-label={t('main')}
|
|
2164
|
+
>
|
|
2165
|
+
<Star
|
|
2166
|
+
className={cn(
|
|
2167
|
+
'h-4 w-4',
|
|
2168
|
+
address.is_primary && 'fill-current'
|
|
2169
|
+
)}
|
|
2170
|
+
/>
|
|
2171
|
+
</Button>
|
|
2172
|
+
</TooltipTrigger>
|
|
2173
|
+
<TooltipContent>{t('main')}</TooltipContent>
|
|
2174
|
+
</Tooltip>
|
|
2175
|
+
|
|
2176
|
+
<Tooltip>
|
|
2177
|
+
<TooltipTrigger asChild>
|
|
2178
|
+
<Button
|
|
2179
|
+
type="button"
|
|
2180
|
+
variant="ghost"
|
|
2181
|
+
size="icon"
|
|
2182
|
+
className="h-8 w-8 text-red-500 hover:text-red-600"
|
|
2183
|
+
onClick={() => removeAddress(address.clientId)}
|
|
2184
|
+
aria-label={t('remove')}
|
|
2185
|
+
>
|
|
2186
|
+
<Trash2 className="h-4 w-4" />
|
|
2187
|
+
</Button>
|
|
2188
|
+
</TooltipTrigger>
|
|
2189
|
+
<TooltipContent>{t('remove')}</TooltipContent>
|
|
2190
|
+
</Tooltip>
|
|
2191
|
+
</div>
|
|
2192
|
+
|
|
2193
|
+
<div className="relative">
|
|
2194
|
+
<Input
|
|
2195
|
+
ref={(element) => {
|
|
2196
|
+
addressLine1Refs.current[address.clientId] =
|
|
2197
|
+
element;
|
|
2198
|
+
}}
|
|
2199
|
+
placeholder={t('addressPlaceholder')}
|
|
2200
|
+
value={address.line1}
|
|
2201
|
+
disabled={Boolean(loadingCEP[address.clientId])}
|
|
2202
|
+
onChange={(event) =>
|
|
2203
|
+
updateAddress(address.clientId, {
|
|
2204
|
+
line1: event.target.value,
|
|
2205
|
+
})
|
|
2206
|
+
}
|
|
2207
|
+
className="h-8 text-xs"
|
|
2208
|
+
/>
|
|
2209
|
+
{loadingCEP[address.clientId] ? (
|
|
2210
|
+
<Loader2 className="absolute top-1/2 right-3 h-4 w-4 -translate-y-1/2 animate-spin text-muted-foreground" />
|
|
2211
|
+
) : null}
|
|
2212
|
+
</div>
|
|
2010
2213
|
|
|
2011
|
-
<div className="grid grid-cols-2 gap-2">
|
|
2012
|
-
<Input
|
|
2013
|
-
placeholder={t('addressCityPlaceholder')}
|
|
2014
|
-
value={address.city}
|
|
2015
|
-
disabled={Boolean(loadingCEP[address.clientId])}
|
|
2016
|
-
onChange={(event) =>
|
|
2017
|
-
updateAddress(address.clientId, {
|
|
2018
|
-
city: event.target.value,
|
|
2019
|
-
})
|
|
2020
|
-
}
|
|
2021
|
-
className="h-8 text-xs"
|
|
2022
|
-
/>
|
|
2023
2214
|
<Input
|
|
2024
|
-
placeholder={t('
|
|
2025
|
-
value={address.
|
|
2026
|
-
disabled={Boolean(loadingCEP[address.clientId])}
|
|
2215
|
+
placeholder={t('addressComplementPlaceholder')}
|
|
2216
|
+
value={address.line2 || ''}
|
|
2027
2217
|
onChange={(event) =>
|
|
2028
2218
|
updateAddress(address.clientId, {
|
|
2029
|
-
|
|
2219
|
+
line2: event.target.value,
|
|
2030
2220
|
})
|
|
2031
2221
|
}
|
|
2032
2222
|
className="h-8 text-xs"
|
|
2033
2223
|
/>
|
|
2034
|
-
</div>
|
|
2035
2224
|
|
|
2036
|
-
|
|
2037
|
-
|
|
2038
|
-
|
|
2039
|
-
|
|
2040
|
-
|
|
2041
|
-
|
|
2042
|
-
|
|
2043
|
-
|
|
2044
|
-
|
|
2045
|
-
|
|
2046
|
-
|
|
2225
|
+
<div className="grid grid-cols-2 gap-2">
|
|
2226
|
+
<Input
|
|
2227
|
+
placeholder={t('addressCityPlaceholder')}
|
|
2228
|
+
value={address.city}
|
|
2229
|
+
disabled={Boolean(loadingCEP[address.clientId])}
|
|
2230
|
+
onChange={(event) =>
|
|
2231
|
+
updateAddress(address.clientId, {
|
|
2232
|
+
city: event.target.value,
|
|
2233
|
+
})
|
|
2234
|
+
}
|
|
2235
|
+
className="h-8 text-xs"
|
|
2047
2236
|
/>
|
|
2048
|
-
|
|
2049
|
-
|
|
2050
|
-
|
|
2051
|
-
|
|
2052
|
-
|
|
2053
|
-
|
|
2054
|
-
|
|
2055
|
-
|
|
2056
|
-
|
|
2057
|
-
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
</CollapsibleContent>
|
|
2061
|
-
</Collapsible>
|
|
2062
|
-
|
|
2063
|
-
<Separator />
|
|
2064
|
-
|
|
2065
|
-
<Collapsible open={documentsOpen} onOpenChange={setDocumentsOpen}>
|
|
2066
|
-
<CollapsibleTrigger asChild>
|
|
2067
|
-
<div className="group flex cursor-pointer flex-wrap items-center gap-2">
|
|
2068
|
-
<div className="flex min-w-0 items-center gap-2">
|
|
2069
|
-
<FileText className="h-4 w-4 text-amber-500" />
|
|
2070
|
-
<h3 className="text-sm font-semibold">
|
|
2071
|
-
{t('tabDocuments')}
|
|
2072
|
-
</h3>
|
|
2073
|
-
{documents.length > 0 ? (
|
|
2074
|
-
<Badge
|
|
2075
|
-
variant="secondary"
|
|
2076
|
-
className="bg-amber-500/10 text-amber-600"
|
|
2077
|
-
>
|
|
2078
|
-
{documents.length}
|
|
2079
|
-
</Badge>
|
|
2080
|
-
) : null}
|
|
2081
|
-
</div>
|
|
2082
|
-
|
|
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>
|
|
2138
|
-
{documentsOpen ? (
|
|
2139
|
-
<ChevronUp className="h-4 w-4" />
|
|
2140
|
-
) : (
|
|
2141
|
-
<ChevronDown className="h-4 w-4" />
|
|
2142
|
-
)}
|
|
2143
|
-
</div>
|
|
2144
|
-
</div>
|
|
2145
|
-
</CollapsibleTrigger>
|
|
2237
|
+
<Input
|
|
2238
|
+
placeholder={t('addressStatePlaceholder')}
|
|
2239
|
+
value={address.state}
|
|
2240
|
+
disabled={Boolean(loadingCEP[address.clientId])}
|
|
2241
|
+
onChange={(event) =>
|
|
2242
|
+
updateAddress(address.clientId, {
|
|
2243
|
+
state: event.target.value,
|
|
2244
|
+
})
|
|
2245
|
+
}
|
|
2246
|
+
className="h-8 text-xs"
|
|
2247
|
+
/>
|
|
2248
|
+
</div>
|
|
2146
2249
|
|
|
2147
|
-
<CollapsibleContent className="mt-2 space-y-2">
|
|
2148
|
-
{documents.length === 0 ? (
|
|
2149
|
-
<div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
|
|
2150
|
-
{t('noDocuments')}
|
|
2151
|
-
</div>
|
|
2152
|
-
) : (
|
|
2153
|
-
documents.map((document) => (
|
|
2154
|
-
<div
|
|
2155
|
-
key={document.clientId}
|
|
2156
|
-
className="rounded-lg border p-2"
|
|
2157
|
-
>
|
|
2158
|
-
<div className="flex items-center gap-2">
|
|
2159
2250
|
<Select
|
|
2160
|
-
value={
|
|
2161
|
-
|
|
2162
|
-
|
|
2163
|
-
:
|
|
2251
|
+
value={address.country_code || 'BRA'}
|
|
2252
|
+
onValueChange={(value) =>
|
|
2253
|
+
updateAddress(address.clientId, {
|
|
2254
|
+
country_code: value,
|
|
2255
|
+
})
|
|
2164
2256
|
}
|
|
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
2257
|
>
|
|
2176
|
-
<SelectTrigger className="h-8
|
|
2258
|
+
<SelectTrigger className="h-8 text-xs">
|
|
2177
2259
|
<SelectValue
|
|
2178
|
-
placeholder={t('
|
|
2260
|
+
placeholder={t('addressCountryPlaceholder')}
|
|
2179
2261
|
/>
|
|
2180
2262
|
</SelectTrigger>
|
|
2181
2263
|
<SelectContent>
|
|
2182
|
-
{
|
|
2264
|
+
{COUNTRIES.map((country) => (
|
|
2183
2265
|
<SelectItem
|
|
2184
|
-
key={
|
|
2185
|
-
value={
|
|
2266
|
+
key={country.code}
|
|
2267
|
+
value={country.code}
|
|
2186
2268
|
>
|
|
2187
|
-
{
|
|
2269
|
+
{country.name}
|
|
2188
2270
|
</SelectItem>
|
|
2189
2271
|
))}
|
|
2190
2272
|
</SelectContent>
|
|
2191
2273
|
</Select>
|
|
2274
|
+
</div>
|
|
2275
|
+
))
|
|
2276
|
+
)}
|
|
2277
|
+
</CollapsibleContent>
|
|
2278
|
+
</Collapsible>
|
|
2279
|
+
|
|
2280
|
+
<Separator />
|
|
2281
|
+
|
|
2282
|
+
<Collapsible open={documentsOpen} onOpenChange={setDocumentsOpen}>
|
|
2283
|
+
<CollapsibleTrigger asChild>
|
|
2284
|
+
<div className="group flex cursor-pointer flex-wrap items-center gap-2">
|
|
2285
|
+
<div className="flex min-w-0 items-center gap-2">
|
|
2286
|
+
<FileText className="h-4 w-4 text-amber-500" />
|
|
2287
|
+
<h3 className="text-sm font-semibold">
|
|
2288
|
+
{t('tabDocuments')}
|
|
2289
|
+
</h3>
|
|
2290
|
+
{documents.length > 0 ? (
|
|
2291
|
+
<Badge
|
|
2292
|
+
variant="secondary"
|
|
2293
|
+
className="bg-amber-500/10 text-amber-600"
|
|
2294
|
+
>
|
|
2295
|
+
{documents.length}
|
|
2296
|
+
</Badge>
|
|
2297
|
+
) : null}
|
|
2298
|
+
</div>
|
|
2192
2299
|
|
|
2193
|
-
|
|
2194
|
-
|
|
2195
|
-
|
|
2196
|
-
|
|
2197
|
-
|
|
2198
|
-
|
|
2199
|
-
|
|
2200
|
-
|
|
2201
|
-
|
|
2202
|
-
|
|
2203
|
-
|
|
2204
|
-
|
|
2205
|
-
|
|
2206
|
-
|
|
2207
|
-
|
|
2208
|
-
|
|
2209
|
-
|
|
2300
|
+
<div className="ml-auto flex max-w-full flex-wrap items-center justify-end gap-1 sm:flex-nowrap">
|
|
2301
|
+
<Button
|
|
2302
|
+
type="button"
|
|
2303
|
+
variant="ghost"
|
|
2304
|
+
size="sm"
|
|
2305
|
+
className="h-7 shrink-0 px-2 text-xs"
|
|
2306
|
+
onClick={(event) => {
|
|
2307
|
+
event.stopPropagation();
|
|
2308
|
+
addDocument('cpf');
|
|
2309
|
+
}}
|
|
2310
|
+
>
|
|
2311
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
2312
|
+
CPF
|
|
2313
|
+
</Button>
|
|
2314
|
+
<Button
|
|
2315
|
+
type="button"
|
|
2316
|
+
variant="ghost"
|
|
2317
|
+
size="sm"
|
|
2318
|
+
className="h-7 shrink-0 px-2 text-xs"
|
|
2319
|
+
onClick={(event) => {
|
|
2320
|
+
event.stopPropagation();
|
|
2321
|
+
addDocument('cnpj');
|
|
2322
|
+
}}
|
|
2323
|
+
>
|
|
2324
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
2325
|
+
CNPJ
|
|
2326
|
+
</Button>
|
|
2327
|
+
<Button
|
|
2328
|
+
type="button"
|
|
2329
|
+
variant="ghost"
|
|
2330
|
+
size="sm"
|
|
2331
|
+
className="h-7 shrink-0 px-2 text-xs"
|
|
2332
|
+
onClick={(event) => {
|
|
2333
|
+
event.stopPropagation();
|
|
2334
|
+
addDocument('rg');
|
|
2335
|
+
}}
|
|
2336
|
+
>
|
|
2337
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
2338
|
+
RG
|
|
2339
|
+
</Button>
|
|
2340
|
+
<Button
|
|
2341
|
+
type="button"
|
|
2342
|
+
variant="ghost"
|
|
2343
|
+
size="sm"
|
|
2344
|
+
className="h-7 shrink-0 px-2 text-xs"
|
|
2345
|
+
onClick={(event) => {
|
|
2346
|
+
event.stopPropagation();
|
|
2347
|
+
addDocument('blank');
|
|
2348
|
+
}}
|
|
2349
|
+
>
|
|
2350
|
+
<Plus className="mr-1 h-3.5 w-3.5" />
|
|
2351
|
+
<span className="max-w-20 truncate sm:max-w-none">
|
|
2352
|
+
{t('addDocument')}
|
|
2353
|
+
</span>
|
|
2354
|
+
</Button>
|
|
2355
|
+
{documentsOpen ? (
|
|
2356
|
+
<ChevronUp className="h-4 w-4" />
|
|
2357
|
+
) : (
|
|
2358
|
+
<ChevronDown className="h-4 w-4" />
|
|
2359
|
+
)}
|
|
2360
|
+
</div>
|
|
2361
|
+
</div>
|
|
2362
|
+
</CollapsibleTrigger>
|
|
2210
2363
|
|
|
2211
|
-
|
|
2212
|
-
|
|
2213
|
-
|
|
2214
|
-
|
|
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>
|
|
2226
|
-
</div>
|
|
2364
|
+
<CollapsibleContent className="mt-2 space-y-2">
|
|
2365
|
+
{documents.length === 0 ? (
|
|
2366
|
+
<div className="rounded-lg border-2 border-dashed py-4 text-center text-sm text-muted-foreground">
|
|
2367
|
+
{t('noDocuments')}
|
|
2227
2368
|
</div>
|
|
2228
|
-
)
|
|
2229
|
-
|
|
2230
|
-
|
|
2231
|
-
|
|
2232
|
-
|
|
2233
|
-
|
|
2369
|
+
) : (
|
|
2370
|
+
documents.map((document) => (
|
|
2371
|
+
<div
|
|
2372
|
+
key={document.clientId}
|
|
2373
|
+
className="rounded-lg border p-2"
|
|
2374
|
+
>
|
|
2375
|
+
<div className="flex items-center gap-2">
|
|
2376
|
+
<Select
|
|
2377
|
+
value={
|
|
2378
|
+
document.document_type_id
|
|
2379
|
+
? String(document.document_type_id)
|
|
2380
|
+
: undefined
|
|
2381
|
+
}
|
|
2382
|
+
onValueChange={(value) => {
|
|
2383
|
+
const nextTypeId = Number(value);
|
|
2384
|
+
updateDocument(document.clientId, {
|
|
2385
|
+
document_type_id: nextTypeId,
|
|
2386
|
+
value: maskDocumentValueByType(
|
|
2387
|
+
document.value || '',
|
|
2388
|
+
nextTypeId
|
|
2389
|
+
),
|
|
2390
|
+
});
|
|
2391
|
+
}}
|
|
2392
|
+
>
|
|
2393
|
+
<SelectTrigger className="h-8 w-36 text-xs">
|
|
2394
|
+
<SelectValue
|
|
2395
|
+
placeholder={t('selectDocumentType')}
|
|
2396
|
+
/>
|
|
2397
|
+
</SelectTrigger>
|
|
2398
|
+
<SelectContent>
|
|
2399
|
+
{documentTypes.map((documentType) => (
|
|
2400
|
+
<SelectItem
|
|
2401
|
+
key={documentType.document_type_id}
|
|
2402
|
+
value={String(documentType.document_type_id)}
|
|
2403
|
+
>
|
|
2404
|
+
{documentType.name}
|
|
2405
|
+
</SelectItem>
|
|
2406
|
+
))}
|
|
2407
|
+
</SelectContent>
|
|
2408
|
+
</Select>
|
|
2409
|
+
|
|
2410
|
+
<Input
|
|
2411
|
+
ref={(element) => {
|
|
2412
|
+
documentValueRefs.current[document.clientId] =
|
|
2413
|
+
element;
|
|
2414
|
+
}}
|
|
2415
|
+
placeholder={t('documentValuePlaceholder')}
|
|
2416
|
+
value={document.value}
|
|
2417
|
+
onChange={(event) =>
|
|
2418
|
+
updateDocument(document.clientId, {
|
|
2419
|
+
value: maskDocumentValueByType(
|
|
2420
|
+
event.target.value,
|
|
2421
|
+
document.document_type_id
|
|
2422
|
+
),
|
|
2423
|
+
})
|
|
2424
|
+
}
|
|
2425
|
+
className="h-8 flex-1 text-xs"
|
|
2426
|
+
/>
|
|
2427
|
+
|
|
2428
|
+
<Tooltip>
|
|
2429
|
+
<TooltipTrigger asChild>
|
|
2430
|
+
<Button
|
|
2431
|
+
type="button"
|
|
2432
|
+
variant="ghost"
|
|
2433
|
+
size="icon"
|
|
2434
|
+
className="h-8 w-8 text-red-500 hover:text-red-600"
|
|
2435
|
+
onClick={() =>
|
|
2436
|
+
removeDocument(document.clientId)
|
|
2437
|
+
}
|
|
2438
|
+
aria-label={t('remove')}
|
|
2439
|
+
>
|
|
2440
|
+
<Trash2 className="h-4 w-4" />
|
|
2441
|
+
</Button>
|
|
2442
|
+
</TooltipTrigger>
|
|
2443
|
+
<TooltipContent>{t('remove')}</TooltipContent>
|
|
2444
|
+
</Tooltip>
|
|
2445
|
+
</div>
|
|
2446
|
+
</div>
|
|
2447
|
+
))
|
|
2448
|
+
)}
|
|
2449
|
+
</CollapsibleContent>
|
|
2450
|
+
</Collapsible>
|
|
2451
|
+
</form>
|
|
2452
|
+
</div>
|
|
2234
2453
|
|
|
2235
|
-
|
|
2236
|
-
|
|
2237
|
-
|
|
2454
|
+
<div className="shrink-0 space-y-2 border-t p-4">
|
|
2455
|
+
<div className="grid gap-2 sm:grid-cols-2">
|
|
2456
|
+
{!isEditing ? (
|
|
2457
|
+
<Button
|
|
2458
|
+
type="button"
|
|
2459
|
+
variant="outline"
|
|
2460
|
+
onClick={() => {
|
|
2461
|
+
const formElement = document.getElementById(
|
|
2462
|
+
'person-form'
|
|
2463
|
+
) as HTMLFormElement | null;
|
|
2464
|
+
formElement?.requestSubmit();
|
|
2465
|
+
}}
|
|
2466
|
+
disabled={isSubmitting || isUploadingAvatar}
|
|
2467
|
+
>
|
|
2468
|
+
{t('saveAndNew')}
|
|
2469
|
+
</Button>
|
|
2470
|
+
) : null}
|
|
2238
2471
|
<Button
|
|
2239
|
-
type="
|
|
2240
|
-
|
|
2241
|
-
|
|
2242
|
-
const formElement = document.getElementById(
|
|
2243
|
-
'person-form'
|
|
2244
|
-
) as HTMLFormElement | null;
|
|
2245
|
-
formElement?.requestSubmit();
|
|
2246
|
-
}}
|
|
2472
|
+
type="submit"
|
|
2473
|
+
form="person-form"
|
|
2474
|
+
className="w-full"
|
|
2247
2475
|
disabled={isSubmitting || isUploadingAvatar}
|
|
2248
2476
|
>
|
|
2249
|
-
{
|
|
2477
|
+
{isSubmitting ? (
|
|
2478
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
2479
|
+
) : (
|
|
2480
|
+
<Save className="mr-2 h-4 w-4" />
|
|
2481
|
+
)}
|
|
2482
|
+
{isSubmitting
|
|
2483
|
+
? t('saving')
|
|
2484
|
+
: isEditing
|
|
2485
|
+
? t('saveChanges')
|
|
2486
|
+
: t('createPerson')}
|
|
2250
2487
|
</Button>
|
|
2251
|
-
|
|
2488
|
+
</div>
|
|
2489
|
+
<p className="text-xs text-muted-foreground">
|
|
2490
|
+
{t('formShortcutsHint')}
|
|
2491
|
+
</p>
|
|
2492
|
+
</div>
|
|
2493
|
+
</SheetContent>
|
|
2494
|
+
</Sheet>
|
|
2495
|
+
|
|
2496
|
+
<AlertDialog
|
|
2497
|
+
open={duplicateDialogOpen}
|
|
2498
|
+
onOpenChange={(nextOpen: boolean) => {
|
|
2499
|
+
if (isResolvingDuplicateAction) return;
|
|
2500
|
+
|
|
2501
|
+
setDuplicateDialogOpen(nextOpen);
|
|
2502
|
+
if (!nextOpen) {
|
|
2503
|
+
setPendingDuplicateSubmission(null);
|
|
2504
|
+
setDuplicateTargetPersonId(null);
|
|
2505
|
+
}
|
|
2506
|
+
}}
|
|
2507
|
+
>
|
|
2508
|
+
<AlertDialogContent>
|
|
2509
|
+
<AlertDialogHeader>
|
|
2510
|
+
<AlertDialogTitle>{t('duplicateDialogTitle')}</AlertDialogTitle>
|
|
2511
|
+
<AlertDialogDescription>
|
|
2512
|
+
{t('duplicateDialogDescription')}
|
|
2513
|
+
</AlertDialogDescription>
|
|
2514
|
+
</AlertDialogHeader>
|
|
2515
|
+
|
|
2516
|
+
{pendingDuplicateSubmission ? (
|
|
2517
|
+
<div className="space-y-3">
|
|
2518
|
+
<div className="space-y-1.5">
|
|
2519
|
+
<Label className="text-xs font-medium">
|
|
2520
|
+
{t('duplicateTargetLabel')}
|
|
2521
|
+
</Label>
|
|
2522
|
+
<Select
|
|
2523
|
+
value={
|
|
2524
|
+
duplicateTargetPersonId
|
|
2525
|
+
? String(duplicateTargetPersonId)
|
|
2526
|
+
: undefined
|
|
2527
|
+
}
|
|
2528
|
+
onValueChange={(value) =>
|
|
2529
|
+
setDuplicateTargetPersonId(Number(value))
|
|
2530
|
+
}
|
|
2531
|
+
>
|
|
2532
|
+
<SelectTrigger className="h-9 w-full">
|
|
2533
|
+
<SelectValue placeholder={t('duplicateSelectTarget')} />
|
|
2534
|
+
</SelectTrigger>
|
|
2535
|
+
<SelectContent>
|
|
2536
|
+
{pendingDuplicateSubmission.matches.map((match) => (
|
|
2537
|
+
<SelectItem key={match.id} value={String(match.id)}>
|
|
2538
|
+
#{match.id} · {match.name}
|
|
2539
|
+
</SelectItem>
|
|
2540
|
+
))}
|
|
2541
|
+
</SelectContent>
|
|
2542
|
+
</Select>
|
|
2543
|
+
</div>
|
|
2544
|
+
|
|
2545
|
+
<div className="max-h-56 space-y-2 overflow-y-auto rounded-md border p-2">
|
|
2546
|
+
{pendingDuplicateSubmission.matches.map((match) => (
|
|
2547
|
+
<div
|
|
2548
|
+
key={match.id}
|
|
2549
|
+
className={cn(
|
|
2550
|
+
'rounded-md border p-2',
|
|
2551
|
+
duplicateTargetPersonId === match.id
|
|
2552
|
+
? 'border-primary/50 bg-primary/5'
|
|
2553
|
+
: 'border-border'
|
|
2554
|
+
)}
|
|
2555
|
+
>
|
|
2556
|
+
<div className="text-sm font-medium">
|
|
2557
|
+
#{match.id} · {match.name}
|
|
2558
|
+
</div>
|
|
2559
|
+
<div className="mt-1 flex flex-wrap gap-1.5">
|
|
2560
|
+
{match.reasons.map((reason) => (
|
|
2561
|
+
<Badge
|
|
2562
|
+
key={`${match.id}-${reason}`}
|
|
2563
|
+
variant="secondary"
|
|
2564
|
+
>
|
|
2565
|
+
{getDuplicateReasonLabel(reason)}
|
|
2566
|
+
</Badge>
|
|
2567
|
+
))}
|
|
2568
|
+
</div>
|
|
2569
|
+
</div>
|
|
2570
|
+
))}
|
|
2571
|
+
</div>
|
|
2572
|
+
</div>
|
|
2573
|
+
) : null}
|
|
2574
|
+
|
|
2575
|
+
<AlertDialogFooter>
|
|
2576
|
+
<Button
|
|
2577
|
+
type="button"
|
|
2578
|
+
variant="outline"
|
|
2579
|
+
onClick={() => {
|
|
2580
|
+
setDuplicateDialogOpen(false);
|
|
2581
|
+
setPendingDuplicateSubmission(null);
|
|
2582
|
+
setDuplicateTargetPersonId(null);
|
|
2583
|
+
}}
|
|
2584
|
+
disabled={isResolvingDuplicateAction}
|
|
2585
|
+
>
|
|
2586
|
+
{t('cancel')}
|
|
2587
|
+
</Button>
|
|
2252
2588
|
<Button
|
|
2253
|
-
type="
|
|
2254
|
-
|
|
2255
|
-
|
|
2256
|
-
disabled={
|
|
2589
|
+
type="button"
|
|
2590
|
+
variant="outline"
|
|
2591
|
+
onClick={() => void handleContinueWithDuplicate()}
|
|
2592
|
+
disabled={isResolvingDuplicateAction}
|
|
2257
2593
|
>
|
|
2258
|
-
{
|
|
2594
|
+
{isResolvingDuplicateAction ? (
|
|
2259
2595
|
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
2260
|
-
) :
|
|
2261
|
-
|
|
2262
|
-
)}
|
|
2263
|
-
{isSubmitting
|
|
2264
|
-
? t('saving')
|
|
2265
|
-
: isEditing
|
|
2266
|
-
? t('saveChanges')
|
|
2267
|
-
: t('createPerson')}
|
|
2596
|
+
) : null}
|
|
2597
|
+
{t('duplicateContinueAction')}
|
|
2268
2598
|
</Button>
|
|
2269
|
-
|
|
2270
|
-
|
|
2271
|
-
|
|
2272
|
-
|
|
2273
|
-
|
|
2274
|
-
|
|
2275
|
-
|
|
2599
|
+
<Button
|
|
2600
|
+
type="button"
|
|
2601
|
+
onClick={() => void handleMergeWithDuplicate()}
|
|
2602
|
+
disabled={isResolvingDuplicateAction || !duplicateTargetPersonId}
|
|
2603
|
+
>
|
|
2604
|
+
{isResolvingDuplicateAction ? (
|
|
2605
|
+
<Loader2 className="mr-2 h-4 w-4 animate-spin" />
|
|
2606
|
+
) : null}
|
|
2607
|
+
{t('duplicateMergeAction')}
|
|
2608
|
+
</Button>
|
|
2609
|
+
</AlertDialogFooter>
|
|
2610
|
+
</AlertDialogContent>
|
|
2611
|
+
</AlertDialog>
|
|
2612
|
+
</>
|
|
2276
2613
|
);
|
|
2277
2614
|
}
|